@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.
@@ -0,0 +1,308 @@
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
+ import { calculateStatistics } from '../math-utils.js';
13
+ export class TimingStrategy extends BaseStrategy {
14
+ constructor() {
15
+ super(...arguments);
16
+ this.name = 'timing';
17
+ this.defaultWeight = 0.15;
18
+ this.actions = [];
19
+ this.mouseStillness = {
20
+ position: null,
21
+ lastMoveTime: 0,
22
+ microMovementCount: 0,
23
+ };
24
+ this.microMovements = [];
25
+ this.isActive = false;
26
+ this.mouseListener = null;
27
+ this.clickListener = null;
28
+ this.keydownListener = null;
29
+ this.scrollListener = null;
30
+ /** Time window to consider for pre-action stillness (ms) */
31
+ this.STILLNESS_WINDOW = 500;
32
+ /** Minimum distance to count as meaningful movement (px) */
33
+ this.MICRO_MOVEMENT_THRESHOLD = 1;
34
+ /** Maximum micro-movement to count (jitter vs intentional) (px) */
35
+ this.MAX_MICRO_MOVEMENT = 5;
36
+ /** Machine-precision threshold (intervals within this are suspicious) */
37
+ this.MACHINE_PRECISION_THRESHOLD = 5; // 5ms
38
+ }
39
+ start() {
40
+ if (this.isActive)
41
+ return;
42
+ this.isActive = true;
43
+ // Track mouse movement for stillness detection
44
+ this.mouseListener = (e) => {
45
+ const mouseEvent = e;
46
+ const now = Date.now();
47
+ const currentPos = { x: mouseEvent.clientX, y: mouseEvent.clientY };
48
+ if (this.mouseStillness.position) {
49
+ const dx = currentPos.x - this.mouseStillness.position.x;
50
+ const dy = currentPos.y - this.mouseStillness.position.y;
51
+ const distance = Math.sqrt(dx * dx + dy * dy);
52
+ // Track micro-movements (1-5px range)
53
+ if (distance >= this.MICRO_MOVEMENT_THRESHOLD && distance <= this.MAX_MICRO_MOVEMENT) {
54
+ this.microMovements.push({ timestamp: now, distance });
55
+ }
56
+ // Any meaningful movement updates the last move time
57
+ if (distance >= this.MICRO_MOVEMENT_THRESHOLD) {
58
+ this.mouseStillness.lastMoveTime = now;
59
+ }
60
+ }
61
+ this.mouseStillness.position = currentPos;
62
+ // Clean up old micro-movements (keep only last STILLNESS_WINDOW)
63
+ const cutoff = now - this.STILLNESS_WINDOW;
64
+ this.microMovements = this.microMovements.filter(m => m.timestamp >= cutoff);
65
+ };
66
+ // Track clicks with timing context
67
+ this.clickListener = (e) => {
68
+ this.recordAction('click', e);
69
+ };
70
+ // Track keydown with timing context
71
+ this.keydownListener = () => {
72
+ this.recordAction('keydown');
73
+ };
74
+ // Track scroll with timing context
75
+ this.scrollListener = () => {
76
+ this.recordAction('scroll');
77
+ };
78
+ document.addEventListener('mousemove', this.mouseListener, { passive: true });
79
+ document.addEventListener('click', this.clickListener, { passive: true });
80
+ document.addEventListener('keydown', this.keydownListener, { passive: true });
81
+ document.addEventListener('scroll', this.scrollListener, { passive: true });
82
+ }
83
+ recordAction(type, _event) {
84
+ const now = Date.now();
85
+ // Calculate time since last mouse movement
86
+ const timeSinceMouseMove = this.mouseStillness.lastMoveTime > 0
87
+ ? now - this.mouseStillness.lastMoveTime
88
+ : -1;
89
+ // Count micro-movements in stillness window (already filtered in mouse listener)
90
+ this.mouseStillness.microMovementCount = this.microMovements.length;
91
+ this.actions.push({
92
+ type,
93
+ timestamp: now,
94
+ timeSinceMouseMove,
95
+ wasHidden: document.hidden,
96
+ });
97
+ // Notify detector
98
+ this.notifyEvent(0.7);
99
+ // Keep only last 100 actions for memory efficiency (shift is O(n) but rare)
100
+ if (this.actions.length > 100) {
101
+ this.actions.shift();
102
+ }
103
+ }
104
+ stop() {
105
+ if (!this.isActive)
106
+ return;
107
+ this.isActive = false;
108
+ if (this.mouseListener) {
109
+ document.removeEventListener('mousemove', this.mouseListener);
110
+ this.mouseListener = null;
111
+ }
112
+ if (this.clickListener) {
113
+ document.removeEventListener('click', this.clickListener);
114
+ this.clickListener = null;
115
+ }
116
+ if (this.keydownListener) {
117
+ document.removeEventListener('keydown', this.keydownListener);
118
+ this.keydownListener = null;
119
+ }
120
+ if (this.scrollListener) {
121
+ document.removeEventListener('scroll', this.scrollListener);
122
+ this.scrollListener = null;
123
+ }
124
+ }
125
+ reset() {
126
+ this.actions = [];
127
+ this.microMovements = [];
128
+ this.mouseStillness = {
129
+ position: null,
130
+ lastMoveTime: 0,
131
+ microMovementCount: 0,
132
+ };
133
+ }
134
+ score() {
135
+ const actionCount = this.actions.length;
136
+ if (actionCount < 3)
137
+ return undefined;
138
+ let score = 0;
139
+ let factors = 0;
140
+ // Cache filtered arrays to avoid repeated iteration
141
+ const clicks = [];
142
+ const clicksWithMouse = [];
143
+ let hiddenCount = 0;
144
+ for (const action of this.actions) {
145
+ if (action.type === 'click') {
146
+ clicks.push(action);
147
+ if (action.timeSinceMouseMove >= 0) {
148
+ clicksWithMouse.push(action);
149
+ }
150
+ }
151
+ if (action.wasHidden)
152
+ hiddenCount++;
153
+ }
154
+ // 1. Pre-action stillness detection
155
+ if (clicks.length >= 2) {
156
+ const stillnessScore = this.scorePreActionStillness(clicks);
157
+ if (stillnessScore !== undefined) {
158
+ score += stillnessScore;
159
+ factors++;
160
+ }
161
+ }
162
+ // 2. Mouse-to-action delay analysis
163
+ if (clicksWithMouse.length >= 3) {
164
+ const mouseToClickScore = this.scoreMouseToClickDelayFromCache(clicksWithMouse);
165
+ if (mouseToClickScore !== undefined) {
166
+ score += mouseToClickScore;
167
+ factors++;
168
+ }
169
+ }
170
+ // 3. Action interval patterns
171
+ if (actionCount >= 5) {
172
+ const intervalScore = this.scoreActionIntervals();
173
+ if (intervalScore !== undefined) {
174
+ score += intervalScore;
175
+ factors++;
176
+ }
177
+ }
178
+ // 4. Hidden document actions
179
+ const hiddenScore = this.scoreHiddenActionsFromCount(hiddenCount, actionCount);
180
+ score += hiddenScore;
181
+ factors++;
182
+ return factors > 0 ? score / factors : undefined;
183
+ }
184
+ /**
185
+ * Score based on pre-action stillness
186
+ * Humans have micro-movements before clicking; bots are perfectly still
187
+ */
188
+ scorePreActionStillness(clicks) {
189
+ // Look at clicks with mouse data
190
+ const clicksWithMouseData = clicks.filter(c => c.timeSinceMouseMove >= 0);
191
+ if (clicksWithMouseData.length < 2)
192
+ return undefined;
193
+ // Count clicks with zero pre-action movement (100-500ms window)
194
+ const perfectlyStillClicks = clicksWithMouseData.filter(c => {
195
+ // Time since mouse move in the suspicious range (100-500ms of perfect stillness)
196
+ return c.timeSinceMouseMove >= 100 && c.timeSinceMouseMove <= 500 &&
197
+ this.mouseStillness.microMovementCount === 0;
198
+ }).length;
199
+ const stillRatio = perfectlyStillClicks / clicksWithMouseData.length;
200
+ // High ratio of perfectly still clicks = likely bot
201
+ // 0% still = very human (1.0)
202
+ // 50%+ still = suspicious (0.3)
203
+ // 100% still = very bot-like (0.1)
204
+ if (stillRatio >= 0.9)
205
+ return 0.1;
206
+ if (stillRatio >= 0.7)
207
+ return 0.3;
208
+ if (stillRatio >= 0.5)
209
+ return 0.5;
210
+ if (stillRatio >= 0.3)
211
+ return 0.7;
212
+ return 1.0;
213
+ }
214
+ /**
215
+ * Score mouse-to-click delay patterns (using pre-filtered cache)
216
+ */
217
+ scoreMouseToClickDelayFromCache(clicksWithMouse) {
218
+ const delays = clicksWithMouse.map(c => c.timeSinceMouseMove);
219
+ const stats = calculateStatistics(delays);
220
+ // Suspiciously fast clicks (< 10ms average)
221
+ if (stats.mean < 10)
222
+ return 0.1;
223
+ // Suspiciously consistent delays (CV < 0.2)
224
+ if (stats.cv < 0.2 && clicksWithMouse.length >= 5)
225
+ return 0.2;
226
+ // Normal human range (50-300ms with variation)
227
+ if (stats.mean >= 50 && stats.mean <= 300 && stats.cv >= 0.3)
228
+ return 1.0;
229
+ // Somewhat suspicious
230
+ if (stats.cv < 0.4)
231
+ return 0.6;
232
+ return 0.8;
233
+ }
234
+ /**
235
+ * Score action intervals for machine-like precision
236
+ * Detects exact intervals: 100ms, 500ms, 1000ms
237
+ */
238
+ scoreActionIntervals() {
239
+ if (this.actions.length < 5)
240
+ return undefined;
241
+ const intervals = [];
242
+ for (let i = 1; i < this.actions.length; i++) {
243
+ intervals.push(this.actions[i].timestamp - this.actions[i - 1].timestamp);
244
+ }
245
+ // Check for machine-precise intervals (multiples of common values)
246
+ const machineIntervals = [100, 200, 250, 500, 1000];
247
+ let preciseCount = 0;
248
+ for (const interval of intervals) {
249
+ for (const machineInterval of machineIntervals) {
250
+ // Check if interval is within threshold of machine interval or its multiples
251
+ const remainder = interval % machineInterval;
252
+ if (remainder < this.MACHINE_PRECISION_THRESHOLD ||
253
+ (machineInterval - remainder) < this.MACHINE_PRECISION_THRESHOLD) {
254
+ preciseCount++;
255
+ break;
256
+ }
257
+ }
258
+ }
259
+ const preciseRatio = preciseCount / intervals.length;
260
+ // High ratio of machine-precise intervals = bot
261
+ if (preciseRatio >= 0.8)
262
+ return 0.1;
263
+ if (preciseRatio >= 0.6)
264
+ return 0.3;
265
+ if (preciseRatio >= 0.4)
266
+ return 0.5;
267
+ // Also check coefficient of variation (should be high for humans)
268
+ const stats = calculateStatistics(intervals);
269
+ if (stats.cv < 0.15)
270
+ return 0.2; // Too consistent
271
+ return 1.0;
272
+ }
273
+ /**
274
+ * Score based on actions while document was hidden (using pre-computed count)
275
+ */
276
+ scoreHiddenActionsFromCount(hiddenCount, totalCount) {
277
+ if (hiddenCount === 0)
278
+ return 1.0; // Perfect - no hidden actions
279
+ const hiddenRatio = hiddenCount / totalCount;
280
+ // Any hidden actions are suspicious
281
+ if (hiddenRatio > 0.5)
282
+ return 0.1; // Major bot indicator
283
+ if (hiddenRatio > 0.2)
284
+ return 0.3;
285
+ if (hiddenRatio > 0)
286
+ return 0.5;
287
+ return 1.0;
288
+ }
289
+ getDebugInfo() {
290
+ const clicks = this.actions.filter(a => a.type === 'click');
291
+ const intervals = [];
292
+ for (let i = 1; i < this.actions.length; i++) {
293
+ intervals.push(this.actions[i].timestamp - this.actions[i - 1].timestamp);
294
+ }
295
+ return {
296
+ actionCount: this.actions.length,
297
+ clickCount: clicks.length,
298
+ keydownCount: this.actions.filter(a => a.type === 'keydown').length,
299
+ scrollCount: this.actions.filter(a => a.type === 'scroll').length,
300
+ hiddenActionCount: this.actions.filter(a => a.wasHidden).length,
301
+ microMovementCount: this.microMovements.length,
302
+ intervals: intervals.slice(-20), // Last 20 intervals
303
+ lastStillnessDuration: this.mouseStillness.lastMoveTime > 0
304
+ ? Date.now() - this.mouseStillness.lastMoveTime
305
+ : null,
306
+ };
307
+ }
308
+ }
@@ -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
  }