@axeptio/behavior-detection 1.0.0
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/README.md +828 -0
- package/dist/behavior-detection.esm.min.js +2 -0
- package/dist/behavior-detection.esm.min.js.map +7 -0
- package/dist/behavior-detection.min.js +2 -0
- package/dist/behavior-detection.min.js.map +7 -0
- package/dist/behavior-detector.d.ts +102 -0
- package/dist/browser.d.ts +33 -0
- package/dist/cjs/behavior-detector.d.ts +102 -0
- package/dist/cjs/behavior-detector.js +315 -0
- package/dist/cjs/browser.d.ts +33 -0
- package/dist/cjs/browser.js +226 -0
- package/dist/cjs/index.d.ts +38 -0
- package/dist/cjs/index.js +55 -0
- package/dist/cjs/math-utils.d.ts +84 -0
- package/dist/cjs/math-utils.js +141 -0
- package/dist/cjs/strategies/click.d.ts +39 -0
- package/dist/cjs/strategies/click.js +173 -0
- package/dist/cjs/strategies/environment.d.ts +52 -0
- package/dist/cjs/strategies/environment.js +148 -0
- package/dist/cjs/strategies/index.d.ts +18 -0
- package/dist/cjs/strategies/index.js +36 -0
- package/dist/cjs/strategies/keyboard.d.ts +43 -0
- package/dist/cjs/strategies/keyboard.js +233 -0
- package/dist/cjs/strategies/mouse.d.ts +39 -0
- package/dist/cjs/strategies/mouse.js +159 -0
- package/dist/cjs/strategies/resize.d.ts +21 -0
- package/dist/cjs/strategies/resize.js +97 -0
- package/dist/cjs/strategies/scroll.d.ts +37 -0
- package/dist/cjs/strategies/scroll.js +149 -0
- package/dist/cjs/strategies/tap.d.ts +38 -0
- package/dist/cjs/strategies/tap.js +214 -0
- package/dist/cjs/strategy.d.ts +107 -0
- package/dist/cjs/strategy.js +33 -0
- package/dist/cjs/types.d.ts +168 -0
- package/dist/cjs/types.js +26 -0
- package/dist/esm/behavior-detector.d.ts +102 -0
- package/dist/esm/behavior-detector.js +311 -0
- package/dist/esm/browser.d.ts +33 -0
- package/dist/esm/browser.js +224 -0
- package/dist/esm/index.d.ts +38 -0
- package/dist/esm/index.js +36 -0
- package/dist/esm/math-utils.d.ts +84 -0
- package/dist/esm/math-utils.js +127 -0
- package/dist/esm/strategies/click.d.ts +39 -0
- package/dist/esm/strategies/click.js +169 -0
- package/dist/esm/strategies/environment.d.ts +52 -0
- package/dist/esm/strategies/environment.js +144 -0
- package/dist/esm/strategies/index.d.ts +18 -0
- package/dist/esm/strategies/index.js +19 -0
- package/dist/esm/strategies/keyboard.d.ts +43 -0
- package/dist/esm/strategies/keyboard.js +229 -0
- package/dist/esm/strategies/mouse.d.ts +39 -0
- package/dist/esm/strategies/mouse.js +155 -0
- package/dist/esm/strategies/resize.d.ts +21 -0
- package/dist/esm/strategies/resize.js +93 -0
- package/dist/esm/strategies/scroll.d.ts +37 -0
- package/dist/esm/strategies/scroll.js +145 -0
- package/dist/esm/strategies/tap.d.ts +38 -0
- package/dist/esm/strategies/tap.js +210 -0
- package/dist/esm/strategy.d.ts +107 -0
- package/dist/esm/strategy.js +29 -0
- package/dist/esm/types.d.ts +168 -0
- package/dist/esm/types.js +23 -0
- package/dist/index.d.ts +38 -0
- package/dist/math-utils.d.ts +84 -0
- package/dist/strategies/click.d.ts +39 -0
- package/dist/strategies/environment.d.ts +52 -0
- package/dist/strategies/index.d.ts +18 -0
- package/dist/strategies/keyboard.d.ts +43 -0
- package/dist/strategies/mouse.d.ts +39 -0
- package/dist/strategies/resize.d.ts +21 -0
- package/dist/strategies/scroll.d.ts +37 -0
- package/dist/strategies/tap.d.ts +38 -0
- package/dist/strategy.d.ts +107 -0
- package/dist/types.d.ts +168 -0
- package/package.json +60 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mouse Movement Detection Strategy
|
|
3
|
+
* Autonomous module that manages its own mouse event listeners and state
|
|
4
|
+
* Tracks distance and angle to detect unnatural jumps and sharp turns
|
|
5
|
+
*/
|
|
6
|
+
import { BaseStrategy, type TimeSeriesPoint } from '../strategy';
|
|
7
|
+
export declare class MouseStrategy extends BaseStrategy {
|
|
8
|
+
readonly name = "mouse";
|
|
9
|
+
readonly defaultWeight = 0.3;
|
|
10
|
+
private distanceSeries;
|
|
11
|
+
private angleSeries;
|
|
12
|
+
private lastPosition;
|
|
13
|
+
private lastAngle;
|
|
14
|
+
private cumulativeAngle;
|
|
15
|
+
private rollingWindowMs;
|
|
16
|
+
private listener;
|
|
17
|
+
private leaveListener;
|
|
18
|
+
private isActive;
|
|
19
|
+
private screenDiagonal;
|
|
20
|
+
constructor(options?: {
|
|
21
|
+
rollingWindow?: number;
|
|
22
|
+
});
|
|
23
|
+
start(): void;
|
|
24
|
+
stop(): void;
|
|
25
|
+
reset(): void;
|
|
26
|
+
score(): number | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Mouse-specific pattern detection
|
|
29
|
+
* Detects bot-like patterns: constant velocity, linear paths
|
|
30
|
+
*/
|
|
31
|
+
private detectMousePatterns;
|
|
32
|
+
getDebugInfo(): {
|
|
33
|
+
eventCount: number;
|
|
34
|
+
rollingWindow: number;
|
|
35
|
+
isActive: boolean;
|
|
36
|
+
distanceSeries: TimeSeriesPoint[];
|
|
37
|
+
angleSeries: TimeSeriesPoint[];
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mouse Movement Detection Strategy
|
|
3
|
+
* Autonomous module that manages its own mouse event listeners and state
|
|
4
|
+
* Tracks distance and angle to detect unnatural jumps and sharp turns
|
|
5
|
+
*/
|
|
6
|
+
import { BaseStrategy } from '../strategy';
|
|
7
|
+
import { calculateStatistics, gaussian } from '../math-utils';
|
|
8
|
+
export class MouseStrategy extends BaseStrategy {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
super();
|
|
11
|
+
this.name = 'mouse';
|
|
12
|
+
this.defaultWeight = 0.30;
|
|
13
|
+
this.distanceSeries = [];
|
|
14
|
+
this.angleSeries = [];
|
|
15
|
+
this.lastPosition = null;
|
|
16
|
+
this.lastAngle = 0;
|
|
17
|
+
this.cumulativeAngle = 0;
|
|
18
|
+
this.rollingWindowMs = 5000;
|
|
19
|
+
this.listener = null;
|
|
20
|
+
this.leaveListener = null;
|
|
21
|
+
this.isActive = false;
|
|
22
|
+
this.screenDiagonal = 1;
|
|
23
|
+
if ((options === null || options === void 0 ? void 0 : options.rollingWindow) !== undefined)
|
|
24
|
+
this.rollingWindowMs = options.rollingWindow;
|
|
25
|
+
}
|
|
26
|
+
start() {
|
|
27
|
+
if (this.isActive)
|
|
28
|
+
return;
|
|
29
|
+
this.isActive = true;
|
|
30
|
+
// Calculate screen diagonal for normalization (distance relative to screen size)
|
|
31
|
+
const width = window.innerWidth;
|
|
32
|
+
const height = window.innerHeight;
|
|
33
|
+
this.screenDiagonal = Math.sqrt(width * width + height * height);
|
|
34
|
+
this.listener = (e) => {
|
|
35
|
+
const mouseEvent = e;
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
const currentPos = { x: mouseEvent.clientX, y: mouseEvent.clientY };
|
|
38
|
+
// Calculate distance and angle from previous position
|
|
39
|
+
if (this.lastPosition) {
|
|
40
|
+
const dx = currentPos.x - this.lastPosition.x;
|
|
41
|
+
const dy = currentPos.y - this.lastPosition.y;
|
|
42
|
+
const pixelDistance = Math.sqrt(dx * dx + dy * dy);
|
|
43
|
+
// Normalize distance to screen diagonal (0-1 range)
|
|
44
|
+
// A full diagonal movement = 1.0, a 10px movement on 2000px screen = 0.005
|
|
45
|
+
const normalizedDistance = pixelDistance / this.screenDiagonal;
|
|
46
|
+
// Only record if movement is meaningful (> 0.001 = ~2px on 2000px screen)
|
|
47
|
+
if (normalizedDistance > 0.001) {
|
|
48
|
+
const rawAngle = Math.atan2(dy, dx); // -PI to PI
|
|
49
|
+
// Unwrap angle to avoid discontinuities (e.g., circles)
|
|
50
|
+
let angleDiff = rawAngle - this.lastAngle;
|
|
51
|
+
// Normalize difference to -PI to PI
|
|
52
|
+
while (angleDiff > Math.PI)
|
|
53
|
+
angleDiff -= 2 * Math.PI;
|
|
54
|
+
while (angleDiff < -Math.PI)
|
|
55
|
+
angleDiff += 2 * Math.PI;
|
|
56
|
+
this.cumulativeAngle += angleDiff;
|
|
57
|
+
this.lastAngle = rawAngle;
|
|
58
|
+
// Store normalized distance (not raw pixels)
|
|
59
|
+
this.distanceSeries.push({ value: normalizedDistance, timestamp: now });
|
|
60
|
+
this.angleSeries.push({ value: this.cumulativeAngle, timestamp: now });
|
|
61
|
+
// Notify detector for confidence tracking
|
|
62
|
+
this.notifyEvent(Math.min(1, normalizedDistance * 100));
|
|
63
|
+
}
|
|
64
|
+
// Apply rolling window efficiently: remove old events from start
|
|
65
|
+
// Since events are chronological, we only check from the beginning
|
|
66
|
+
const cutoff = now - this.rollingWindowMs;
|
|
67
|
+
while (this.distanceSeries.length > 0 && this.distanceSeries[0].timestamp < cutoff) {
|
|
68
|
+
this.distanceSeries.shift();
|
|
69
|
+
this.angleSeries.shift(); // Keep both arrays in sync
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
this.lastPosition = currentPos;
|
|
73
|
+
};
|
|
74
|
+
document.addEventListener('mousemove', this.listener, { passive: true });
|
|
75
|
+
// Clear data when mouse leaves the window (discontinuous tracking)
|
|
76
|
+
this.leaveListener = () => {
|
|
77
|
+
this.distanceSeries = [];
|
|
78
|
+
this.angleSeries = [];
|
|
79
|
+
this.lastPosition = null;
|
|
80
|
+
this.lastAngle = 0;
|
|
81
|
+
this.cumulativeAngle = 0;
|
|
82
|
+
};
|
|
83
|
+
document.addEventListener('mouseleave', this.leaveListener, { passive: true });
|
|
84
|
+
}
|
|
85
|
+
stop() {
|
|
86
|
+
if (!this.isActive)
|
|
87
|
+
return;
|
|
88
|
+
this.isActive = false;
|
|
89
|
+
if (this.listener) {
|
|
90
|
+
document.removeEventListener('mousemove', this.listener);
|
|
91
|
+
this.listener = null;
|
|
92
|
+
}
|
|
93
|
+
if (this.leaveListener) {
|
|
94
|
+
document.removeEventListener('mouseleave', this.leaveListener);
|
|
95
|
+
this.leaveListener = null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
reset() {
|
|
99
|
+
this.distanceSeries = [];
|
|
100
|
+
this.angleSeries = [];
|
|
101
|
+
this.lastPosition = null;
|
|
102
|
+
this.lastAngle = 0;
|
|
103
|
+
this.cumulativeAngle = 0;
|
|
104
|
+
}
|
|
105
|
+
score() {
|
|
106
|
+
if (this.distanceSeries.length < 10)
|
|
107
|
+
return undefined;
|
|
108
|
+
// Mouse-specific pattern detection (optimized for normalized continuous data)
|
|
109
|
+
// Generic smoothness detector is calibrated for discrete events, not continuous movement
|
|
110
|
+
return this.detectMousePatterns();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Mouse-specific pattern detection
|
|
114
|
+
* Detects bot-like patterns: constant velocity, linear paths
|
|
115
|
+
*/
|
|
116
|
+
detectMousePatterns() {
|
|
117
|
+
if (this.distanceSeries.length < 10)
|
|
118
|
+
return undefined;
|
|
119
|
+
let score = 0;
|
|
120
|
+
let factors = 0;
|
|
121
|
+
// 1. VELOCITY CONSISTENCY - Bots often move at constant speed
|
|
122
|
+
const distances = this.distanceSeries.map(p => p.value);
|
|
123
|
+
if (distances.length >= 3) {
|
|
124
|
+
const stats = calculateStatistics(distances);
|
|
125
|
+
// Real human movement has high variation (CV ~0.8-1.2)
|
|
126
|
+
// page.mouse.move() with steps has lower CV (~0.4-0.6)
|
|
127
|
+
// Narrower gaussian to be strict
|
|
128
|
+
score += gaussian(stats.cv, 0.9, 0.35);
|
|
129
|
+
factors++;
|
|
130
|
+
}
|
|
131
|
+
// 2. DIRECTION CHANGES - Bots often have too few or too many sharp turns
|
|
132
|
+
const angles = this.angleSeries.map(p => p.value);
|
|
133
|
+
if (angles.length >= 3) {
|
|
134
|
+
const angleChanges = [];
|
|
135
|
+
for (let i = 1; i < angles.length; i++) {
|
|
136
|
+
angleChanges.push(Math.abs(angles[i] - angles[i - 1]));
|
|
137
|
+
}
|
|
138
|
+
const avgChange = angleChanges.reduce((a, b) => a + b, 0) / angleChanges.length;
|
|
139
|
+
// Real humans have moderate but varied direction changes
|
|
140
|
+
// page.mouse.move() linear interpolation has very small, consistent changes
|
|
141
|
+
score += gaussian(avgChange, 0.15, 0.12);
|
|
142
|
+
factors++;
|
|
143
|
+
}
|
|
144
|
+
return factors > 0 ? score / factors : undefined;
|
|
145
|
+
}
|
|
146
|
+
getDebugInfo() {
|
|
147
|
+
return {
|
|
148
|
+
eventCount: this.distanceSeries.length,
|
|
149
|
+
rollingWindow: this.rollingWindowMs,
|
|
150
|
+
isActive: this.isActive,
|
|
151
|
+
distanceSeries: this.distanceSeries,
|
|
152
|
+
angleSeries: this.angleSeries,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resize Behavior Detection Strategy
|
|
3
|
+
*/
|
|
4
|
+
import { BaseStrategy } from '../strategy';
|
|
5
|
+
export declare class ResizeStrategy extends BaseStrategy {
|
|
6
|
+
readonly name = "resize";
|
|
7
|
+
readonly defaultWeight = 0.02;
|
|
8
|
+
private events;
|
|
9
|
+
private listener;
|
|
10
|
+
private isActive;
|
|
11
|
+
private lastMousePosition;
|
|
12
|
+
private mouseListener;
|
|
13
|
+
start(): void;
|
|
14
|
+
stop(): void;
|
|
15
|
+
reset(): void;
|
|
16
|
+
score(): number | undefined;
|
|
17
|
+
getDebugInfo(): {
|
|
18
|
+
eventCount: number;
|
|
19
|
+
withMouseData: number;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resize Behavior Detection Strategy
|
|
3
|
+
*/
|
|
4
|
+
import { BaseStrategy } from '../strategy';
|
|
5
|
+
import { inverseSigmoid, sigmoid } from '../math-utils';
|
|
6
|
+
export class ResizeStrategy extends BaseStrategy {
|
|
7
|
+
constructor() {
|
|
8
|
+
super(...arguments);
|
|
9
|
+
this.name = 'resize';
|
|
10
|
+
this.defaultWeight = 0.02;
|
|
11
|
+
this.events = [];
|
|
12
|
+
this.listener = null;
|
|
13
|
+
this.isActive = false;
|
|
14
|
+
this.lastMousePosition = null;
|
|
15
|
+
this.mouseListener = null;
|
|
16
|
+
}
|
|
17
|
+
start() {
|
|
18
|
+
if (this.isActive)
|
|
19
|
+
return;
|
|
20
|
+
this.isActive = true;
|
|
21
|
+
// Track mouse for resize detection
|
|
22
|
+
this.mouseListener = (e) => {
|
|
23
|
+
const mouseEvent = e;
|
|
24
|
+
this.lastMousePosition = {
|
|
25
|
+
x: mouseEvent.clientX,
|
|
26
|
+
y: mouseEvent.clientY,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
document.addEventListener('mousemove', this.mouseListener, { passive: true });
|
|
30
|
+
// Track resizes
|
|
31
|
+
this.listener = () => {
|
|
32
|
+
var _a, _b;
|
|
33
|
+
const mouseX = (_a = this.lastMousePosition) === null || _a === void 0 ? void 0 : _a.x;
|
|
34
|
+
const mouseY = (_b = this.lastMousePosition) === null || _b === void 0 ? void 0 : _b.y;
|
|
35
|
+
let mouseNearEdge = false;
|
|
36
|
+
if (mouseX !== undefined && mouseY !== undefined) {
|
|
37
|
+
const edgeThreshold = 50;
|
|
38
|
+
mouseNearEdge = (mouseX < edgeThreshold ||
|
|
39
|
+
mouseX > window.innerWidth - edgeThreshold ||
|
|
40
|
+
mouseY < edgeThreshold ||
|
|
41
|
+
mouseY > window.innerHeight - edgeThreshold);
|
|
42
|
+
}
|
|
43
|
+
this.events.push({
|
|
44
|
+
width: window.innerWidth,
|
|
45
|
+
height: window.innerHeight,
|
|
46
|
+
mouseX,
|
|
47
|
+
mouseY,
|
|
48
|
+
mouseNearEdge,
|
|
49
|
+
timestamp: Date.now(),
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
window.addEventListener('resize', this.listener);
|
|
53
|
+
}
|
|
54
|
+
stop() {
|
|
55
|
+
if (!this.isActive)
|
|
56
|
+
return;
|
|
57
|
+
this.isActive = false;
|
|
58
|
+
if (this.listener) {
|
|
59
|
+
window.removeEventListener('resize', this.listener);
|
|
60
|
+
this.listener = null;
|
|
61
|
+
}
|
|
62
|
+
if (this.mouseListener) {
|
|
63
|
+
document.removeEventListener('mousemove', this.mouseListener);
|
|
64
|
+
this.mouseListener = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
reset() {
|
|
68
|
+
this.events = [];
|
|
69
|
+
}
|
|
70
|
+
score() {
|
|
71
|
+
if (this.events.length === 0)
|
|
72
|
+
return undefined;
|
|
73
|
+
let score = 0;
|
|
74
|
+
let factors = 0;
|
|
75
|
+
// Frequency
|
|
76
|
+
score += inverseSigmoid(this.events.length, 5, 0.5);
|
|
77
|
+
factors++;
|
|
78
|
+
// Mouse near edge
|
|
79
|
+
const withMouse = this.events.filter(e => e.mouseX !== undefined);
|
|
80
|
+
if (withMouse.length > 0) {
|
|
81
|
+
const nearEdge = withMouse.filter(e => e.mouseNearEdge).length;
|
|
82
|
+
score += sigmoid(nearEdge / withMouse.length, 0.5, 8);
|
|
83
|
+
factors++;
|
|
84
|
+
}
|
|
85
|
+
return factors > 0 ? score / factors : undefined;
|
|
86
|
+
}
|
|
87
|
+
getDebugInfo() {
|
|
88
|
+
return {
|
|
89
|
+
eventCount: this.events.length,
|
|
90
|
+
withMouseData: this.events.filter(e => e.mouseX !== undefined).length,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll Behavior Detection Strategy
|
|
3
|
+
* Autonomous module managing scroll event listeners
|
|
4
|
+
* Tracks normalized scroll distance and velocity patterns
|
|
5
|
+
*/
|
|
6
|
+
import { BaseStrategy, type TimeSeriesPoint } from '../strategy';
|
|
7
|
+
export declare class ScrollStrategy extends BaseStrategy {
|
|
8
|
+
readonly name = "scroll";
|
|
9
|
+
readonly defaultWeight = 0.15;
|
|
10
|
+
private distanceSeries;
|
|
11
|
+
private velocitySeries;
|
|
12
|
+
private lastScrollY;
|
|
13
|
+
private lastTimestamp;
|
|
14
|
+
private rollingWindowMs;
|
|
15
|
+
private documentHeight;
|
|
16
|
+
private listener;
|
|
17
|
+
private isActive;
|
|
18
|
+
constructor(options?: {
|
|
19
|
+
rollingWindow?: number;
|
|
20
|
+
});
|
|
21
|
+
start(): void;
|
|
22
|
+
stop(): void;
|
|
23
|
+
reset(): void;
|
|
24
|
+
score(): number | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Scroll-specific pattern detection
|
|
27
|
+
* Detects bot-like patterns: constant deltas, instant jumps, too smooth
|
|
28
|
+
*/
|
|
29
|
+
private detectScrollPatterns;
|
|
30
|
+
getDebugInfo(): {
|
|
31
|
+
eventCount: number;
|
|
32
|
+
rollingWindow: number;
|
|
33
|
+
isActive: boolean;
|
|
34
|
+
distanceSeries: TimeSeriesPoint[];
|
|
35
|
+
velocitySeries: TimeSeriesPoint[];
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll Behavior Detection Strategy
|
|
3
|
+
* Autonomous module managing scroll event listeners
|
|
4
|
+
* Tracks normalized scroll distance and velocity patterns
|
|
5
|
+
*/
|
|
6
|
+
import { BaseStrategy } from '../strategy';
|
|
7
|
+
import { calculateStatistics, gaussian, inverseSigmoid } from '../math-utils';
|
|
8
|
+
export class ScrollStrategy extends BaseStrategy {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
super();
|
|
11
|
+
this.name = 'scroll';
|
|
12
|
+
this.defaultWeight = 0.15;
|
|
13
|
+
this.distanceSeries = [];
|
|
14
|
+
this.velocitySeries = [];
|
|
15
|
+
this.lastScrollY = null;
|
|
16
|
+
this.lastTimestamp = 0;
|
|
17
|
+
this.rollingWindowMs = 5000;
|
|
18
|
+
this.documentHeight = 1;
|
|
19
|
+
this.listener = null;
|
|
20
|
+
this.isActive = false;
|
|
21
|
+
if ((options === null || options === void 0 ? void 0 : options.rollingWindow) !== undefined)
|
|
22
|
+
this.rollingWindowMs = options.rollingWindow;
|
|
23
|
+
}
|
|
24
|
+
start() {
|
|
25
|
+
if (this.isActive)
|
|
26
|
+
return;
|
|
27
|
+
this.isActive = true;
|
|
28
|
+
// Calculate document height for normalization
|
|
29
|
+
this.documentHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, 1);
|
|
30
|
+
this.listener = (_e) => {
|
|
31
|
+
const scrollY = window.scrollY;
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
// Always update last position on first event
|
|
34
|
+
if (this.lastScrollY === null) {
|
|
35
|
+
this.lastScrollY = scrollY;
|
|
36
|
+
this.lastTimestamp = now;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const pixelDelta = scrollY - this.lastScrollY;
|
|
40
|
+
const deltaTime = now - this.lastTimestamp;
|
|
41
|
+
// Skip if no actual scroll happened
|
|
42
|
+
if (pixelDelta === 0) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Normalize scroll distance to document height (0-1 range)
|
|
46
|
+
const normalizedDistance = Math.abs(pixelDelta) / this.documentHeight;
|
|
47
|
+
// Record all scroll movements (no threshold - let pattern detection decide)
|
|
48
|
+
if (deltaTime > 0) {
|
|
49
|
+
const velocity = normalizedDistance / deltaTime; // normalized units per ms
|
|
50
|
+
// Store as time series
|
|
51
|
+
this.distanceSeries.push({ value: normalizedDistance, timestamp: now });
|
|
52
|
+
this.velocitySeries.push({ value: velocity, timestamp: now });
|
|
53
|
+
// Notify detector for confidence tracking
|
|
54
|
+
this.notifyEvent(Math.min(1, normalizedDistance * 10));
|
|
55
|
+
// Apply rolling window efficiently
|
|
56
|
+
const cutoff = now - this.rollingWindowMs;
|
|
57
|
+
while (this.distanceSeries.length > 0 && this.distanceSeries[0].timestamp < cutoff) {
|
|
58
|
+
this.distanceSeries.shift();
|
|
59
|
+
this.velocitySeries.shift();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
this.lastScrollY = scrollY;
|
|
63
|
+
this.lastTimestamp = now;
|
|
64
|
+
};
|
|
65
|
+
// Listen on window for scroll events (document scrolling)
|
|
66
|
+
window.addEventListener('scroll', this.listener, { passive: true });
|
|
67
|
+
}
|
|
68
|
+
stop() {
|
|
69
|
+
if (!this.isActive)
|
|
70
|
+
return;
|
|
71
|
+
this.isActive = false;
|
|
72
|
+
if (this.listener) {
|
|
73
|
+
window.removeEventListener('scroll', this.listener);
|
|
74
|
+
this.listener = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
reset() {
|
|
78
|
+
this.distanceSeries = [];
|
|
79
|
+
this.velocitySeries = [];
|
|
80
|
+
this.lastScrollY = null;
|
|
81
|
+
this.lastTimestamp = 0;
|
|
82
|
+
}
|
|
83
|
+
score() {
|
|
84
|
+
if (this.distanceSeries.length < 2)
|
|
85
|
+
return undefined;
|
|
86
|
+
// Scroll-specific pattern detection
|
|
87
|
+
return this.detectScrollPatterns();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Scroll-specific pattern detection
|
|
91
|
+
* Detects bot-like patterns: constant deltas, instant jumps, too smooth
|
|
92
|
+
*/
|
|
93
|
+
detectScrollPatterns() {
|
|
94
|
+
if (this.distanceSeries.length < 2)
|
|
95
|
+
return undefined;
|
|
96
|
+
let score = 0;
|
|
97
|
+
let factors = 0;
|
|
98
|
+
// 1. IDENTICAL DELTAS - Smoking gun for bots
|
|
99
|
+
const distances = this.distanceSeries.map(p => p.value);
|
|
100
|
+
if (distances.length >= 2) {
|
|
101
|
+
// Check if all distances are identical (tolerance for floating point)
|
|
102
|
+
const uniqueDistances = new Set(distances.map(d => Math.round(d * 1000))).size;
|
|
103
|
+
if (uniqueDistances === 1) {
|
|
104
|
+
// All scroll amounts identical = definite bot
|
|
105
|
+
return 0.05;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// 2. DISTANCE CONSISTENCY - Bots often scroll same amount repeatedly
|
|
109
|
+
if (distances.length >= 2) {
|
|
110
|
+
const stats = calculateStatistics(distances);
|
|
111
|
+
// Real humans have high CV (>0.8) due to natural scroll acceleration/deceleration
|
|
112
|
+
// Simple automation has lower CV even with varied amounts
|
|
113
|
+
// Gaussian centered high with narrow width - be strict
|
|
114
|
+
score += gaussian(stats.cv, 1.0, 0.4);
|
|
115
|
+
factors++;
|
|
116
|
+
}
|
|
117
|
+
// 3. VELOCITY VARIATION - Humans have highly variable scroll speeds
|
|
118
|
+
const velocities = this.velocitySeries.map(p => p.value);
|
|
119
|
+
if (velocities.length >= 2) {
|
|
120
|
+
const stats = calculateStatistics(velocities);
|
|
121
|
+
// Human velocity is very chaotic (acceleration/deceleration)
|
|
122
|
+
// Automation has more consistent velocity even with delays
|
|
123
|
+
score += gaussian(stats.cv, 1.2, 0.5);
|
|
124
|
+
factors++;
|
|
125
|
+
}
|
|
126
|
+
// 4. INSTANT JUMPS - Detect programmatic scrollTo
|
|
127
|
+
const instantJumps = distances.filter(d => d > 0.1).length; // >10% of document in one event
|
|
128
|
+
if (distances.length > 0) {
|
|
129
|
+
const jumpRatio = instantJumps / distances.length;
|
|
130
|
+
// Penalize high jump ratio (programmatic scrollTo)
|
|
131
|
+
score += inverseSigmoid(jumpRatio, 0.3, 15);
|
|
132
|
+
factors++;
|
|
133
|
+
}
|
|
134
|
+
return factors > 0 ? score / factors : undefined;
|
|
135
|
+
}
|
|
136
|
+
getDebugInfo() {
|
|
137
|
+
return {
|
|
138
|
+
eventCount: this.distanceSeries.length,
|
|
139
|
+
rollingWindow: this.rollingWindowMs,
|
|
140
|
+
isActive: this.isActive,
|
|
141
|
+
distanceSeries: this.distanceSeries,
|
|
142
|
+
velocitySeries: this.velocitySeries,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tap Behavior Detection Strategy (Mobile)
|
|
3
|
+
* Monitors touch interactions on specific elements
|
|
4
|
+
* Analyzes tap duration, precision, movement, and timing patterns
|
|
5
|
+
*/
|
|
6
|
+
import { BaseStrategy } from '../strategy';
|
|
7
|
+
export declare class TapStrategy extends BaseStrategy {
|
|
8
|
+
readonly name = "tap";
|
|
9
|
+
readonly defaultWeight = 0.35;
|
|
10
|
+
private events;
|
|
11
|
+
private targetSelectors;
|
|
12
|
+
private touchStartListeners;
|
|
13
|
+
private touchEndListeners;
|
|
14
|
+
private activeTouches;
|
|
15
|
+
private isActive;
|
|
16
|
+
constructor(options?: {
|
|
17
|
+
targetSelectors?: string[];
|
|
18
|
+
});
|
|
19
|
+
start(): void;
|
|
20
|
+
addTarget(selector: string): void;
|
|
21
|
+
private attachTouchListeners;
|
|
22
|
+
private attachTouchListenersForSelector;
|
|
23
|
+
stop(): void;
|
|
24
|
+
reset(): void;
|
|
25
|
+
score(): number | undefined;
|
|
26
|
+
getDebugInfo(): {
|
|
27
|
+
eventCount: number;
|
|
28
|
+
positions: {
|
|
29
|
+
outside: number;
|
|
30
|
+
deadCenter: number;
|
|
31
|
+
overElement: number;
|
|
32
|
+
};
|
|
33
|
+
durations: number[];
|
|
34
|
+
movements: number[];
|
|
35
|
+
inViewport: number;
|
|
36
|
+
trackedElements: number;
|
|
37
|
+
};
|
|
38
|
+
}
|