@axeptio/behavior-detection 1.0.3 → 1.1.1

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.
@@ -2,8 +2,27 @@
2
2
  * Mouse Movement Detection Strategy
3
3
  * Autonomous module that manages its own mouse event listeners and state
4
4
  * Tracks distance and angle to detect unnatural jumps and sharp turns
5
+ *
6
+ * Enhanced detection:
7
+ * - Pre-click stillness (micro-movements before clicks)
8
+ * - Entry point analysis (where mouse enters viewport)
9
+ * - Velocity consistency
5
10
  */
6
11
  import { BaseStrategy, type TimeSeriesPoint } from '../strategy.js';
12
+ interface MousePosition {
13
+ x: number;
14
+ y: number;
15
+ timestamp: number;
16
+ }
17
+ interface EntryPoint {
18
+ x: number;
19
+ y: number;
20
+ timestamp: number;
21
+ /** Distance from nearest edge (0 = at edge) */
22
+ edgeDistance: number;
23
+ /** Which edge: 'top' | 'bottom' | 'left' | 'right' | 'corner' | 'center' */
24
+ entryEdge: 'top' | 'bottom' | 'left' | 'right' | 'corner' | 'center';
25
+ }
7
26
  export declare class MouseStrategy extends BaseStrategy {
8
27
  readonly name = "mouse";
9
28
  readonly defaultWeight = 0.3;
@@ -15,8 +34,15 @@ export declare class MouseStrategy extends BaseStrategy {
15
34
  private rollingWindowMs;
16
35
  private listener;
17
36
  private leaveListener;
37
+ private enterListener;
18
38
  private isActive;
19
39
  private screenDiagonal;
40
+ /** Entry points tracking */
41
+ private entryPoints;
42
+ /** Micro-movements in last 500ms (for stillness detection) */
43
+ private microMovements;
44
+ /** Stillness window in ms */
45
+ private readonly STILLNESS_WINDOW;
20
46
  constructor(options?: {
21
47
  rollingWindow?: number;
22
48
  });
@@ -26,14 +52,40 @@ export declare class MouseStrategy extends BaseStrategy {
26
52
  score(): number | undefined;
27
53
  /**
28
54
  * Mouse-specific pattern detection
29
- * Detects bot-like patterns: constant velocity, linear paths
55
+ * Detects bot-like patterns: constant velocity, linear paths, suspicious entry points
30
56
  */
31
57
  private detectMousePatterns;
58
+ /**
59
+ * Score based on viewport entry points
60
+ * Humans enter from edges with momentum; bots often start at (0,0) or center
61
+ */
62
+ private scoreEntryPoints;
63
+ /**
64
+ * Score based on micro-movements (tremor detection)
65
+ * Humans have natural hand tremor causing 1-5px jitter
66
+ * Bots have perfect stillness or no micro-movements
67
+ */
68
+ private scoreMicroMovements;
69
+ /**
70
+ * Get micro-movement count for external use (e.g., by ClickStrategy)
71
+ */
72
+ getMicroMovementCount(): number;
73
+ /**
74
+ * Get last position for external use
75
+ */
76
+ getLastPosition(): {
77
+ x: number;
78
+ y: number;
79
+ } | null;
32
80
  getDebugInfo(): {
33
81
  eventCount: number;
34
82
  rollingWindow: number;
35
83
  isActive: boolean;
36
84
  distanceSeries: TimeSeriesPoint[];
37
85
  angleSeries: TimeSeriesPoint[];
86
+ entryPoints: EntryPoint[];
87
+ microMovementCount: number;
88
+ lastPosition: MousePosition | null;
38
89
  };
39
90
  }
91
+ export {};
@@ -2,6 +2,11 @@
2
2
  * Mouse Movement Detection Strategy
3
3
  * Autonomous module that manages its own mouse event listeners and state
4
4
  * Tracks distance and angle to detect unnatural jumps and sharp turns
5
+ *
6
+ * Enhanced detection:
7
+ * - Pre-click stillness (micro-movements before clicks)
8
+ * - Entry point analysis (where mouse enters viewport)
9
+ * - Velocity consistency
5
10
  */
6
11
  import { BaseStrategy } from '../strategy.js';
7
12
  import { calculateStatistics, gaussian } from '../math-utils.js';
@@ -18,8 +23,15 @@ export class MouseStrategy extends BaseStrategy {
18
23
  this.rollingWindowMs = 5000;
19
24
  this.listener = null;
20
25
  this.leaveListener = null;
26
+ this.enterListener = null;
21
27
  this.isActive = false;
22
28
  this.screenDiagonal = 1;
29
+ /** Entry points tracking */
30
+ this.entryPoints = [];
31
+ /** Micro-movements in last 500ms (for stillness detection) */
32
+ this.microMovements = [];
33
+ /** Stillness window in ms */
34
+ this.STILLNESS_WINDOW = 500;
23
35
  if ((options === null || options === void 0 ? void 0 : options.rollingWindow) !== undefined)
24
36
  this.rollingWindowMs = options.rollingWindow;
25
37
  }
@@ -34,12 +46,21 @@ export class MouseStrategy extends BaseStrategy {
34
46
  this.listener = (e) => {
35
47
  const mouseEvent = e;
36
48
  const now = Date.now();
37
- const currentPos = { x: mouseEvent.clientX, y: mouseEvent.clientY };
49
+ const currentPos = { x: mouseEvent.clientX, y: mouseEvent.clientY, timestamp: now };
38
50
  // Calculate distance and angle from previous position
39
51
  if (this.lastPosition) {
40
52
  const dx = currentPos.x - this.lastPosition.x;
41
53
  const dy = currentPos.y - this.lastPosition.y;
42
54
  const pixelDistance = Math.sqrt(dx * dx + dy * dy);
55
+ // Track micro-movements (1-5px) for stillness detection
56
+ if (pixelDistance >= 1 && pixelDistance <= 5) {
57
+ this.microMovements.push({ dx, dy, timestamp: now });
58
+ // Clean up old micro-movements (shift from front, more efficient than filter)
59
+ const cutoffMicro = now - this.STILLNESS_WINDOW;
60
+ while (this.microMovements.length > 0 && this.microMovements[0].timestamp < cutoffMicro) {
61
+ this.microMovements.shift();
62
+ }
63
+ }
43
64
  // Normalize distance to screen diagonal (0-1 range)
44
65
  // A full diagonal movement = 1.0, a 10px movement on 2000px screen = 0.005
45
66
  const normalizedDistance = pixelDistance / this.screenDiagonal;
@@ -72,6 +93,67 @@ export class MouseStrategy extends BaseStrategy {
72
93
  this.lastPosition = currentPos;
73
94
  };
74
95
  document.addEventListener('mousemove', this.listener, { passive: true });
96
+ // Track mouse entry points into viewport
97
+ this.enterListener = (e) => {
98
+ const mouseEvent = e;
99
+ const now = Date.now();
100
+ const x = mouseEvent.clientX;
101
+ const y = mouseEvent.clientY;
102
+ const width = window.innerWidth;
103
+ const height = window.innerHeight;
104
+ // Calculate edge distances
105
+ const distTop = y;
106
+ const distBottom = height - y;
107
+ const distLeft = x;
108
+ const distRight = width - x;
109
+ const minEdgeDist = Math.min(distTop, distBottom, distLeft, distRight);
110
+ // Determine entry edge
111
+ let entryEdge;
112
+ const edgeThreshold = 50; // pixels from edge to count as edge entry
113
+ if (minEdgeDist < edgeThreshold) {
114
+ if (distTop <= minEdgeDist)
115
+ entryEdge = 'top';
116
+ else if (distBottom <= minEdgeDist)
117
+ entryEdge = 'bottom';
118
+ else if (distLeft <= minEdgeDist)
119
+ entryEdge = 'left';
120
+ else
121
+ entryEdge = 'right';
122
+ }
123
+ else if (x > width * 0.35 && x < width * 0.65 &&
124
+ y > height * 0.35 && y < height * 0.65) {
125
+ entryEdge = 'center'; // Suspicious - bots often start at center
126
+ }
127
+ else if ((distTop < edgeThreshold * 2 && distLeft < edgeThreshold * 2) ||
128
+ (distTop < edgeThreshold * 2 && distRight < edgeThreshold * 2) ||
129
+ (distBottom < edgeThreshold * 2 && distLeft < edgeThreshold * 2) ||
130
+ (distBottom < edgeThreshold * 2 && distRight < edgeThreshold * 2)) {
131
+ entryEdge = 'corner';
132
+ }
133
+ else {
134
+ // Entry from middle of an edge, normal
135
+ if (distTop < distBottom && distTop < distLeft && distTop < distRight)
136
+ entryEdge = 'top';
137
+ else if (distBottom < distLeft && distBottom < distRight)
138
+ entryEdge = 'bottom';
139
+ else if (distLeft < distRight)
140
+ entryEdge = 'left';
141
+ else
142
+ entryEdge = 'right';
143
+ }
144
+ this.entryPoints.push({
145
+ x,
146
+ y,
147
+ timestamp: now,
148
+ edgeDistance: minEdgeDist,
149
+ entryEdge,
150
+ });
151
+ // Keep only last 20 entry points (shift is more memory efficient than slice)
152
+ if (this.entryPoints.length > 20) {
153
+ this.entryPoints.shift();
154
+ }
155
+ };
156
+ document.addEventListener('mouseenter', this.enterListener, { passive: true });
75
157
  // Clear data when mouse leaves the window (discontinuous tracking)
76
158
  this.leaveListener = () => {
77
159
  this.distanceSeries = [];
@@ -79,6 +161,7 @@ export class MouseStrategy extends BaseStrategy {
79
161
  this.lastPosition = null;
80
162
  this.lastAngle = 0;
81
163
  this.cumulativeAngle = 0;
164
+ this.microMovements = [];
82
165
  };
83
166
  document.addEventListener('mouseleave', this.leaveListener, { passive: true });
84
167
  }
@@ -94,6 +177,10 @@ export class MouseStrategy extends BaseStrategy {
94
177
  document.removeEventListener('mouseleave', this.leaveListener);
95
178
  this.leaveListener = null;
96
179
  }
180
+ if (this.enterListener) {
181
+ document.removeEventListener('mouseenter', this.enterListener);
182
+ this.enterListener = null;
183
+ }
97
184
  }
98
185
  reset() {
99
186
  this.distanceSeries = [];
@@ -101,6 +188,8 @@ export class MouseStrategy extends BaseStrategy {
101
188
  this.lastPosition = null;
102
189
  this.lastAngle = 0;
103
190
  this.cumulativeAngle = 0;
191
+ this.entryPoints = [];
192
+ this.microMovements = [];
104
193
  }
105
194
  score() {
106
195
  if (this.distanceSeries.length < 10)
@@ -111,7 +200,7 @@ export class MouseStrategy extends BaseStrategy {
111
200
  }
112
201
  /**
113
202
  * Mouse-specific pattern detection
114
- * Detects bot-like patterns: constant velocity, linear paths
203
+ * Detects bot-like patterns: constant velocity, linear paths, suspicious entry points
115
204
  */
116
205
  detectMousePatterns() {
117
206
  if (this.distanceSeries.length < 10)
@@ -141,8 +230,112 @@ export class MouseStrategy extends BaseStrategy {
141
230
  score += gaussian(avgChange, 0.15, 0.12);
142
231
  factors++;
143
232
  }
233
+ // 3. ENTRY POINT ANALYSIS - Where does mouse enter the viewport?
234
+ const entryScore = this.scoreEntryPoints();
235
+ if (entryScore !== undefined) {
236
+ score += entryScore;
237
+ factors++;
238
+ }
239
+ // 4. MICRO-MOVEMENT PRESENCE - Humans have tremors, bots don't
240
+ const microScore = this.scoreMicroMovements();
241
+ if (microScore !== undefined) {
242
+ score += microScore;
243
+ factors++;
244
+ }
144
245
  return factors > 0 ? score / factors : undefined;
145
246
  }
247
+ /**
248
+ * Score based on viewport entry points
249
+ * Humans enter from edges with momentum; bots often start at (0,0) or center
250
+ */
251
+ scoreEntryPoints() {
252
+ if (this.entryPoints.length < 2)
253
+ return undefined;
254
+ let suspiciousCount = 0;
255
+ let centerCount = 0;
256
+ let cornerCount = 0;
257
+ let originCount = 0;
258
+ for (const entry of this.entryPoints) {
259
+ // Check for (0,0) or near-origin entries (very suspicious)
260
+ if (entry.x < 5 && entry.y < 5) {
261
+ originCount++;
262
+ suspiciousCount++;
263
+ }
264
+ else if (entry.entryEdge === 'center') {
265
+ centerCount++;
266
+ suspiciousCount++;
267
+ }
268
+ else if (entry.entryEdge === 'corner') {
269
+ cornerCount++;
270
+ // Corners are slightly suspicious but not as bad as center
271
+ if (entry.edgeDistance < 10)
272
+ suspiciousCount++;
273
+ }
274
+ }
275
+ const total = this.entryPoints.length;
276
+ const suspiciousRatio = suspiciousCount / total;
277
+ // High ratio of origin entries = definitely bot
278
+ if (originCount >= 2 || originCount / total >= 0.5)
279
+ return 0.1;
280
+ // High ratio of center entries = likely bot
281
+ if (centerCount / total >= 0.5)
282
+ return 0.2;
283
+ // Mix of suspicious entries
284
+ if (suspiciousRatio >= 0.7)
285
+ return 0.3;
286
+ if (suspiciousRatio >= 0.5)
287
+ return 0.5;
288
+ if (suspiciousRatio >= 0.3)
289
+ return 0.7;
290
+ // Mostly edge entries = human
291
+ return 1.0;
292
+ }
293
+ /**
294
+ * Score based on micro-movements (tremor detection)
295
+ * Humans have natural hand tremor causing 1-5px jitter
296
+ * Bots have perfect stillness or no micro-movements
297
+ */
298
+ scoreMicroMovements() {
299
+ // Need some movement data first
300
+ if (this.distanceSeries.length < 20)
301
+ return undefined;
302
+ const microCount = this.microMovements.length;
303
+ // Calculate expected micro-movements based on tracking duration
304
+ // Humans should have ~5-20 micro-movements per second when cursor is active
305
+ const oldestEvent = this.distanceSeries[0];
306
+ const newestEvent = this.distanceSeries[this.distanceSeries.length - 1];
307
+ const durationMs = newestEvent.timestamp - oldestEvent.timestamp;
308
+ if (durationMs < 1000)
309
+ return undefined; // Need at least 1 second of data
310
+ // Micro-movements per second
311
+ const microPerSecond = (microCount / durationMs) * 1000;
312
+ // No micro-movements at all = very suspicious
313
+ if (microCount === 0)
314
+ return 0.3;
315
+ // Very few micro-movements = somewhat suspicious
316
+ if (microPerSecond < 1)
317
+ return 0.5;
318
+ // Some micro-movements = more human-like
319
+ if (microPerSecond < 5)
320
+ return 0.7;
321
+ // Good amount of micro-movements = human
322
+ if (microPerSecond >= 5 && microPerSecond <= 30)
323
+ return 1.0;
324
+ // Excessive micro-movements (> 30/sec) might be artificial noise
325
+ return 0.8;
326
+ }
327
+ /**
328
+ * Get micro-movement count for external use (e.g., by ClickStrategy)
329
+ */
330
+ getMicroMovementCount() {
331
+ return this.microMovements.length;
332
+ }
333
+ /**
334
+ * Get last position for external use
335
+ */
336
+ getLastPosition() {
337
+ return this.lastPosition ? { x: this.lastPosition.x, y: this.lastPosition.y } : null;
338
+ }
146
339
  getDebugInfo() {
147
340
  return {
148
341
  eventCount: this.distanceSeries.length,
@@ -150,6 +343,9 @@ export class MouseStrategy extends BaseStrategy {
150
343
  isActive: this.isActive,
151
344
  distanceSeries: this.distanceSeries,
152
345
  angleSeries: this.angleSeries,
346
+ entryPoints: this.entryPoints,
347
+ microMovementCount: this.microMovements.length,
348
+ lastPosition: this.lastPosition,
153
349
  };
154
350
  }
155
351
  }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Timing Analysis Strategy
3
+ * Detects bot-like timing patterns in user interactions
4
+ *
5
+ * Key detection signals:
6
+ * - Pre-action stillness (humans have micro-movements before clicking)
7
+ * - Action timing patterns (machine-precise intervals)
8
+ * - Inactivity patterns (bursts after long silence, no idle variance)
9
+ * - Actions while tab is hidden (bots ignore visibility)
10
+ */
11
+ import { BaseStrategy } from '../strategy.js';
12
+ export declare class TimingStrategy extends BaseStrategy {
13
+ readonly name = "timing";
14
+ readonly defaultWeight = 0.15;
15
+ private actions;
16
+ private mouseStillness;
17
+ private microMovements;
18
+ private isActive;
19
+ private mouseListener;
20
+ private clickListener;
21
+ private keydownListener;
22
+ private scrollListener;
23
+ /** Time window to consider for pre-action stillness (ms) */
24
+ private readonly STILLNESS_WINDOW;
25
+ /** Minimum distance to count as meaningful movement (px) */
26
+ private readonly MICRO_MOVEMENT_THRESHOLD;
27
+ /** Maximum micro-movement to count (jitter vs intentional) (px) */
28
+ private readonly MAX_MICRO_MOVEMENT;
29
+ /** Machine-precision threshold (intervals within this are suspicious) */
30
+ private readonly MACHINE_PRECISION_THRESHOLD;
31
+ start(): void;
32
+ private recordAction;
33
+ stop(): void;
34
+ reset(): void;
35
+ score(): number | undefined;
36
+ /**
37
+ * Score based on pre-action stillness
38
+ * Humans have micro-movements before clicking; bots are perfectly still
39
+ */
40
+ private scorePreActionStillness;
41
+ /**
42
+ * Score mouse-to-click delay patterns (using pre-filtered cache)
43
+ */
44
+ private scoreMouseToClickDelayFromCache;
45
+ /**
46
+ * Score action intervals for machine-like precision
47
+ * Detects exact intervals: 100ms, 500ms, 1000ms
48
+ */
49
+ private scoreActionIntervals;
50
+ /**
51
+ * Score based on actions while document was hidden (using pre-computed count)
52
+ */
53
+ private scoreHiddenActionsFromCount;
54
+ getDebugInfo(): {
55
+ actionCount: number;
56
+ clickCount: number;
57
+ keydownCount: number;
58
+ scrollCount: number;
59
+ hiddenActionCount: number;
60
+ microMovementCount: number;
61
+ intervals: number[];
62
+ lastStillnessDuration: number | null;
63
+ };
64
+ }