arn-browser 0.0.4 → 0.0.6

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,16 +1,21 @@
1
1
  {
2
2
  "name": "arn-browser",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "A lightweight, browser autmation helper.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
7
+ "files": [
8
+ "src"
9
+ ],
7
10
  "directories": {
8
11
  "test": "test"
9
12
  },
10
13
  "dependencies": {
11
14
  "@aws-sdk/client-ec2": "^3.946.0",
12
15
  "@ghostery/adblocker": "^2.13.0",
16
+ "arn-knexjs": "^0.0.1",
13
17
  "camoufox-js": "^0.8.4",
18
+ "dotenv": "^17.2.3",
14
19
  "fingerprint-injector": "^2.1.78",
15
20
  "https-proxy-agent": "^7.0.6",
16
21
  "node-cache": "^5.1.2",
@@ -0,0 +1,516 @@
1
+ /**
2
+ * HumanCursor - Human-like cursor movements for Playwright
3
+ * Drop-in replacement for Page with human cursor for click/fill/type/hover
4
+ */
5
+
6
+ import { HumanizeMouseTrajectory } from './bezier.js';
7
+ import { generateRandomCurveParameters, calculateAbsoluteOffset } from './randomizer.js';
8
+
9
+ /**
10
+ * Random integer between min and max (inclusive)
11
+ */
12
+ function randInt(min, max) {
13
+ return Math.floor(Math.random() * (max - min + 1)) + min;
14
+ }
15
+
16
+ /**
17
+ * Sleep for specified milliseconds
18
+ */
19
+ function sleep(ms) {
20
+ return new Promise(resolve => setTimeout(resolve, ms));
21
+ }
22
+
23
+ /**
24
+ * Creates a HumanLocator that wraps a Playwright Locator
25
+ * with human-like cursor movement for actions
26
+ */
27
+ function createHumanLocator(cursor, locator) {
28
+ // Methods that need human cursor movement
29
+ const humanMethods = {
30
+ async click(options = {}) {
31
+ // If humanize is disabled, use native Playwright click
32
+ if (!cursor.config.humanize) {
33
+ return await locator.click(options);
34
+ }
35
+
36
+ await cursor._moveToLocator(locator, options);
37
+ const clicks = options.clickCount || 1;
38
+ const button = options.button || 'left';
39
+
40
+ for (let i = 0; i < clicks; i++) {
41
+ if (options.delay) {
42
+ await cursor._page.mouse.down({ button });
43
+ await sleep(options.delay);
44
+ await cursor._page.mouse.up({ button });
45
+ } else {
46
+ await cursor._page.mouse.click(
47
+ cursor.originCoordinates[0],
48
+ cursor.originCoordinates[1],
49
+ { button }
50
+ );
51
+ }
52
+ if (i < clicks - 1) {
53
+ await sleep(randInt(170, 280));
54
+ }
55
+ }
56
+ },
57
+
58
+ async dblclick(options = {}) {
59
+ if (!cursor.config.humanize) {
60
+ return await locator.dblclick(options);
61
+ }
62
+ await cursor._moveToLocator(locator, options);
63
+ await cursor._page.mouse.dblclick(
64
+ cursor.originCoordinates[0],
65
+ cursor.originCoordinates[1]
66
+ );
67
+ },
68
+
69
+ async fill(value, options = {}) {
70
+ if (!cursor.config.humanize) {
71
+ return await locator.fill(value, options);
72
+ }
73
+ // Humanize: Move to element, click, then fill instantly
74
+ await this.click(options);
75
+ return await locator.fill(value, options);
76
+ },
77
+
78
+ async fillSequentially(value, options = {}) {
79
+ if (!cursor.config.humanize) {
80
+ return await locator.fill(value, options);
81
+ }
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);
87
+ },
88
+
89
+ async type(text, options = {}) {
90
+ if (!cursor.config.humanize) {
91
+ return await locator.type(text, options);
92
+ }
93
+ await this.click(options);
94
+ await cursor._type(text, options);
95
+ },
96
+
97
+ async hover(options = {}) {
98
+ if (!cursor.config.humanize) {
99
+ return await locator.hover(options);
100
+ }
101
+ await cursor._moveToLocator(locator, options);
102
+ },
103
+
104
+ async press(key, options = {}) {
105
+ if (!cursor.config.humanize) {
106
+ return await locator.press(key, options);
107
+ }
108
+ await this.click(options);
109
+ await cursor._page.keyboard.press(key);
110
+ },
111
+
112
+ async check(options = {}) {
113
+ if (!cursor.config.humanize) {
114
+ return await locator.check(options);
115
+ }
116
+ if (!(await locator.isChecked())) {
117
+ await this.click(options);
118
+ }
119
+ },
120
+
121
+ async uncheck(options = {}) {
122
+ if (!cursor.config.humanize) {
123
+ return await locator.uncheck(options);
124
+ }
125
+ if (await locator.isChecked()) {
126
+ await this.click(options);
127
+ }
128
+ },
129
+
130
+ async setChecked(checked, options = {}) {
131
+ if (!cursor.config.humanize) {
132
+ return await locator.setChecked(checked, options);
133
+ }
134
+ if (checked) {
135
+ await this.check(options);
136
+ } else {
137
+ await this.uncheck(options);
138
+ }
139
+ },
140
+
141
+ async pressSequentially(text, options = {}) {
142
+ if (!cursor.config.humanize) {
143
+ return await locator.pressSequentially(text, options);
144
+ }
145
+ await this.click(options);
146
+ await cursor._type(text, options);
147
+ },
148
+
149
+ // Chainable methods that return new HumanLocators
150
+ filter(options) {
151
+ return createHumanLocator(cursor, locator.filter(options));
152
+ },
153
+ first() {
154
+ return createHumanLocator(cursor, locator.first());
155
+ },
156
+ last() {
157
+ return createHumanLocator(cursor, locator.last());
158
+ },
159
+ nth(index) {
160
+ return createHumanLocator(cursor, locator.nth(index));
161
+ },
162
+ locator(selector) {
163
+ return createHumanLocator(cursor, locator.locator(selector));
164
+ },
165
+ getByText(text, options) {
166
+ return createHumanLocator(cursor, locator.getByText(text, options));
167
+ },
168
+ getByRole(role, options) {
169
+ return createHumanLocator(cursor, locator.getByRole(role, options));
170
+ },
171
+ getByLabel(text, options) {
172
+ return createHumanLocator(cursor, locator.getByLabel(text, options));
173
+ },
174
+ getByPlaceholder(text, options) {
175
+ return createHumanLocator(cursor, locator.getByPlaceholder(text, options));
176
+ },
177
+ getByTestId(testId) {
178
+ return createHumanLocator(cursor, locator.getByTestId(testId));
179
+ }
180
+ };
181
+
182
+ // Use Proxy to pass through all other methods to original locator
183
+ return new Proxy(locator, {
184
+ get(target, prop) {
185
+ // Human methods override
186
+ if (prop in humanMethods) {
187
+ return humanMethods[prop];
188
+ }
189
+ // Pass through to original locator
190
+ const value = target[prop];
191
+ if (typeof value === 'function') {
192
+ return value.bind(target);
193
+ }
194
+ return value;
195
+ }
196
+ });
197
+ }
198
+
199
+ /**
200
+ * Create a HumanCursor that acts as a drop-in replacement for Page
201
+ * All page methods work, but click/fill/type/check/hover use human cursor
202
+ *
203
+ * @param {import('playwright').Page} page - Playwright Page object
204
+ * @param {Object} options - Configuration options
205
+ * @returns {import('playwright').Page & HumanCursorMethods}
206
+ */
207
+ export function createCursor(page, options = {}) {
208
+ const config = {
209
+ humanize: options.humanize ?? true, // Enable human cursor by default
210
+ maxTime: options.maxTime ?? 1.5,
211
+ minTime: options.minTime ?? 0.5,
212
+ showCursor: options.showCursor ?? true // Show cursor by default
213
+ };
214
+
215
+ // Internal state
216
+ const cursor = {
217
+ _page: page,
218
+ originCoordinates: [0, 0],
219
+ config,
220
+
221
+ async _moveToLocator(locator, options = {}) {
222
+ await locator.scrollIntoViewIfNeeded();
223
+ const bounds = await locator.boundingBox();
224
+
225
+ if (!bounds) {
226
+ throw new Error('Element not found or not visible');
227
+ }
228
+
229
+ let xOffset, yOffset;
230
+ if (options.position) {
231
+ xOffset = options.position.x;
232
+ yOffset = options.position.y;
233
+ } else {
234
+ xOffset = bounds.width * (randInt(25, 75) / 100);
235
+ yOffset = bounds.height * (randInt(25, 75) / 100);
236
+ }
237
+
238
+ const destination = [bounds.x + xOffset, bounds.y + yOffset];
239
+ return await this._moveToPoint(destination, options);
240
+ },
241
+
242
+ async _moveToPoint(destination, options = {}) {
243
+ const viewport = page.viewportSize() || { width: 1280, height: 720 };
244
+
245
+ const params = generateRandomCurveParameters(
246
+ viewport,
247
+ this.originCoordinates,
248
+ destination
249
+ );
250
+
251
+ if (options.steady) {
252
+ params.offsetBoundaryX = 10;
253
+ params.offsetBoundaryY = 10;
254
+ params.distortionMean = 1.2;
255
+ params.distortionStdDev = 1.2;
256
+ params.distortionFrequency = 1;
257
+ }
258
+
259
+ const curve = new HumanizeMouseTrajectory(
260
+ this.originCoordinates,
261
+ destination,
262
+ {
263
+ offsetBoundaryX: params.offsetBoundaryX,
264
+ offsetBoundaryY: params.offsetBoundaryY,
265
+ knotsCount: params.knotsCount,
266
+ distortionMean: params.distortionMean,
267
+ distortionStdDev: params.distortionStdDev,
268
+ distortionFrequency: params.distortionFrequency,
269
+ tweening: params.tween,
270
+ targetPoints: params.targetPoints
271
+ }
272
+ );
273
+
274
+ const distance = Math.sqrt(
275
+ Math.pow(destination[0] - this.originCoordinates[0], 2) +
276
+ Math.pow(destination[1] - this.originCoordinates[1], 2)
277
+ );
278
+ const totalTime = Math.min(
279
+ config.maxTime,
280
+ Math.max(config.minTime, distance / 1000)
281
+ ) * 1000;
282
+ const baseDelay = totalTime / curve.points.length;
283
+ const speedMultiplier = options.moveSpeed || 1.0;
284
+
285
+ for (const point of curve.points) {
286
+ await page.mouse.move(point[0], point[1]);
287
+ if (baseDelay > 0) {
288
+ await sleep((baseDelay / speedMultiplier) + Math.random() * 2);
289
+ }
290
+ }
291
+
292
+ this.originCoordinates = destination;
293
+ return destination;
294
+ },
295
+
296
+ async _type(text, options = {}) {
297
+ const minDelay = options.minDelay ?? 50;
298
+ const maxDelay = options.maxDelay ?? 150;
299
+
300
+ for (const char of text) {
301
+ await page.keyboard.type(char);
302
+ await sleep(randInt(minDelay, maxDelay));
303
+ }
304
+ },
305
+
306
+ async showCursor() {
307
+ await page.evaluate(() => {
308
+ if (window.__humanCursorDot) return;
309
+
310
+ let cursor;
311
+ function updateCursor(event) {
312
+ const x = event.clientX;
313
+ const y = event.clientY;
314
+
315
+ if (!cursor) {
316
+ // Create cursor container
317
+ cursor = document.createElement('div');
318
+ cursor.id = '__human_cursor';
319
+ cursor.style.cssText = `
320
+ position: fixed;
321
+ pointer-events: none;
322
+ z-index: 2147483647;
323
+ transition: left 0.05s ease-out, top 0.05s ease-out;
324
+ `;
325
+
326
+ // Outer ring (animated)
327
+ const ring = document.createElement('div');
328
+ ring.style.cssText = `
329
+ width: 28px;
330
+ height: 28px;
331
+ border: 2px solid rgba(59, 130, 246, 0.6);
332
+ border-radius: 50%;
333
+ box-sizing: border-box;
334
+ position: absolute;
335
+ top: -14px;
336
+ left: -14px;
337
+ animation: pulse 1.5s ease-in-out infinite;
338
+ `;
339
+
340
+ // Inner dot
341
+ const dot = document.createElement('div');
342
+ dot.style.cssText = `
343
+ width: 10px;
344
+ height: 10px;
345
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6);
346
+ border-radius: 50%;
347
+ position: absolute;
348
+ top: -5px;
349
+ left: -5px;
350
+ box-shadow: 0 0 12px rgba(59, 130, 246, 0.8), 0 0 4px rgba(255, 255, 255, 0.9);
351
+ `;
352
+
353
+ // Click indicator
354
+ const clickRing = document.createElement('div');
355
+ clickRing.id = '__human_cursor_click';
356
+ clickRing.style.cssText = `
357
+ width: 36px;
358
+ height: 36px;
359
+ border: 2px solid rgba(239, 68, 68, 0.8);
360
+ border-radius: 50%;
361
+ box-sizing: border-box;
362
+ position: absolute;
363
+ top: -18px;
364
+ left: -18px;
365
+ opacity: 0;
366
+ transform: scale(0.5);
367
+ transition: opacity 0.15s, transform 0.15s;
368
+ `;
369
+
370
+ // Add pulse animation
371
+ const style = document.createElement('style');
372
+ style.textContent = `
373
+ @keyframes pulse {
374
+ 0%, 100% { transform: scale(1); opacity: 0.6; }
375
+ 50% { transform: scale(1.2); opacity: 0.3; }
376
+ }
377
+ `;
378
+ document.head.appendChild(style);
379
+
380
+ cursor.appendChild(ring);
381
+ cursor.appendChild(dot);
382
+ cursor.appendChild(clickRing);
383
+ document.body.appendChild(cursor);
384
+
385
+ // Click effect
386
+ document.addEventListener('mousedown', () => {
387
+ const click = document.getElementById('__human_cursor_click');
388
+ if (click) {
389
+ click.style.opacity = '1';
390
+ click.style.transform = 'scale(1)';
391
+ }
392
+ });
393
+ document.addEventListener('mouseup', () => {
394
+ const click = document.getElementById('__human_cursor_click');
395
+ if (click) {
396
+ click.style.opacity = '0';
397
+ click.style.transform = 'scale(0.5)';
398
+ }
399
+ });
400
+ }
401
+
402
+ cursor.style.left = x + 'px';
403
+ cursor.style.top = y + 'px';
404
+ }
405
+
406
+ document.addEventListener('mousemove', updateCursor);
407
+ window.__humanCursorDot = true;
408
+ });
409
+ }
410
+ };
411
+
412
+ // Methods that return HumanLocators
413
+ const locatorMethods = {
414
+ locator(selector) {
415
+ return createHumanLocator(cursor, page.locator(selector));
416
+ },
417
+ getByText(text, opts) {
418
+ return createHumanLocator(cursor, page.getByText(text, opts));
419
+ },
420
+ getByRole(role, opts) {
421
+ return createHumanLocator(cursor, page.getByRole(role, opts));
422
+ },
423
+ getByLabel(text, opts) {
424
+ return createHumanLocator(cursor, page.getByLabel(text, opts));
425
+ },
426
+ getByPlaceholder(text, opts) {
427
+ return createHumanLocator(cursor, page.getByPlaceholder(text, opts));
428
+ },
429
+ getByAltText(text, opts) {
430
+ return createHumanLocator(cursor, page.getByAltText(text, opts));
431
+ },
432
+ getByTitle(text, opts) {
433
+ return createHumanLocator(cursor, page.getByTitle(text, opts));
434
+ },
435
+ getByTestId(testId) {
436
+ return createHumanLocator(cursor, page.getByTestId(testId));
437
+ }
438
+ };
439
+
440
+ // Wrap goto to auto-show cursor after navigation
441
+ const originalGoto = page.goto.bind(page);
442
+ const wrappedGoto = async (...args) => {
443
+ const result = await originalGoto(...args);
444
+ if (config.showCursor) {
445
+ await cursor.showCursor();
446
+ }
447
+ return result;
448
+ };
449
+
450
+ // Use Proxy to create drop-in page replacement
451
+ return new Proxy(page, {
452
+ get(target, prop) {
453
+ // Override goto to auto-show cursor
454
+ if (prop === 'goto') {
455
+ return wrappedGoto;
456
+ }
457
+ // Locator methods return HumanLocators
458
+ if (prop in locatorMethods) {
459
+ return locatorMethods[prop];
460
+ }
461
+
462
+ // Override Page methods to use HumanLocator
463
+ if (['click', 'dblclick', 'fill', 'hover', 'type', 'press', 'check', 'uncheck'].includes(prop)) {
464
+ return async (selector, ...args) => {
465
+ const humanLocator = createHumanLocator(cursor, page.locator(selector));
466
+ return await humanLocator[prop](...args);
467
+ };
468
+ }
469
+ if (prop === 'fillSequentially') {
470
+ return async (selector, ...args) => {
471
+ const humanLocator = createHumanLocator(cursor, page.locator(selector));
472
+ return await humanLocator.fillSequentially(...args);
473
+ };
474
+ }
475
+ if (prop === 'pressSequentially') {
476
+ return async (selector, ...args) => {
477
+ const humanLocator = createHumanLocator(cursor, page.locator(selector));
478
+ return await humanLocator.pressSequentially(...args);
479
+ };
480
+ }
481
+
482
+ // Expose cursor utilities
483
+ if (prop === 'showCursor') {
484
+ return cursor.showCursor.bind(cursor);
485
+ }
486
+ if (prop === 'originCoordinates') {
487
+ return cursor.originCoordinates;
488
+ }
489
+ if (prop === 'setPosition') {
490
+ return (x, y) => { cursor.originCoordinates = [x, y]; };
491
+ }
492
+ // Pass through everything else to original page
493
+ const value = target[prop];
494
+ if (typeof value === 'function') {
495
+ return value.bind(target);
496
+ }
497
+ return value;
498
+ }
499
+ });
500
+ }
501
+
502
+ // Legacy class export for backwards compatibility
503
+ export class HumanCursor {
504
+ constructor(page, options = {}) {
505
+ this.page = page;
506
+ this.originCoordinates = [0, 0];
507
+ this.options = {
508
+ maxTime: options.maxTime ?? 1.5,
509
+ minTime: options.minTime ?? 0.5,
510
+ showCursor: options.showCursor ?? true // Show cursor by default
511
+ };
512
+
513
+ // Return proxy instead of class instance
514
+ return createCursor(page, options);
515
+ }
516
+ }