@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,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Detection Strategies - Modular behavior analysis
|
|
4
|
+
* Import only what you need for optimal bundle size
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.Resize = exports.Environment = exports.Keyboard = exports.Tap = exports.Click = exports.Scroll = exports.Mouse = exports.ResizeStrategy = exports.EnvironmentStrategy = exports.KeyboardStrategy = exports.TapStrategy = exports.ClickStrategy = exports.ScrollStrategy = exports.MouseStrategy = void 0;
|
|
8
|
+
var mouse_1 = require("./mouse");
|
|
9
|
+
Object.defineProperty(exports, "MouseStrategy", { enumerable: true, get: function () { return mouse_1.MouseStrategy; } });
|
|
10
|
+
var scroll_1 = require("./scroll");
|
|
11
|
+
Object.defineProperty(exports, "ScrollStrategy", { enumerable: true, get: function () { return scroll_1.ScrollStrategy; } });
|
|
12
|
+
var click_1 = require("./click");
|
|
13
|
+
Object.defineProperty(exports, "ClickStrategy", { enumerable: true, get: function () { return click_1.ClickStrategy; } });
|
|
14
|
+
var tap_1 = require("./tap");
|
|
15
|
+
Object.defineProperty(exports, "TapStrategy", { enumerable: true, get: function () { return tap_1.TapStrategy; } });
|
|
16
|
+
var keyboard_1 = require("./keyboard");
|
|
17
|
+
Object.defineProperty(exports, "KeyboardStrategy", { enumerable: true, get: function () { return keyboard_1.KeyboardStrategy; } });
|
|
18
|
+
var environment_1 = require("./environment");
|
|
19
|
+
Object.defineProperty(exports, "EnvironmentStrategy", { enumerable: true, get: function () { return environment_1.EnvironmentStrategy; } });
|
|
20
|
+
var resize_1 = require("./resize");
|
|
21
|
+
Object.defineProperty(exports, "ResizeStrategy", { enumerable: true, get: function () { return resize_1.ResizeStrategy; } });
|
|
22
|
+
// Convenience: All strategies
|
|
23
|
+
var mouse_2 = require("./mouse");
|
|
24
|
+
Object.defineProperty(exports, "Mouse", { enumerable: true, get: function () { return mouse_2.MouseStrategy; } });
|
|
25
|
+
var scroll_2 = require("./scroll");
|
|
26
|
+
Object.defineProperty(exports, "Scroll", { enumerable: true, get: function () { return scroll_2.ScrollStrategy; } });
|
|
27
|
+
var click_2 = require("./click");
|
|
28
|
+
Object.defineProperty(exports, "Click", { enumerable: true, get: function () { return click_2.ClickStrategy; } });
|
|
29
|
+
var tap_2 = require("./tap");
|
|
30
|
+
Object.defineProperty(exports, "Tap", { enumerable: true, get: function () { return tap_2.TapStrategy; } });
|
|
31
|
+
var keyboard_2 = require("./keyboard");
|
|
32
|
+
Object.defineProperty(exports, "Keyboard", { enumerable: true, get: function () { return keyboard_2.KeyboardStrategy; } });
|
|
33
|
+
var environment_2 = require("./environment");
|
|
34
|
+
Object.defineProperty(exports, "Environment", { enumerable: true, get: function () { return environment_2.EnvironmentStrategy; } });
|
|
35
|
+
var resize_2 = require("./resize");
|
|
36
|
+
Object.defineProperty(exports, "Resize", { enumerable: true, get: function () { return resize_2.ResizeStrategy; } });
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard Timing Detection Strategy
|
|
3
|
+
* Monitors typing in specific input fields
|
|
4
|
+
* Analyzes keydown/keyup timing and micro-variations
|
|
5
|
+
*/
|
|
6
|
+
import { BaseStrategy } from '../strategy';
|
|
7
|
+
export declare class KeyboardStrategy extends BaseStrategy {
|
|
8
|
+
readonly name = "keyboard";
|
|
9
|
+
readonly defaultWeight = 0.1;
|
|
10
|
+
private events;
|
|
11
|
+
private targetSelectors;
|
|
12
|
+
private focusedElement;
|
|
13
|
+
private focusedElementSelector;
|
|
14
|
+
private lastEventTimestamp;
|
|
15
|
+
private sessionPauseThreshold;
|
|
16
|
+
private downListener;
|
|
17
|
+
private upListener;
|
|
18
|
+
private focusListeners;
|
|
19
|
+
private blurListeners;
|
|
20
|
+
private isActive;
|
|
21
|
+
constructor(options?: {
|
|
22
|
+
targetSelectors?: string[];
|
|
23
|
+
});
|
|
24
|
+
start(): void;
|
|
25
|
+
/**
|
|
26
|
+
* Add a new target selector at runtime
|
|
27
|
+
*/
|
|
28
|
+
addTarget(selector: string): void;
|
|
29
|
+
private attachFocusListeners;
|
|
30
|
+
private attachFocusListenersForSelector;
|
|
31
|
+
stop(): void;
|
|
32
|
+
reset(): void;
|
|
33
|
+
score(): number | undefined;
|
|
34
|
+
getDebugInfo(): {
|
|
35
|
+
eventCount: number;
|
|
36
|
+
downEvents: number;
|
|
37
|
+
upEvents: number;
|
|
38
|
+
backspaceCount: number;
|
|
39
|
+
pressDurations: number[];
|
|
40
|
+
focusedElement: string;
|
|
41
|
+
trackedElements: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Keyboard Timing Detection Strategy
|
|
4
|
+
* Monitors typing in specific input fields
|
|
5
|
+
* Analyzes keydown/keyup timing and micro-variations
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.KeyboardStrategy = void 0;
|
|
9
|
+
const strategy_1 = require("../strategy");
|
|
10
|
+
const math_utils_1 = require("../math-utils");
|
|
11
|
+
class KeyboardStrategy extends strategy_1.BaseStrategy {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
super();
|
|
14
|
+
this.name = 'keyboard';
|
|
15
|
+
this.defaultWeight = 0.10;
|
|
16
|
+
this.events = [];
|
|
17
|
+
this.targetSelectors = ['input[type="text"]', 'input[type="email"]', 'textarea'];
|
|
18
|
+
this.focusedElement = null;
|
|
19
|
+
this.focusedElementSelector = '';
|
|
20
|
+
this.lastEventTimestamp = 0;
|
|
21
|
+
this.sessionPauseThreshold = 1000;
|
|
22
|
+
this.downListener = null;
|
|
23
|
+
this.upListener = null;
|
|
24
|
+
this.focusListeners = new Map();
|
|
25
|
+
this.blurListeners = new Map();
|
|
26
|
+
this.isActive = false;
|
|
27
|
+
if (options === null || options === void 0 ? void 0 : options.targetSelectors) {
|
|
28
|
+
this.targetSelectors = options.targetSelectors;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
start() {
|
|
32
|
+
if (this.isActive)
|
|
33
|
+
return;
|
|
34
|
+
this.isActive = true;
|
|
35
|
+
// Attach focus/blur listeners to all matching elements
|
|
36
|
+
this.attachFocusListeners();
|
|
37
|
+
// Key event listeners (only track when an input is focused)
|
|
38
|
+
this.downListener = (e) => {
|
|
39
|
+
if (!this.focusedElement)
|
|
40
|
+
return; // No input focused
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
const keyEvent = e;
|
|
43
|
+
// Clear events if pause > 1s (new typing session)
|
|
44
|
+
if (this.lastEventTimestamp > 0 && now - this.lastEventTimestamp > this.sessionPauseThreshold) {
|
|
45
|
+
this.events = [];
|
|
46
|
+
}
|
|
47
|
+
this.events.push({
|
|
48
|
+
key: keyEvent.key,
|
|
49
|
+
type: 'down',
|
|
50
|
+
timestamp: now,
|
|
51
|
+
targetElement: this.focusedElementSelector,
|
|
52
|
+
});
|
|
53
|
+
// Notify detector - keystrokes are valuable events
|
|
54
|
+
this.notifyEvent(0.8);
|
|
55
|
+
this.lastEventTimestamp = now;
|
|
56
|
+
};
|
|
57
|
+
this.upListener = (e) => {
|
|
58
|
+
if (!this.focusedElement)
|
|
59
|
+
return; // No input focused
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const keyEvent = e;
|
|
62
|
+
// Clear events if pause > 1s (new typing session)
|
|
63
|
+
if (this.lastEventTimestamp > 0 && now - this.lastEventTimestamp > this.sessionPauseThreshold) {
|
|
64
|
+
this.events = [];
|
|
65
|
+
}
|
|
66
|
+
this.events.push({
|
|
67
|
+
key: keyEvent.key,
|
|
68
|
+
type: 'up',
|
|
69
|
+
timestamp: now,
|
|
70
|
+
targetElement: this.focusedElementSelector,
|
|
71
|
+
});
|
|
72
|
+
this.lastEventTimestamp = now;
|
|
73
|
+
};
|
|
74
|
+
document.addEventListener('keydown', this.downListener);
|
|
75
|
+
document.addEventListener('keyup', this.upListener);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Add a new target selector at runtime
|
|
79
|
+
*/
|
|
80
|
+
addTarget(selector) {
|
|
81
|
+
if (!this.targetSelectors.includes(selector)) {
|
|
82
|
+
this.targetSelectors.push(selector);
|
|
83
|
+
if (this.isActive) {
|
|
84
|
+
this.attachFocusListenersForSelector(selector);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
attachFocusListeners() {
|
|
89
|
+
this.targetSelectors.forEach(selector => {
|
|
90
|
+
this.attachFocusListenersForSelector(selector);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
attachFocusListenersForSelector(selector) {
|
|
94
|
+
const elements = document.querySelectorAll(selector);
|
|
95
|
+
elements.forEach(element => {
|
|
96
|
+
if (this.focusListeners.has(element))
|
|
97
|
+
return; // Already attached
|
|
98
|
+
const focusListener = () => {
|
|
99
|
+
this.focusedElement = element;
|
|
100
|
+
this.focusedElementSelector = selector;
|
|
101
|
+
};
|
|
102
|
+
const blurListener = () => {
|
|
103
|
+
this.focusedElement = null;
|
|
104
|
+
this.focusedElementSelector = '';
|
|
105
|
+
};
|
|
106
|
+
element.addEventListener('focus', focusListener);
|
|
107
|
+
element.addEventListener('blur', blurListener);
|
|
108
|
+
this.focusListeners.set(element, focusListener);
|
|
109
|
+
this.blurListeners.set(element, blurListener);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
stop() {
|
|
113
|
+
if (!this.isActive)
|
|
114
|
+
return;
|
|
115
|
+
this.isActive = false;
|
|
116
|
+
if (this.downListener) {
|
|
117
|
+
document.removeEventListener('keydown', this.downListener);
|
|
118
|
+
this.downListener = null;
|
|
119
|
+
}
|
|
120
|
+
if (this.upListener) {
|
|
121
|
+
document.removeEventListener('keyup', this.upListener);
|
|
122
|
+
this.upListener = null;
|
|
123
|
+
}
|
|
124
|
+
// Remove focus/blur listeners
|
|
125
|
+
this.focusListeners.forEach((listener, element) => {
|
|
126
|
+
element.removeEventListener('focus', listener);
|
|
127
|
+
});
|
|
128
|
+
this.focusListeners.clear();
|
|
129
|
+
this.blurListeners.forEach((listener, element) => {
|
|
130
|
+
element.removeEventListener('blur', listener);
|
|
131
|
+
});
|
|
132
|
+
this.blurListeners.clear();
|
|
133
|
+
this.focusedElement = null;
|
|
134
|
+
}
|
|
135
|
+
reset() {
|
|
136
|
+
this.events = [];
|
|
137
|
+
this.focusedElement = null;
|
|
138
|
+
this.focusedElementSelector = '';
|
|
139
|
+
this.lastEventTimestamp = 0;
|
|
140
|
+
}
|
|
141
|
+
score() {
|
|
142
|
+
if (this.events.length < 6)
|
|
143
|
+
return undefined; // Need at least 3 key pairs
|
|
144
|
+
const downEvents = this.events.filter(e => e.type === 'down');
|
|
145
|
+
if (downEvents.length < 3)
|
|
146
|
+
return undefined;
|
|
147
|
+
let score = 0;
|
|
148
|
+
let factors = 0;
|
|
149
|
+
// 1. Analyze keystroke intervals (time between keydown events)
|
|
150
|
+
const keystrokeIntervals = [];
|
|
151
|
+
for (let i = 1; i < downEvents.length; i++) {
|
|
152
|
+
keystrokeIntervals.push(downEvents[i].timestamp - downEvents[i - 1].timestamp);
|
|
153
|
+
}
|
|
154
|
+
const keystrokeAnalysis = (0, math_utils_1.analyzeIntervals)(keystrokeIntervals);
|
|
155
|
+
if (keystrokeAnalysis) {
|
|
156
|
+
const { statistics, allIdentical } = keystrokeAnalysis;
|
|
157
|
+
// Keyboard-specific heuristics for keystroke timing
|
|
158
|
+
if (allIdentical) {
|
|
159
|
+
return 0.1; // All identical - bot!
|
|
160
|
+
}
|
|
161
|
+
const keystrokeScore = (0, math_utils_1.scoreCoefficientOfVariation)(statistics.cv);
|
|
162
|
+
score += keystrokeScore;
|
|
163
|
+
factors++;
|
|
164
|
+
// Early return if bot detected
|
|
165
|
+
if (keystrokeScore <= 0.1)
|
|
166
|
+
return keystrokeScore;
|
|
167
|
+
}
|
|
168
|
+
// 2. Analyze press durations (keydown-to-keyup duration)
|
|
169
|
+
const pressDurations = [];
|
|
170
|
+
for (let i = 0; i < this.events.length - 1; i++) {
|
|
171
|
+
if (this.events[i].type === 'down' && this.events[i + 1].type === 'up' &&
|
|
172
|
+
this.events[i].key === this.events[i + 1].key) {
|
|
173
|
+
pressDurations.push(this.events[i + 1].timestamp - this.events[i].timestamp);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const pressDurationAnalysis = (0, math_utils_1.analyzeIntervals)(pressDurations);
|
|
177
|
+
if (pressDurationAnalysis) {
|
|
178
|
+
const { statistics, allIdentical } = pressDurationAnalysis;
|
|
179
|
+
// Keyboard-specific heuristics for press duration
|
|
180
|
+
if (allIdentical) {
|
|
181
|
+
return 0.1; // All identical - bot!
|
|
182
|
+
}
|
|
183
|
+
// Check for suspiciously fast key releases (bots often have <5ms press duration)
|
|
184
|
+
if (statistics.mean < 5) {
|
|
185
|
+
score += 0.1; // Too fast = bot (instant release)
|
|
186
|
+
factors++;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
const pressDurationScore = (0, math_utils_1.scoreCoefficientOfVariation)(statistics.cv);
|
|
190
|
+
score += pressDurationScore;
|
|
191
|
+
factors++;
|
|
192
|
+
// Early return if bot detected
|
|
193
|
+
if (pressDurationScore <= 0.1)
|
|
194
|
+
return pressDurationScore;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// 3. Bonus for backspace usage (human error correction)
|
|
198
|
+
const backspaceCount = this.events.filter(e => e.key === 'Backspace').length;
|
|
199
|
+
if (backspaceCount > 0) {
|
|
200
|
+
// Natural human behavior - making corrections
|
|
201
|
+
const backspaceRatio = backspaceCount / downEvents.length;
|
|
202
|
+
if (backspaceRatio > 0.05 && backspaceRatio < 0.3) {
|
|
203
|
+
score += 1.0; // Reasonable error correction
|
|
204
|
+
factors++;
|
|
205
|
+
}
|
|
206
|
+
else if (backspaceRatio > 0) {
|
|
207
|
+
score += 0.8; // Some backspaces
|
|
208
|
+
factors++;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return factors > 0 ? score / factors : undefined;
|
|
212
|
+
}
|
|
213
|
+
getDebugInfo() {
|
|
214
|
+
// Calculate press durations for visualization
|
|
215
|
+
const pressDurations = [];
|
|
216
|
+
for (let i = 0; i < this.events.length - 1; i++) {
|
|
217
|
+
if (this.events[i].type === 'down' && this.events[i + 1].type === 'up' &&
|
|
218
|
+
this.events[i].key === this.events[i + 1].key) {
|
|
219
|
+
pressDurations.push(this.events[i + 1].timestamp - this.events[i].timestamp);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
eventCount: this.events.length,
|
|
224
|
+
downEvents: this.events.filter(e => e.type === 'down').length,
|
|
225
|
+
upEvents: this.events.filter(e => e.type === 'up').length,
|
|
226
|
+
backspaceCount: this.events.filter(e => e.key === 'Backspace').length,
|
|
227
|
+
pressDurations, // For graphing
|
|
228
|
+
focusedElement: this.focusedElementSelector,
|
|
229
|
+
trackedElements: this.focusListeners.size,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
exports.KeyboardStrategy = KeyboardStrategy;
|
|
@@ -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,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Mouse Movement Detection Strategy
|
|
4
|
+
* Autonomous module that manages its own mouse event listeners and state
|
|
5
|
+
* Tracks distance and angle to detect unnatural jumps and sharp turns
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.MouseStrategy = void 0;
|
|
9
|
+
const strategy_1 = require("../strategy");
|
|
10
|
+
const math_utils_1 = require("../math-utils");
|
|
11
|
+
class MouseStrategy extends strategy_1.BaseStrategy {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
super();
|
|
14
|
+
this.name = 'mouse';
|
|
15
|
+
this.defaultWeight = 0.30;
|
|
16
|
+
this.distanceSeries = [];
|
|
17
|
+
this.angleSeries = [];
|
|
18
|
+
this.lastPosition = null;
|
|
19
|
+
this.lastAngle = 0;
|
|
20
|
+
this.cumulativeAngle = 0;
|
|
21
|
+
this.rollingWindowMs = 5000;
|
|
22
|
+
this.listener = null;
|
|
23
|
+
this.leaveListener = null;
|
|
24
|
+
this.isActive = false;
|
|
25
|
+
this.screenDiagonal = 1;
|
|
26
|
+
if ((options === null || options === void 0 ? void 0 : options.rollingWindow) !== undefined)
|
|
27
|
+
this.rollingWindowMs = options.rollingWindow;
|
|
28
|
+
}
|
|
29
|
+
start() {
|
|
30
|
+
if (this.isActive)
|
|
31
|
+
return;
|
|
32
|
+
this.isActive = true;
|
|
33
|
+
// Calculate screen diagonal for normalization (distance relative to screen size)
|
|
34
|
+
const width = window.innerWidth;
|
|
35
|
+
const height = window.innerHeight;
|
|
36
|
+
this.screenDiagonal = Math.sqrt(width * width + height * height);
|
|
37
|
+
this.listener = (e) => {
|
|
38
|
+
const mouseEvent = e;
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
const currentPos = { x: mouseEvent.clientX, y: mouseEvent.clientY };
|
|
41
|
+
// Calculate distance and angle from previous position
|
|
42
|
+
if (this.lastPosition) {
|
|
43
|
+
const dx = currentPos.x - this.lastPosition.x;
|
|
44
|
+
const dy = currentPos.y - this.lastPosition.y;
|
|
45
|
+
const pixelDistance = Math.sqrt(dx * dx + dy * dy);
|
|
46
|
+
// Normalize distance to screen diagonal (0-1 range)
|
|
47
|
+
// A full diagonal movement = 1.0, a 10px movement on 2000px screen = 0.005
|
|
48
|
+
const normalizedDistance = pixelDistance / this.screenDiagonal;
|
|
49
|
+
// Only record if movement is meaningful (> 0.001 = ~2px on 2000px screen)
|
|
50
|
+
if (normalizedDistance > 0.001) {
|
|
51
|
+
const rawAngle = Math.atan2(dy, dx); // -PI to PI
|
|
52
|
+
// Unwrap angle to avoid discontinuities (e.g., circles)
|
|
53
|
+
let angleDiff = rawAngle - this.lastAngle;
|
|
54
|
+
// Normalize difference to -PI to PI
|
|
55
|
+
while (angleDiff > Math.PI)
|
|
56
|
+
angleDiff -= 2 * Math.PI;
|
|
57
|
+
while (angleDiff < -Math.PI)
|
|
58
|
+
angleDiff += 2 * Math.PI;
|
|
59
|
+
this.cumulativeAngle += angleDiff;
|
|
60
|
+
this.lastAngle = rawAngle;
|
|
61
|
+
// Store normalized distance (not raw pixels)
|
|
62
|
+
this.distanceSeries.push({ value: normalizedDistance, timestamp: now });
|
|
63
|
+
this.angleSeries.push({ value: this.cumulativeAngle, timestamp: now });
|
|
64
|
+
// Notify detector for confidence tracking
|
|
65
|
+
this.notifyEvent(Math.min(1, normalizedDistance * 100));
|
|
66
|
+
}
|
|
67
|
+
// Apply rolling window efficiently: remove old events from start
|
|
68
|
+
// Since events are chronological, we only check from the beginning
|
|
69
|
+
const cutoff = now - this.rollingWindowMs;
|
|
70
|
+
while (this.distanceSeries.length > 0 && this.distanceSeries[0].timestamp < cutoff) {
|
|
71
|
+
this.distanceSeries.shift();
|
|
72
|
+
this.angleSeries.shift(); // Keep both arrays in sync
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
this.lastPosition = currentPos;
|
|
76
|
+
};
|
|
77
|
+
document.addEventListener('mousemove', this.listener, { passive: true });
|
|
78
|
+
// Clear data when mouse leaves the window (discontinuous tracking)
|
|
79
|
+
this.leaveListener = () => {
|
|
80
|
+
this.distanceSeries = [];
|
|
81
|
+
this.angleSeries = [];
|
|
82
|
+
this.lastPosition = null;
|
|
83
|
+
this.lastAngle = 0;
|
|
84
|
+
this.cumulativeAngle = 0;
|
|
85
|
+
};
|
|
86
|
+
document.addEventListener('mouseleave', this.leaveListener, { passive: true });
|
|
87
|
+
}
|
|
88
|
+
stop() {
|
|
89
|
+
if (!this.isActive)
|
|
90
|
+
return;
|
|
91
|
+
this.isActive = false;
|
|
92
|
+
if (this.listener) {
|
|
93
|
+
document.removeEventListener('mousemove', this.listener);
|
|
94
|
+
this.listener = null;
|
|
95
|
+
}
|
|
96
|
+
if (this.leaveListener) {
|
|
97
|
+
document.removeEventListener('mouseleave', this.leaveListener);
|
|
98
|
+
this.leaveListener = null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
reset() {
|
|
102
|
+
this.distanceSeries = [];
|
|
103
|
+
this.angleSeries = [];
|
|
104
|
+
this.lastPosition = null;
|
|
105
|
+
this.lastAngle = 0;
|
|
106
|
+
this.cumulativeAngle = 0;
|
|
107
|
+
}
|
|
108
|
+
score() {
|
|
109
|
+
if (this.distanceSeries.length < 10)
|
|
110
|
+
return undefined;
|
|
111
|
+
// Mouse-specific pattern detection (optimized for normalized continuous data)
|
|
112
|
+
// Generic smoothness detector is calibrated for discrete events, not continuous movement
|
|
113
|
+
return this.detectMousePatterns();
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Mouse-specific pattern detection
|
|
117
|
+
* Detects bot-like patterns: constant velocity, linear paths
|
|
118
|
+
*/
|
|
119
|
+
detectMousePatterns() {
|
|
120
|
+
if (this.distanceSeries.length < 10)
|
|
121
|
+
return undefined;
|
|
122
|
+
let score = 0;
|
|
123
|
+
let factors = 0;
|
|
124
|
+
// 1. VELOCITY CONSISTENCY - Bots often move at constant speed
|
|
125
|
+
const distances = this.distanceSeries.map(p => p.value);
|
|
126
|
+
if (distances.length >= 3) {
|
|
127
|
+
const stats = (0, math_utils_1.calculateStatistics)(distances);
|
|
128
|
+
// Real human movement has high variation (CV ~0.8-1.2)
|
|
129
|
+
// page.mouse.move() with steps has lower CV (~0.4-0.6)
|
|
130
|
+
// Narrower gaussian to be strict
|
|
131
|
+
score += (0, math_utils_1.gaussian)(stats.cv, 0.9, 0.35);
|
|
132
|
+
factors++;
|
|
133
|
+
}
|
|
134
|
+
// 2. DIRECTION CHANGES - Bots often have too few or too many sharp turns
|
|
135
|
+
const angles = this.angleSeries.map(p => p.value);
|
|
136
|
+
if (angles.length >= 3) {
|
|
137
|
+
const angleChanges = [];
|
|
138
|
+
for (let i = 1; i < angles.length; i++) {
|
|
139
|
+
angleChanges.push(Math.abs(angles[i] - angles[i - 1]));
|
|
140
|
+
}
|
|
141
|
+
const avgChange = angleChanges.reduce((a, b) => a + b, 0) / angleChanges.length;
|
|
142
|
+
// Real humans have moderate but varied direction changes
|
|
143
|
+
// page.mouse.move() linear interpolation has very small, consistent changes
|
|
144
|
+
score += (0, math_utils_1.gaussian)(avgChange, 0.15, 0.12);
|
|
145
|
+
factors++;
|
|
146
|
+
}
|
|
147
|
+
return factors > 0 ? score / factors : undefined;
|
|
148
|
+
}
|
|
149
|
+
getDebugInfo() {
|
|
150
|
+
return {
|
|
151
|
+
eventCount: this.distanceSeries.length,
|
|
152
|
+
rollingWindow: this.rollingWindowMs,
|
|
153
|
+
isActive: this.isActive,
|
|
154
|
+
distanceSeries: this.distanceSeries,
|
|
155
|
+
angleSeries: this.angleSeries,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
exports.MouseStrategy = MouseStrategy;
|
|
@@ -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,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Resize Behavior Detection Strategy
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ResizeStrategy = void 0;
|
|
7
|
+
const strategy_1 = require("../strategy");
|
|
8
|
+
const math_utils_1 = require("../math-utils");
|
|
9
|
+
class ResizeStrategy extends strategy_1.BaseStrategy {
|
|
10
|
+
constructor() {
|
|
11
|
+
super(...arguments);
|
|
12
|
+
this.name = 'resize';
|
|
13
|
+
this.defaultWeight = 0.02;
|
|
14
|
+
this.events = [];
|
|
15
|
+
this.listener = null;
|
|
16
|
+
this.isActive = false;
|
|
17
|
+
this.lastMousePosition = null;
|
|
18
|
+
this.mouseListener = null;
|
|
19
|
+
}
|
|
20
|
+
start() {
|
|
21
|
+
if (this.isActive)
|
|
22
|
+
return;
|
|
23
|
+
this.isActive = true;
|
|
24
|
+
// Track mouse for resize detection
|
|
25
|
+
this.mouseListener = (e) => {
|
|
26
|
+
const mouseEvent = e;
|
|
27
|
+
this.lastMousePosition = {
|
|
28
|
+
x: mouseEvent.clientX,
|
|
29
|
+
y: mouseEvent.clientY,
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
document.addEventListener('mousemove', this.mouseListener, { passive: true });
|
|
33
|
+
// Track resizes
|
|
34
|
+
this.listener = () => {
|
|
35
|
+
var _a, _b;
|
|
36
|
+
const mouseX = (_a = this.lastMousePosition) === null || _a === void 0 ? void 0 : _a.x;
|
|
37
|
+
const mouseY = (_b = this.lastMousePosition) === null || _b === void 0 ? void 0 : _b.y;
|
|
38
|
+
let mouseNearEdge = false;
|
|
39
|
+
if (mouseX !== undefined && mouseY !== undefined) {
|
|
40
|
+
const edgeThreshold = 50;
|
|
41
|
+
mouseNearEdge = (mouseX < edgeThreshold ||
|
|
42
|
+
mouseX > window.innerWidth - edgeThreshold ||
|
|
43
|
+
mouseY < edgeThreshold ||
|
|
44
|
+
mouseY > window.innerHeight - edgeThreshold);
|
|
45
|
+
}
|
|
46
|
+
this.events.push({
|
|
47
|
+
width: window.innerWidth,
|
|
48
|
+
height: window.innerHeight,
|
|
49
|
+
mouseX,
|
|
50
|
+
mouseY,
|
|
51
|
+
mouseNearEdge,
|
|
52
|
+
timestamp: Date.now(),
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
window.addEventListener('resize', this.listener);
|
|
56
|
+
}
|
|
57
|
+
stop() {
|
|
58
|
+
if (!this.isActive)
|
|
59
|
+
return;
|
|
60
|
+
this.isActive = false;
|
|
61
|
+
if (this.listener) {
|
|
62
|
+
window.removeEventListener('resize', this.listener);
|
|
63
|
+
this.listener = null;
|
|
64
|
+
}
|
|
65
|
+
if (this.mouseListener) {
|
|
66
|
+
document.removeEventListener('mousemove', this.mouseListener);
|
|
67
|
+
this.mouseListener = null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
reset() {
|
|
71
|
+
this.events = [];
|
|
72
|
+
}
|
|
73
|
+
score() {
|
|
74
|
+
if (this.events.length === 0)
|
|
75
|
+
return undefined;
|
|
76
|
+
let score = 0;
|
|
77
|
+
let factors = 0;
|
|
78
|
+
// Frequency
|
|
79
|
+
score += (0, math_utils_1.inverseSigmoid)(this.events.length, 5, 0.5);
|
|
80
|
+
factors++;
|
|
81
|
+
// Mouse near edge
|
|
82
|
+
const withMouse = this.events.filter(e => e.mouseX !== undefined);
|
|
83
|
+
if (withMouse.length > 0) {
|
|
84
|
+
const nearEdge = withMouse.filter(e => e.mouseNearEdge).length;
|
|
85
|
+
score += (0, math_utils_1.sigmoid)(nearEdge / withMouse.length, 0.5, 8);
|
|
86
|
+
factors++;
|
|
87
|
+
}
|
|
88
|
+
return factors > 0 ? score / factors : undefined;
|
|
89
|
+
}
|
|
90
|
+
getDebugInfo() {
|
|
91
|
+
return {
|
|
92
|
+
eventCount: this.events.length,
|
|
93
|
+
withMouseData: this.events.filter(e => e.mouseX !== undefined).length,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.ResizeStrategy = ResizeStrategy;
|
|
@@ -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
|
+
}
|