arn-browser 0.0.7 → 0.0.9

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.9",
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 =============
@@ -205,43 +205,53 @@ export interface LaunchOptions {
205
205
  }
206
206
 
207
207
  /**
208
- * The object returned by launchBrowser.
208
+ * Successful browser launch result.
209
+ * When launchError is null, browser/context/page are guaranteed to be valid.
209
210
  */
210
- export interface BrowserController {
211
- /**
212
- * The Playwright Browser or BrowserContext instance.
213
- * Note: Multilogin and Persistent profiles return a BrowserContext here.
214
- */
215
- browser: Browser | BrowserContext | null;
216
-
217
- /**
218
- * The active BrowserContext.
219
- */
220
- context: BrowserContext | null;
221
-
211
+ export interface BrowserControllerSuccess {
212
+ /** The Playwright Browser or BrowserContext instance. */
213
+ browser: Browser | BrowserContext;
214
+ /** The active BrowserContext. */
215
+ context: BrowserContext;
222
216
  /**
223
217
  * The initial Page object.
224
218
  * When humanize_options is provided, this will be a HumanPage with human-like cursor methods.
225
219
  * All standard Playwright Page methods are available.
226
220
  */
227
- page: Page | HumanPage | null;
228
-
229
- /**
230
- * Checks if the browser context is currently active.
231
- */
221
+ page: Page | HumanPage;
222
+ /** Returns true since the browser is running. */
232
223
  isBrowserRunning: () => boolean;
233
-
234
- /**
235
- * Safely closes the browser and cleans up temporary directories if applicable.
236
- */
224
+ /** Safely closes the browser and cleans up temporary directories if applicable. */
237
225
  closeBrowser: () => Promise<boolean>;
226
+ /** No error occurred during launch. */
227
+ launchError: null;
228
+ }
238
229
 
239
- /**
240
- * Captures any error that occurred during launch.
241
- */
242
- launchError: Error | null;
230
+ /**
231
+ * Failed browser launch result.
232
+ * When launchError is set, browser/context/page are null.
233
+ */
234
+ export interface BrowserControllerError {
235
+ /** Null on launch failure. */
236
+ browser: null;
237
+ /** Null on launch failure. */
238
+ context: null;
239
+ /** Null on launch failure. */
240
+ page: null;
241
+ /** Returns false since the browser failed to launch. */
242
+ isBrowserRunning: () => boolean;
243
+ /** No-op cleanup function. */
244
+ closeBrowser: () => Promise<boolean>;
245
+ /** The error that occurred during launch. */
246
+ launchError: Error;
243
247
  }
244
248
 
249
+ /**
250
+ * The object returned by launchBrowser.
251
+ * Check launchError to discriminate between success and failure states.
252
+ */
253
+ export type BrowserController = BrowserControllerSuccess | BrowserControllerError;
254
+
245
255
  /**
246
256
  * Launches a browser based on the provided options.
247
257
  */
@@ -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":
@@ -256,7 +258,7 @@ export async function launchBrowser({
256
258
  return browserInstance;
257
259
  } catch (error) {
258
260
  console.error("❌ [LaunchBrowser] Critical Error:", error.message || error);
259
- return { data: null, error: error, closeBrowser: async () => { } };
261
+ return { browser: null, context: null, page: null, isBrowserRunning: () => false, closeBrowser: async () => false, launchError: error };
260
262
  }
261
263
  }
262
264
 
@@ -727,7 +729,7 @@ async function launchExistingMultiloginProfile(profileId, humanize_options = nul
727
729
  try {
728
730
  await stopMultiloginProfile(profileId);
729
731
  } catch (e) { }
730
- return { data: null, error, closeBrowser: async () => { } };
732
+ return { browser: null, context: null, page: null, isBrowserRunning: () => false, closeBrowser: async () => false, launchError: error };
731
733
  }
732
734
  }
733
735
 
@@ -799,7 +801,7 @@ async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, medi
799
801
  } catch (error) {
800
802
  console.error("Quick Profile Error:", error);
801
803
  if (profileId) await stopMultiloginProfile(profileId);
802
- return { data: null, error, closeBrowser: async () => { } };
804
+ return { browser: null, context: null, page: null, isBrowserRunning: () => false, closeBrowser: async () => false, launchError: error };
803
805
  }
804
806
  }
805
807