@axeptio/behavior-detection 1.0.2 → 1.1.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 +126 -12
- package/dist/behavior-detection.esm.min.js +1 -1
- package/dist/behavior-detection.esm.min.js.map +4 -4
- package/dist/behavior-detection.min.js +1 -1
- package/dist/behavior-detection.min.js.map +3 -3
- package/dist/cjs/index.cjs +2320 -0
- package/dist/esm/browser.js +0 -2
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/strategies/click.d.ts +8 -0
- package/dist/esm/strategies/click.js +108 -9
- package/dist/esm/strategies/environment.d.ts +39 -0
- package/dist/esm/strategies/environment.js +188 -7
- package/dist/esm/strategies/index.d.ts +4 -0
- package/dist/esm/strategies/index.js +4 -0
- package/dist/esm/strategies/mouse.d.ts +53 -1
- package/dist/esm/strategies/mouse.js +198 -2
- package/dist/esm/strategies/timing.d.ts +64 -0
- package/dist/esm/strategies/timing.js +308 -0
- package/dist/esm/strategies/visibility.d.ts +64 -0
- package/dist/esm/strategies/visibility.js +295 -0
- package/dist/index.d.ts +1 -1
- package/dist/strategies/click.d.ts +8 -0
- package/dist/strategies/environment.d.ts +39 -0
- package/dist/strategies/index.d.ts +4 -0
- package/dist/strategies/mouse.d.ts +53 -1
- package/dist/strategies/timing.d.ts +64 -0
- package/dist/strategies/visibility.d.ts +64 -0
- package/package.json +16 -15
- package/dist/cjs/behavior-detector.d.ts +0 -102
- package/dist/cjs/behavior-detector.js +0 -315
- package/dist/cjs/browser.d.ts +0 -33
- package/dist/cjs/browser.js +0 -226
- package/dist/cjs/index.d.ts +0 -38
- package/dist/cjs/index.js +0 -55
- package/dist/cjs/math-utils.d.ts +0 -84
- package/dist/cjs/math-utils.js +0 -141
- package/dist/cjs/strategies/click.d.ts +0 -39
- package/dist/cjs/strategies/click.js +0 -173
- package/dist/cjs/strategies/environment.d.ts +0 -52
- package/dist/cjs/strategies/environment.js +0 -148
- package/dist/cjs/strategies/index.d.ts +0 -18
- package/dist/cjs/strategies/index.js +0 -36
- package/dist/cjs/strategies/keyboard.d.ts +0 -43
- package/dist/cjs/strategies/keyboard.js +0 -233
- package/dist/cjs/strategies/mouse.d.ts +0 -39
- package/dist/cjs/strategies/mouse.js +0 -159
- package/dist/cjs/strategies/resize.d.ts +0 -21
- package/dist/cjs/strategies/resize.js +0 -97
- package/dist/cjs/strategies/scroll.d.ts +0 -37
- package/dist/cjs/strategies/scroll.js +0 -149
- package/dist/cjs/strategies/tap.d.ts +0 -38
- package/dist/cjs/strategies/tap.js +0 -214
- package/dist/cjs/strategy.d.ts +0 -107
- package/dist/cjs/strategy.js +0 -33
- package/dist/cjs/types.d.ts +0 -168
- package/dist/cjs/types.js +0 -26
- package/dist/esm/browser-iife.d.ts +0 -5
- package/dist/esm/browser-iife.js +0 -157
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visibility Strategy
|
|
3
|
+
* Detects bot-like behavior based on tab visibility and focus patterns
|
|
4
|
+
*
|
|
5
|
+
* Key detection signals:
|
|
6
|
+
* - Actions while document.hidden === true (bots ignore visibility)
|
|
7
|
+
* - No pause when tab loses focus
|
|
8
|
+
* - Activity resumes too quickly after tab regains focus
|
|
9
|
+
* - Focus patterns: humans focus inputs before typing
|
|
10
|
+
*/
|
|
11
|
+
import { BaseStrategy } from '../strategy.js';
|
|
12
|
+
export declare class VisibilityStrategy extends BaseStrategy {
|
|
13
|
+
readonly name = "visibility";
|
|
14
|
+
readonly defaultWeight = 0.1;
|
|
15
|
+
private events;
|
|
16
|
+
private focusTypingPairs;
|
|
17
|
+
private actionsWhileHidden;
|
|
18
|
+
private lastVisibilityChange;
|
|
19
|
+
private resumeDelays;
|
|
20
|
+
private isActive;
|
|
21
|
+
private visibilityListener;
|
|
22
|
+
private focusListener;
|
|
23
|
+
private blurListener;
|
|
24
|
+
private clickListener;
|
|
25
|
+
private keydownListener;
|
|
26
|
+
private inputFocusListener;
|
|
27
|
+
private lastFocusedInput;
|
|
28
|
+
private hasTypedInFocusedInput;
|
|
29
|
+
private lastActionTime;
|
|
30
|
+
private preHideActionTime;
|
|
31
|
+
start(): void;
|
|
32
|
+
private recordAction;
|
|
33
|
+
stop(): void;
|
|
34
|
+
reset(): void;
|
|
35
|
+
score(): number | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Score based on actions while document was hidden
|
|
38
|
+
* Humans can't interact with hidden tabs; bots can
|
|
39
|
+
*/
|
|
40
|
+
private scoreHiddenActions;
|
|
41
|
+
/**
|
|
42
|
+
* Score based on how quickly activity resumes after tab becomes visible
|
|
43
|
+
* Humans need time to refocus (100-500ms minimum)
|
|
44
|
+
* Bots often resume instantly (< 50ms)
|
|
45
|
+
*/
|
|
46
|
+
private scoreResumeDelays;
|
|
47
|
+
/**
|
|
48
|
+
* Score based on focus-to-keypress timing
|
|
49
|
+
* Humans focus inputs before typing (natural delay 100-500ms)
|
|
50
|
+
* Bots often type without focusing or with instant delay
|
|
51
|
+
*/
|
|
52
|
+
private scoreFocusTyping;
|
|
53
|
+
getDebugInfo(): {
|
|
54
|
+
eventCount: number;
|
|
55
|
+
actionsWhileHidden: number;
|
|
56
|
+
visibilityChanges: number;
|
|
57
|
+
focusChanges: number;
|
|
58
|
+
resumeDelays: number[];
|
|
59
|
+
focusTypingPairs: {
|
|
60
|
+
delay: number;
|
|
61
|
+
element: string;
|
|
62
|
+
}[];
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visibility Strategy
|
|
3
|
+
* Detects bot-like behavior based on tab visibility and focus patterns
|
|
4
|
+
*
|
|
5
|
+
* Key detection signals:
|
|
6
|
+
* - Actions while document.hidden === true (bots ignore visibility)
|
|
7
|
+
* - No pause when tab loses focus
|
|
8
|
+
* - Activity resumes too quickly after tab regains focus
|
|
9
|
+
* - Focus patterns: humans focus inputs before typing
|
|
10
|
+
*/
|
|
11
|
+
import { BaseStrategy } from '../strategy.js';
|
|
12
|
+
export class VisibilityStrategy extends BaseStrategy {
|
|
13
|
+
constructor() {
|
|
14
|
+
super(...arguments);
|
|
15
|
+
this.name = 'visibility';
|
|
16
|
+
this.defaultWeight = 0.10;
|
|
17
|
+
this.events = [];
|
|
18
|
+
this.focusTypingPairs = [];
|
|
19
|
+
this.actionsWhileHidden = 0;
|
|
20
|
+
this.lastVisibilityChange = null;
|
|
21
|
+
this.resumeDelays = [];
|
|
22
|
+
this.isActive = false;
|
|
23
|
+
// Listeners
|
|
24
|
+
this.visibilityListener = null;
|
|
25
|
+
this.focusListener = null;
|
|
26
|
+
this.blurListener = null;
|
|
27
|
+
this.clickListener = null;
|
|
28
|
+
this.keydownListener = null;
|
|
29
|
+
this.inputFocusListener = null;
|
|
30
|
+
// State
|
|
31
|
+
this.lastFocusedInput = null;
|
|
32
|
+
this.hasTypedInFocusedInput = false;
|
|
33
|
+
this.lastActionTime = 0;
|
|
34
|
+
this.preHideActionTime = 0;
|
|
35
|
+
}
|
|
36
|
+
start() {
|
|
37
|
+
if (this.isActive)
|
|
38
|
+
return;
|
|
39
|
+
this.isActive = true;
|
|
40
|
+
// Track visibility changes
|
|
41
|
+
this.visibilityListener = () => {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
const wasHidden = document.hidden;
|
|
44
|
+
// Track time between visibility changes
|
|
45
|
+
if (this.lastVisibilityChange && !wasHidden) {
|
|
46
|
+
// Tab just became visible again
|
|
47
|
+
// Check if there was activity right before becoming visible
|
|
48
|
+
// Humans need time to refocus after tab switch
|
|
49
|
+
if (this.lastActionTime > 0 && this.preHideActionTime > 0) {
|
|
50
|
+
const timeSinceLastAction = now - this.lastActionTime;
|
|
51
|
+
// If action was very recent (< 50ms), suspicious
|
|
52
|
+
if (timeSinceLastAction < 50) {
|
|
53
|
+
this.actionsWhileHidden++;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Track resume delay (time until first action after visibility)
|
|
57
|
+
// Will be calculated when next action occurs
|
|
58
|
+
this.lastVisibilityChange = { hidden: false, timestamp: now };
|
|
59
|
+
}
|
|
60
|
+
else if (wasHidden) {
|
|
61
|
+
// Tab just became hidden - record pre-hide action time
|
|
62
|
+
this.preHideActionTime = this.lastActionTime;
|
|
63
|
+
this.lastVisibilityChange = { hidden: true, timestamp: now };
|
|
64
|
+
}
|
|
65
|
+
this.events.push({
|
|
66
|
+
type: 'visibility_change',
|
|
67
|
+
timestamp: now,
|
|
68
|
+
wasHidden,
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
// Track window focus
|
|
72
|
+
this.focusListener = () => {
|
|
73
|
+
this.events.push({
|
|
74
|
+
type: 'focus_change',
|
|
75
|
+
timestamp: Date.now(),
|
|
76
|
+
wasHidden: document.hidden,
|
|
77
|
+
wasFocused: true,
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
this.blurListener = () => {
|
|
81
|
+
this.events.push({
|
|
82
|
+
type: 'focus_change',
|
|
83
|
+
timestamp: Date.now(),
|
|
84
|
+
wasHidden: document.hidden,
|
|
85
|
+
wasFocused: false,
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
// Track actions while hidden
|
|
89
|
+
this.clickListener = (e) => {
|
|
90
|
+
this.recordAction('click', e);
|
|
91
|
+
};
|
|
92
|
+
this.keydownListener = (e) => {
|
|
93
|
+
this.recordAction('keydown', e);
|
|
94
|
+
// Track focus-to-keypress timing
|
|
95
|
+
if (this.lastFocusedInput && !this.hasTypedInFocusedInput) {
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
const delay = now - this.lastFocusedInput.timestamp;
|
|
98
|
+
this.focusTypingPairs.push({
|
|
99
|
+
focusTime: this.lastFocusedInput.timestamp,
|
|
100
|
+
firstKeypressTime: now,
|
|
101
|
+
delay,
|
|
102
|
+
element: this.lastFocusedInput.element,
|
|
103
|
+
});
|
|
104
|
+
this.hasTypedInFocusedInput = true;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
// Track input focus for focus-typing analysis
|
|
108
|
+
this.inputFocusListener = (e) => {
|
|
109
|
+
const target = e.target;
|
|
110
|
+
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) {
|
|
111
|
+
this.lastFocusedInput = {
|
|
112
|
+
element: target.tagName.toLowerCase(),
|
|
113
|
+
timestamp: Date.now(),
|
|
114
|
+
};
|
|
115
|
+
this.hasTypedInFocusedInput = false;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
document.addEventListener('visibilitychange', this.visibilityListener);
|
|
119
|
+
window.addEventListener('focus', this.focusListener);
|
|
120
|
+
window.addEventListener('blur', this.blurListener);
|
|
121
|
+
document.addEventListener('click', this.clickListener, { passive: true });
|
|
122
|
+
document.addEventListener('keydown', this.keydownListener, { passive: true });
|
|
123
|
+
document.addEventListener('focusin', this.inputFocusListener, { passive: true });
|
|
124
|
+
}
|
|
125
|
+
recordAction(_type, _event) {
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
this.lastActionTime = now;
|
|
128
|
+
// Check if action occurred while document was hidden
|
|
129
|
+
if (document.hidden) {
|
|
130
|
+
this.actionsWhileHidden++;
|
|
131
|
+
this.events.push({
|
|
132
|
+
type: 'action_while_hidden',
|
|
133
|
+
timestamp: now,
|
|
134
|
+
wasHidden: true,
|
|
135
|
+
});
|
|
136
|
+
// Notify detector - this is a strong bot signal
|
|
137
|
+
this.notifyEvent(1.0);
|
|
138
|
+
}
|
|
139
|
+
// Track resume delay after visibility change
|
|
140
|
+
if (this.lastVisibilityChange && !this.lastVisibilityChange.hidden) {
|
|
141
|
+
const resumeDelay = now - this.lastVisibilityChange.timestamp;
|
|
142
|
+
// Only count if this is the first action after becoming visible
|
|
143
|
+
// and the delay is reasonable (< 10 seconds)
|
|
144
|
+
if (resumeDelay > 0 && resumeDelay < 10000 && this.resumeDelays.length < 20) {
|
|
145
|
+
// Check if we haven't already recorded a delay for this visibility change
|
|
146
|
+
const lastRecordedDelay = this.resumeDelays[this.resumeDelays.length - 1];
|
|
147
|
+
if (lastRecordedDelay === undefined || Math.abs(resumeDelay - lastRecordedDelay) > 100) {
|
|
148
|
+
this.resumeDelays.push(resumeDelay);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
stop() {
|
|
154
|
+
if (!this.isActive)
|
|
155
|
+
return;
|
|
156
|
+
this.isActive = false;
|
|
157
|
+
if (this.visibilityListener) {
|
|
158
|
+
document.removeEventListener('visibilitychange', this.visibilityListener);
|
|
159
|
+
this.visibilityListener = null;
|
|
160
|
+
}
|
|
161
|
+
if (this.focusListener) {
|
|
162
|
+
window.removeEventListener('focus', this.focusListener);
|
|
163
|
+
this.focusListener = null;
|
|
164
|
+
}
|
|
165
|
+
if (this.blurListener) {
|
|
166
|
+
window.removeEventListener('blur', this.blurListener);
|
|
167
|
+
this.blurListener = null;
|
|
168
|
+
}
|
|
169
|
+
if (this.clickListener) {
|
|
170
|
+
document.removeEventListener('click', this.clickListener);
|
|
171
|
+
this.clickListener = null;
|
|
172
|
+
}
|
|
173
|
+
if (this.keydownListener) {
|
|
174
|
+
document.removeEventListener('keydown', this.keydownListener);
|
|
175
|
+
this.keydownListener = null;
|
|
176
|
+
}
|
|
177
|
+
if (this.inputFocusListener) {
|
|
178
|
+
document.removeEventListener('focusin', this.inputFocusListener);
|
|
179
|
+
this.inputFocusListener = null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
reset() {
|
|
183
|
+
this.events = [];
|
|
184
|
+
this.focusTypingPairs = [];
|
|
185
|
+
this.actionsWhileHidden = 0;
|
|
186
|
+
this.lastVisibilityChange = null;
|
|
187
|
+
this.resumeDelays = [];
|
|
188
|
+
this.lastFocusedInput = null;
|
|
189
|
+
this.hasTypedInFocusedInput = false;
|
|
190
|
+
this.lastActionTime = 0;
|
|
191
|
+
this.preHideActionTime = 0;
|
|
192
|
+
}
|
|
193
|
+
score() {
|
|
194
|
+
// Need some visibility events to score
|
|
195
|
+
if (this.events.length < 2)
|
|
196
|
+
return undefined;
|
|
197
|
+
let score = 0;
|
|
198
|
+
let factors = 0;
|
|
199
|
+
// 1. Actions while hidden (strongest signal)
|
|
200
|
+
const hiddenActionScore = this.scoreHiddenActions();
|
|
201
|
+
if (hiddenActionScore !== undefined) {
|
|
202
|
+
score += hiddenActionScore;
|
|
203
|
+
factors++;
|
|
204
|
+
}
|
|
205
|
+
// 2. Resume delay analysis
|
|
206
|
+
const resumeScore = this.scoreResumeDelays();
|
|
207
|
+
if (resumeScore !== undefined) {
|
|
208
|
+
score += resumeScore;
|
|
209
|
+
factors++;
|
|
210
|
+
}
|
|
211
|
+
// 3. Focus-typing pattern
|
|
212
|
+
const focusTypingScore = this.scoreFocusTyping();
|
|
213
|
+
if (focusTypingScore !== undefined) {
|
|
214
|
+
score += focusTypingScore;
|
|
215
|
+
factors++;
|
|
216
|
+
}
|
|
217
|
+
return factors > 0 ? score / factors : undefined;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Score based on actions while document was hidden
|
|
221
|
+
* Humans can't interact with hidden tabs; bots can
|
|
222
|
+
*/
|
|
223
|
+
scoreHiddenActions() {
|
|
224
|
+
if (this.actionsWhileHidden === 0)
|
|
225
|
+
return 1.0; // Perfect
|
|
226
|
+
// Any action while hidden is highly suspicious
|
|
227
|
+
if (this.actionsWhileHidden >= 5)
|
|
228
|
+
return 0.1;
|
|
229
|
+
if (this.actionsWhileHidden >= 3)
|
|
230
|
+
return 0.2;
|
|
231
|
+
if (this.actionsWhileHidden >= 1)
|
|
232
|
+
return 0.3;
|
|
233
|
+
return 1.0;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Score based on how quickly activity resumes after tab becomes visible
|
|
237
|
+
* Humans need time to refocus (100-500ms minimum)
|
|
238
|
+
* Bots often resume instantly (< 50ms)
|
|
239
|
+
*/
|
|
240
|
+
scoreResumeDelays() {
|
|
241
|
+
if (this.resumeDelays.length < 2)
|
|
242
|
+
return undefined;
|
|
243
|
+
// Count suspiciously fast resumes (< 50ms)
|
|
244
|
+
const fastResumeCount = this.resumeDelays.filter(d => d < 50).length;
|
|
245
|
+
const fastResumeRatio = fastResumeCount / this.resumeDelays.length;
|
|
246
|
+
if (fastResumeRatio >= 0.8)
|
|
247
|
+
return 0.1;
|
|
248
|
+
if (fastResumeRatio >= 0.5)
|
|
249
|
+
return 0.3;
|
|
250
|
+
if (fastResumeRatio >= 0.3)
|
|
251
|
+
return 0.5;
|
|
252
|
+
if (fastResumeRatio > 0)
|
|
253
|
+
return 0.7;
|
|
254
|
+
return 1.0;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Score based on focus-to-keypress timing
|
|
258
|
+
* Humans focus inputs before typing (natural delay 100-500ms)
|
|
259
|
+
* Bots often type without focusing or with instant delay
|
|
260
|
+
*/
|
|
261
|
+
scoreFocusTyping() {
|
|
262
|
+
if (this.focusTypingPairs.length < 2)
|
|
263
|
+
return undefined;
|
|
264
|
+
let suspiciousCount = 0;
|
|
265
|
+
for (const pair of this.focusTypingPairs) {
|
|
266
|
+
// Instant typing after focus (< 20ms) is suspicious
|
|
267
|
+
if (pair.delay < 20) {
|
|
268
|
+
suspiciousCount++;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const suspiciousRatio = suspiciousCount / this.focusTypingPairs.length;
|
|
272
|
+
if (suspiciousRatio >= 0.8)
|
|
273
|
+
return 0.2;
|
|
274
|
+
if (suspiciousRatio >= 0.5)
|
|
275
|
+
return 0.4;
|
|
276
|
+
if (suspiciousRatio >= 0.3)
|
|
277
|
+
return 0.6;
|
|
278
|
+
if (suspiciousRatio > 0)
|
|
279
|
+
return 0.8;
|
|
280
|
+
return 1.0;
|
|
281
|
+
}
|
|
282
|
+
getDebugInfo() {
|
|
283
|
+
return {
|
|
284
|
+
eventCount: this.events.length,
|
|
285
|
+
actionsWhileHidden: this.actionsWhileHidden,
|
|
286
|
+
visibilityChanges: this.events.filter(e => e.type === 'visibility_change').length,
|
|
287
|
+
focusChanges: this.events.filter(e => e.type === 'focus_change').length,
|
|
288
|
+
resumeDelays: this.resumeDelays,
|
|
289
|
+
focusTypingPairs: this.focusTypingPairs.map(p => ({
|
|
290
|
+
delay: p.delay,
|
|
291
|
+
element: p.element,
|
|
292
|
+
})),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -33,6 +33,6 @@
|
|
|
33
33
|
*/
|
|
34
34
|
export { BehaviorDetector } from './behavior-detector.js';
|
|
35
35
|
export type { DetectionStrategy, StrategyConfig } from './strategy.js';
|
|
36
|
-
export { Mouse, Scroll, Click, Tap, Keyboard, Environment, Resize, MouseStrategy, ScrollStrategy, ClickStrategy, TapStrategy, KeyboardStrategy, EnvironmentStrategy, ResizeStrategy, } from './strategies/index.js';
|
|
36
|
+
export { Mouse, Scroll, Click, Tap, Keyboard, Environment, Resize, Timing, Visibility, MouseStrategy, ScrollStrategy, ClickStrategy, TapStrategy, KeyboardStrategy, EnvironmentStrategy, ResizeStrategy, TimingStrategy, VisibilityStrategy, } from './strategies/index.js';
|
|
37
37
|
export type { BehaviorSettings, ScoreOptions, ScoreResult, ScoreBreakdown, TrackedEvent, EventType, ScoringFunction, } from './types.js';
|
|
38
38
|
export { DEFAULT_SETTINGS } from './types.js';
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Click Behavior Detection Strategy
|
|
3
3
|
* Monitors specific elements for click positioning patterns
|
|
4
|
+
*
|
|
5
|
+
* Enhanced detection:
|
|
6
|
+
* - Click-mouse position delta (bots using .click() have mismatched positions)
|
|
7
|
+
* - Pre-click mouse trajectory analysis
|
|
8
|
+
* - Time between last mousemove and click
|
|
4
9
|
*/
|
|
5
10
|
import { BaseStrategy } from '../strategy.js';
|
|
6
11
|
export declare class ClickStrategy extends BaseStrategy {
|
|
@@ -35,5 +40,8 @@ export declare class ClickStrategy extends BaseStrategy {
|
|
|
35
40
|
};
|
|
36
41
|
inViewport: number;
|
|
37
42
|
trackedElements: number;
|
|
43
|
+
mouseClickDeltas: number[];
|
|
44
|
+
timeSinceMouseMoves: number[];
|
|
45
|
+
untrustedClicks: number;
|
|
38
46
|
};
|
|
39
47
|
}
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Environment Fingerprinting Strategy
|
|
3
3
|
* Captures browser environment on initialization and tick updates
|
|
4
|
+
* Includes critical automation/bot detection signals
|
|
4
5
|
*/
|
|
5
6
|
import { BaseStrategy } from '../strategy.js';
|
|
7
|
+
interface AutomationIndicators {
|
|
8
|
+
/** navigator.webdriver === true (most reliable indicator) */
|
|
9
|
+
isWebdriver: boolean;
|
|
10
|
+
/** HeadlessChrome or Headless in user agent */
|
|
11
|
+
hasHeadlessUA: boolean;
|
|
12
|
+
/** window.chrome exists but window.chrome.runtime is undefined */
|
|
13
|
+
hasChromelessRuntime: boolean;
|
|
14
|
+
/** WebGL renderer is SwiftShader or software renderer */
|
|
15
|
+
hasSoftwareRenderer: boolean;
|
|
16
|
+
/** No plugins on desktop browser (suspicious) */
|
|
17
|
+
hasNoPlugins: boolean;
|
|
18
|
+
/** Automation framework globals detected */
|
|
19
|
+
hasAutomationGlobals: boolean;
|
|
20
|
+
/** List of detected automation globals */
|
|
21
|
+
detectedGlobals: string[];
|
|
22
|
+
/** CDP (Chrome DevTools Protocol) injection detected */
|
|
23
|
+
hasCDPInjection: boolean;
|
|
24
|
+
/** Detected CDP injection keys */
|
|
25
|
+
detectedCDPKeys: string[];
|
|
26
|
+
}
|
|
6
27
|
interface EnvironmentData {
|
|
7
28
|
screenWidth: number;
|
|
8
29
|
screenHeight: number;
|
|
@@ -29,6 +50,10 @@ interface EnvironmentData {
|
|
|
29
50
|
featureInconsistency: boolean;
|
|
30
51
|
isMobile: boolean;
|
|
31
52
|
timestamp: number;
|
|
53
|
+
/** Automation/bot detection indicators */
|
|
54
|
+
automation: AutomationIndicators;
|
|
55
|
+
/** WebGL renderer string (for debugging) */
|
|
56
|
+
webglRenderer?: string;
|
|
32
57
|
}
|
|
33
58
|
export declare class EnvironmentStrategy extends BaseStrategy {
|
|
34
59
|
readonly name = "environment";
|
|
@@ -40,6 +65,10 @@ export declare class EnvironmentStrategy extends BaseStrategy {
|
|
|
40
65
|
onTick(_timestamp: number): void;
|
|
41
66
|
score(): number | undefined;
|
|
42
67
|
private isMobileDevice;
|
|
68
|
+
/**
|
|
69
|
+
* Detect automation indicators
|
|
70
|
+
*/
|
|
71
|
+
private detectAutomation;
|
|
43
72
|
private captureEnvironment;
|
|
44
73
|
getDebugInfo(): EnvironmentData | null;
|
|
45
74
|
/**
|
|
@@ -48,5 +77,15 @@ export declare class EnvironmentStrategy extends BaseStrategy {
|
|
|
48
77
|
* Returns null if environment hasn't been captured yet
|
|
49
78
|
*/
|
|
50
79
|
isMobile(): boolean | null;
|
|
80
|
+
/**
|
|
81
|
+
* Get automation detection results
|
|
82
|
+
* Returns null if environment hasn't been captured yet
|
|
83
|
+
*/
|
|
84
|
+
getAutomationIndicators(): AutomationIndicators | null;
|
|
85
|
+
/**
|
|
86
|
+
* Quick check if any strong automation indicator is present
|
|
87
|
+
* Returns true if likely a bot, false if likely human, null if not checked yet
|
|
88
|
+
*/
|
|
89
|
+
isLikelyBot(): boolean | null;
|
|
51
90
|
}
|
|
52
91
|
export {};
|
|
@@ -9,6 +9,8 @@ export { TapStrategy } from './tap.js';
|
|
|
9
9
|
export { KeyboardStrategy } from './keyboard.js';
|
|
10
10
|
export { EnvironmentStrategy } from './environment.js';
|
|
11
11
|
export { ResizeStrategy } from './resize.js';
|
|
12
|
+
export { TimingStrategy } from './timing.js';
|
|
13
|
+
export { VisibilityStrategy } from './visibility.js';
|
|
12
14
|
export { MouseStrategy as Mouse } from './mouse.js';
|
|
13
15
|
export { ScrollStrategy as Scroll } from './scroll.js';
|
|
14
16
|
export { ClickStrategy as Click } from './click.js';
|
|
@@ -16,3 +18,5 @@ export { TapStrategy as Tap } from './tap.js';
|
|
|
16
18
|
export { KeyboardStrategy as Keyboard } from './keyboard.js';
|
|
17
19
|
export { EnvironmentStrategy as Environment } from './environment.js';
|
|
18
20
|
export { ResizeStrategy as Resize } from './resize.js';
|
|
21
|
+
export { TimingStrategy as Timing } from './timing.js';
|
|
22
|
+
export { VisibilityStrategy as Visibility } from './visibility.js';
|
|
@@ -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 {};
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visibility Strategy
|
|
3
|
+
* Detects bot-like behavior based on tab visibility and focus patterns
|
|
4
|
+
*
|
|
5
|
+
* Key detection signals:
|
|
6
|
+
* - Actions while document.hidden === true (bots ignore visibility)
|
|
7
|
+
* - No pause when tab loses focus
|
|
8
|
+
* - Activity resumes too quickly after tab regains focus
|
|
9
|
+
* - Focus patterns: humans focus inputs before typing
|
|
10
|
+
*/
|
|
11
|
+
import { BaseStrategy } from '../strategy.js';
|
|
12
|
+
export declare class VisibilityStrategy extends BaseStrategy {
|
|
13
|
+
readonly name = "visibility";
|
|
14
|
+
readonly defaultWeight = 0.1;
|
|
15
|
+
private events;
|
|
16
|
+
private focusTypingPairs;
|
|
17
|
+
private actionsWhileHidden;
|
|
18
|
+
private lastVisibilityChange;
|
|
19
|
+
private resumeDelays;
|
|
20
|
+
private isActive;
|
|
21
|
+
private visibilityListener;
|
|
22
|
+
private focusListener;
|
|
23
|
+
private blurListener;
|
|
24
|
+
private clickListener;
|
|
25
|
+
private keydownListener;
|
|
26
|
+
private inputFocusListener;
|
|
27
|
+
private lastFocusedInput;
|
|
28
|
+
private hasTypedInFocusedInput;
|
|
29
|
+
private lastActionTime;
|
|
30
|
+
private preHideActionTime;
|
|
31
|
+
start(): void;
|
|
32
|
+
private recordAction;
|
|
33
|
+
stop(): void;
|
|
34
|
+
reset(): void;
|
|
35
|
+
score(): number | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Score based on actions while document was hidden
|
|
38
|
+
* Humans can't interact with hidden tabs; bots can
|
|
39
|
+
*/
|
|
40
|
+
private scoreHiddenActions;
|
|
41
|
+
/**
|
|
42
|
+
* Score based on how quickly activity resumes after tab becomes visible
|
|
43
|
+
* Humans need time to refocus (100-500ms minimum)
|
|
44
|
+
* Bots often resume instantly (< 50ms)
|
|
45
|
+
*/
|
|
46
|
+
private scoreResumeDelays;
|
|
47
|
+
/**
|
|
48
|
+
* Score based on focus-to-keypress timing
|
|
49
|
+
* Humans focus inputs before typing (natural delay 100-500ms)
|
|
50
|
+
* Bots often type without focusing or with instant delay
|
|
51
|
+
*/
|
|
52
|
+
private scoreFocusTyping;
|
|
53
|
+
getDebugInfo(): {
|
|
54
|
+
eventCount: number;
|
|
55
|
+
actionsWhileHidden: number;
|
|
56
|
+
visibilityChanges: number;
|
|
57
|
+
focusChanges: number;
|
|
58
|
+
resumeDelays: number[];
|
|
59
|
+
focusTypingPairs: {
|
|
60
|
+
delay: number;
|
|
61
|
+
element: string;
|
|
62
|
+
}[];
|
|
63
|
+
};
|
|
64
|
+
}
|