arn-browser 0.0.4 → 0.0.5

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.5",
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,448 @@
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
+ await this.click(options);
74
+ await cursor._page.keyboard.press('Control+A');
75
+ await sleep(randInt(50, 100));
76
+ await cursor._type(value, options);
77
+ },
78
+
79
+ async type(text, options = {}) {
80
+ if (!cursor.config.humanize) {
81
+ return await locator.type(text, options);
82
+ }
83
+ await this.click(options);
84
+ await cursor._type(text, options);
85
+ },
86
+
87
+ async hover(options = {}) {
88
+ if (!cursor.config.humanize) {
89
+ return await locator.hover(options);
90
+ }
91
+ await cursor._moveToLocator(locator, options);
92
+ },
93
+
94
+ async press(key, options = {}) {
95
+ if (!cursor.config.humanize) {
96
+ return await locator.press(key, options);
97
+ }
98
+ await this.click(options);
99
+ await cursor._page.keyboard.press(key);
100
+ },
101
+
102
+ // Chainable methods that return new HumanLocators
103
+ filter(options) {
104
+ return createHumanLocator(cursor, locator.filter(options));
105
+ },
106
+ first() {
107
+ return createHumanLocator(cursor, locator.first());
108
+ },
109
+ last() {
110
+ return createHumanLocator(cursor, locator.last());
111
+ },
112
+ nth(index) {
113
+ return createHumanLocator(cursor, locator.nth(index));
114
+ },
115
+ locator(selector) {
116
+ return createHumanLocator(cursor, locator.locator(selector));
117
+ },
118
+ getByText(text, options) {
119
+ return createHumanLocator(cursor, locator.getByText(text, options));
120
+ },
121
+ getByRole(role, options) {
122
+ return createHumanLocator(cursor, locator.getByRole(role, options));
123
+ },
124
+ getByLabel(text, options) {
125
+ return createHumanLocator(cursor, locator.getByLabel(text, options));
126
+ },
127
+ getByPlaceholder(text, options) {
128
+ return createHumanLocator(cursor, locator.getByPlaceholder(text, options));
129
+ },
130
+ getByTestId(testId) {
131
+ return createHumanLocator(cursor, locator.getByTestId(testId));
132
+ }
133
+ };
134
+
135
+ // Use Proxy to pass through all other methods to original locator
136
+ return new Proxy(locator, {
137
+ get(target, prop) {
138
+ // Human methods override
139
+ if (prop in humanMethods) {
140
+ return humanMethods[prop];
141
+ }
142
+ // Pass through to original locator
143
+ const value = target[prop];
144
+ if (typeof value === 'function') {
145
+ return value.bind(target);
146
+ }
147
+ return value;
148
+ }
149
+ });
150
+ }
151
+
152
+ /**
153
+ * Create a HumanCursor that acts as a drop-in replacement for Page
154
+ * All page methods work, but click/fill/type/hover use human cursor
155
+ *
156
+ * @param {import('playwright').Page} page - Playwright Page object
157
+ * @param {Object} options - Configuration options
158
+ * @returns {import('playwright').Page & HumanCursorMethods}
159
+ */
160
+ export function createCursor(page, options = {}) {
161
+ const config = {
162
+ humanize: options.humanize ?? true, // Enable human cursor by default
163
+ maxTime: options.maxTime ?? 1.5,
164
+ minTime: options.minTime ?? 0.5,
165
+ showCursor: options.showCursor ?? true // Show cursor by default
166
+ };
167
+
168
+ // Internal state
169
+ const cursor = {
170
+ _page: page,
171
+ originCoordinates: [0, 0],
172
+ config,
173
+
174
+ async _moveToLocator(locator, options = {}) {
175
+ await locator.scrollIntoViewIfNeeded();
176
+ const bounds = await locator.boundingBox();
177
+
178
+ if (!bounds) {
179
+ throw new Error('Element not found or not visible');
180
+ }
181
+
182
+ let xOffset, yOffset;
183
+ if (options.position) {
184
+ xOffset = options.position.x;
185
+ yOffset = options.position.y;
186
+ } else {
187
+ xOffset = bounds.width * (randInt(25, 75) / 100);
188
+ yOffset = bounds.height * (randInt(25, 75) / 100);
189
+ }
190
+
191
+ const destination = [bounds.x + xOffset, bounds.y + yOffset];
192
+ return await this._moveToPoint(destination, options);
193
+ },
194
+
195
+ async _moveToPoint(destination, options = {}) {
196
+ const viewport = page.viewportSize() || { width: 1280, height: 720 };
197
+
198
+ const params = generateRandomCurveParameters(
199
+ viewport,
200
+ this.originCoordinates,
201
+ destination
202
+ );
203
+
204
+ if (options.steady) {
205
+ params.offsetBoundaryX = 10;
206
+ params.offsetBoundaryY = 10;
207
+ params.distortionMean = 1.2;
208
+ params.distortionStdDev = 1.2;
209
+ params.distortionFrequency = 1;
210
+ }
211
+
212
+ const curve = new HumanizeMouseTrajectory(
213
+ this.originCoordinates,
214
+ destination,
215
+ {
216
+ offsetBoundaryX: params.offsetBoundaryX,
217
+ offsetBoundaryY: params.offsetBoundaryY,
218
+ knotsCount: params.knotsCount,
219
+ distortionMean: params.distortionMean,
220
+ distortionStdDev: params.distortionStdDev,
221
+ distortionFrequency: params.distortionFrequency,
222
+ tweening: params.tween,
223
+ targetPoints: params.targetPoints
224
+ }
225
+ );
226
+
227
+ const distance = Math.sqrt(
228
+ Math.pow(destination[0] - this.originCoordinates[0], 2) +
229
+ Math.pow(destination[1] - this.originCoordinates[1], 2)
230
+ );
231
+ const totalTime = Math.min(
232
+ config.maxTime,
233
+ Math.max(config.minTime, distance / 1000)
234
+ ) * 1000;
235
+ const baseDelay = totalTime / curve.points.length;
236
+ const speedMultiplier = options.moveSpeed || 1.0;
237
+
238
+ for (const point of curve.points) {
239
+ await page.mouse.move(point[0], point[1]);
240
+ if (baseDelay > 0) {
241
+ await sleep((baseDelay / speedMultiplier) + Math.random() * 2);
242
+ }
243
+ }
244
+
245
+ this.originCoordinates = destination;
246
+ return destination;
247
+ },
248
+
249
+ async _type(text, options = {}) {
250
+ const minDelay = options.minDelay ?? 50;
251
+ const maxDelay = options.maxDelay ?? 150;
252
+
253
+ for (const char of text) {
254
+ await page.keyboard.type(char);
255
+ await sleep(randInt(minDelay, maxDelay));
256
+ }
257
+ },
258
+
259
+ async showCursor() {
260
+ await page.evaluate(() => {
261
+ if (window.__humanCursorDot) return;
262
+
263
+ let cursor;
264
+ function updateCursor(event) {
265
+ const x = event.clientX;
266
+ const y = event.clientY;
267
+
268
+ if (!cursor) {
269
+ // Create cursor container
270
+ cursor = document.createElement('div');
271
+ cursor.id = '__human_cursor';
272
+ cursor.style.cssText = `
273
+ position: fixed;
274
+ pointer-events: none;
275
+ z-index: 2147483647;
276
+ transition: left 0.05s ease-out, top 0.05s ease-out;
277
+ `;
278
+
279
+ // Outer ring (animated)
280
+ const ring = document.createElement('div');
281
+ ring.style.cssText = `
282
+ width: 28px;
283
+ height: 28px;
284
+ border: 2px solid rgba(59, 130, 246, 0.6);
285
+ border-radius: 50%;
286
+ box-sizing: border-box;
287
+ position: absolute;
288
+ top: -14px;
289
+ left: -14px;
290
+ animation: pulse 1.5s ease-in-out infinite;
291
+ `;
292
+
293
+ // Inner dot
294
+ const dot = document.createElement('div');
295
+ dot.style.cssText = `
296
+ width: 10px;
297
+ height: 10px;
298
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6);
299
+ border-radius: 50%;
300
+ position: absolute;
301
+ top: -5px;
302
+ left: -5px;
303
+ box-shadow: 0 0 12px rgba(59, 130, 246, 0.8), 0 0 4px rgba(255, 255, 255, 0.9);
304
+ `;
305
+
306
+ // Click indicator
307
+ const clickRing = document.createElement('div');
308
+ clickRing.id = '__human_cursor_click';
309
+ clickRing.style.cssText = `
310
+ width: 36px;
311
+ height: 36px;
312
+ border: 2px solid rgba(239, 68, 68, 0.8);
313
+ border-radius: 50%;
314
+ box-sizing: border-box;
315
+ position: absolute;
316
+ top: -18px;
317
+ left: -18px;
318
+ opacity: 0;
319
+ transform: scale(0.5);
320
+ transition: opacity 0.15s, transform 0.15s;
321
+ `;
322
+
323
+ // Add pulse animation
324
+ const style = document.createElement('style');
325
+ style.textContent = `
326
+ @keyframes pulse {
327
+ 0%, 100% { transform: scale(1); opacity: 0.6; }
328
+ 50% { transform: scale(1.2); opacity: 0.3; }
329
+ }
330
+ `;
331
+ document.head.appendChild(style);
332
+
333
+ cursor.appendChild(ring);
334
+ cursor.appendChild(dot);
335
+ cursor.appendChild(clickRing);
336
+ document.body.appendChild(cursor);
337
+
338
+ // Click effect
339
+ document.addEventListener('mousedown', () => {
340
+ const click = document.getElementById('__human_cursor_click');
341
+ if (click) {
342
+ click.style.opacity = '1';
343
+ click.style.transform = 'scale(1)';
344
+ }
345
+ });
346
+ document.addEventListener('mouseup', () => {
347
+ const click = document.getElementById('__human_cursor_click');
348
+ if (click) {
349
+ click.style.opacity = '0';
350
+ click.style.transform = 'scale(0.5)';
351
+ }
352
+ });
353
+ }
354
+
355
+ cursor.style.left = x + 'px';
356
+ cursor.style.top = y + 'px';
357
+ }
358
+
359
+ document.addEventListener('mousemove', updateCursor);
360
+ window.__humanCursorDot = true;
361
+ });
362
+ }
363
+ };
364
+
365
+ // Methods that return HumanLocators
366
+ const locatorMethods = {
367
+ locator(selector) {
368
+ return createHumanLocator(cursor, page.locator(selector));
369
+ },
370
+ getByText(text, opts) {
371
+ return createHumanLocator(cursor, page.getByText(text, opts));
372
+ },
373
+ getByRole(role, opts) {
374
+ return createHumanLocator(cursor, page.getByRole(role, opts));
375
+ },
376
+ getByLabel(text, opts) {
377
+ return createHumanLocator(cursor, page.getByLabel(text, opts));
378
+ },
379
+ getByPlaceholder(text, opts) {
380
+ return createHumanLocator(cursor, page.getByPlaceholder(text, opts));
381
+ },
382
+ getByAltText(text, opts) {
383
+ return createHumanLocator(cursor, page.getByAltText(text, opts));
384
+ },
385
+ getByTitle(text, opts) {
386
+ return createHumanLocator(cursor, page.getByTitle(text, opts));
387
+ },
388
+ getByTestId(testId) {
389
+ return createHumanLocator(cursor, page.getByTestId(testId));
390
+ }
391
+ };
392
+
393
+ // Wrap goto to auto-show cursor after navigation
394
+ const originalGoto = page.goto.bind(page);
395
+ const wrappedGoto = async (...args) => {
396
+ const result = await originalGoto(...args);
397
+ if (config.showCursor) {
398
+ await cursor.showCursor();
399
+ }
400
+ return result;
401
+ };
402
+
403
+ // Use Proxy to create drop-in page replacement
404
+ return new Proxy(page, {
405
+ get(target, prop) {
406
+ // Override goto to auto-show cursor
407
+ if (prop === 'goto') {
408
+ return wrappedGoto;
409
+ }
410
+ // Locator methods return HumanLocators
411
+ if (prop in locatorMethods) {
412
+ return locatorMethods[prop];
413
+ }
414
+ // Expose cursor utilities
415
+ if (prop === 'showCursor') {
416
+ return cursor.showCursor.bind(cursor);
417
+ }
418
+ if (prop === 'originCoordinates') {
419
+ return cursor.originCoordinates;
420
+ }
421
+ if (prop === 'setPosition') {
422
+ return (x, y) => { cursor.originCoordinates = [x, y]; };
423
+ }
424
+ // Pass through everything else to original page
425
+ const value = target[prop];
426
+ if (typeof value === 'function') {
427
+ return value.bind(target);
428
+ }
429
+ return value;
430
+ }
431
+ });
432
+ }
433
+
434
+ // Legacy class export for backwards compatibility
435
+ export class HumanCursor {
436
+ constructor(page, options = {}) {
437
+ this.page = page;
438
+ this.originCoordinates = [0, 0];
439
+ this.options = {
440
+ maxTime: options.maxTime ?? 1.5,
441
+ minTime: options.minTime ?? 0.5,
442
+ showCursor: options.showCursor ?? true // Show cursor by default
443
+ };
444
+
445
+ // Return proxy instead of class instance
446
+ return createCursor(page, options);
447
+ }
448
+ }