@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,210 @@
|
|
|
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
|
+
import { analyzeIntervals, scoreCoefficientOfVariation, gaussian } from '../math-utils';
|
|
8
|
+
export class TapStrategy extends BaseStrategy {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
super();
|
|
11
|
+
this.name = 'tap';
|
|
12
|
+
this.defaultWeight = 0.35;
|
|
13
|
+
this.events = [];
|
|
14
|
+
this.targetSelectors = ['button', 'a', 'input[type="submit"]', '[role="button"]'];
|
|
15
|
+
this.touchStartListeners = new Map();
|
|
16
|
+
this.touchEndListeners = new Map();
|
|
17
|
+
this.activeTouches = new Map();
|
|
18
|
+
this.isActive = false;
|
|
19
|
+
if (options === null || options === void 0 ? void 0 : options.targetSelectors) {
|
|
20
|
+
this.targetSelectors = options.targetSelectors;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
start() {
|
|
24
|
+
if (this.isActive)
|
|
25
|
+
return;
|
|
26
|
+
this.isActive = true;
|
|
27
|
+
this.attachTouchListeners();
|
|
28
|
+
}
|
|
29
|
+
addTarget(selector) {
|
|
30
|
+
if (!this.targetSelectors.includes(selector)) {
|
|
31
|
+
this.targetSelectors.push(selector);
|
|
32
|
+
if (this.isActive) {
|
|
33
|
+
this.attachTouchListenersForSelector(selector);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
attachTouchListeners() {
|
|
38
|
+
this.targetSelectors.forEach(selector => {
|
|
39
|
+
this.attachTouchListenersForSelector(selector);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
attachTouchListenersForSelector(selector) {
|
|
43
|
+
const elements = document.querySelectorAll(selector);
|
|
44
|
+
elements.forEach(element => {
|
|
45
|
+
if (this.touchStartListeners.has(element))
|
|
46
|
+
return;
|
|
47
|
+
const startListener = (e) => {
|
|
48
|
+
const touchEvent = e;
|
|
49
|
+
// Only track first touch (ignore multi-touch)
|
|
50
|
+
if (touchEvent.touches.length !== 1)
|
|
51
|
+
return;
|
|
52
|
+
const touch = touchEvent.touches[0];
|
|
53
|
+
this.activeTouches.set(touch.identifier, {
|
|
54
|
+
x: touch.clientX,
|
|
55
|
+
y: touch.clientY,
|
|
56
|
+
timestamp: Date.now(),
|
|
57
|
+
element,
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
const endListener = (e) => {
|
|
61
|
+
const touchEvent = e;
|
|
62
|
+
if (touchEvent.changedTouches.length !== 1)
|
|
63
|
+
return;
|
|
64
|
+
const touch = touchEvent.changedTouches[0];
|
|
65
|
+
const startData = this.activeTouches.get(touch.identifier);
|
|
66
|
+
if (!startData || startData.element !== element)
|
|
67
|
+
return;
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
const duration = now - startData.timestamp;
|
|
70
|
+
const dx = touch.clientX - startData.x;
|
|
71
|
+
const dy = touch.clientY - startData.y;
|
|
72
|
+
const movement = Math.sqrt(dx * dx + dy * dy);
|
|
73
|
+
const rect = element.getBoundingClientRect();
|
|
74
|
+
const inViewport = (rect.top >= 0 &&
|
|
75
|
+
rect.left >= 0 &&
|
|
76
|
+
rect.bottom <= window.innerHeight &&
|
|
77
|
+
rect.right <= window.innerWidth);
|
|
78
|
+
// Analyze tap position
|
|
79
|
+
const tx = touch.clientX;
|
|
80
|
+
const ty = touch.clientY;
|
|
81
|
+
const overElement = (tx >= rect.left &&
|
|
82
|
+
tx <= rect.right &&
|
|
83
|
+
ty >= rect.top &&
|
|
84
|
+
ty <= rect.bottom);
|
|
85
|
+
let position;
|
|
86
|
+
if (!overElement) {
|
|
87
|
+
position = 'outside';
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const centerX = rect.left + rect.width / 2;
|
|
91
|
+
const centerY = rect.top + rect.height / 2;
|
|
92
|
+
const distanceFromCenter = Math.sqrt((tx - centerX) ** 2 + (ty - centerY) ** 2);
|
|
93
|
+
// Dead center = within 2px
|
|
94
|
+
position = distanceFromCenter < 2 ? 'dead-center' : 'over-element';
|
|
95
|
+
}
|
|
96
|
+
this.events.push({
|
|
97
|
+
x: tx,
|
|
98
|
+
y: ty,
|
|
99
|
+
rect,
|
|
100
|
+
inViewport,
|
|
101
|
+
position,
|
|
102
|
+
duration,
|
|
103
|
+
movement,
|
|
104
|
+
timestamp: now,
|
|
105
|
+
});
|
|
106
|
+
// Notify detector - taps are high-value events
|
|
107
|
+
this.notifyEvent(1.0);
|
|
108
|
+
this.activeTouches.delete(touch.identifier);
|
|
109
|
+
};
|
|
110
|
+
element.addEventListener('touchstart', startListener, { passive: true });
|
|
111
|
+
element.addEventListener('touchend', endListener, { passive: true });
|
|
112
|
+
this.touchStartListeners.set(element, startListener);
|
|
113
|
+
this.touchEndListeners.set(element, endListener);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
stop() {
|
|
117
|
+
if (!this.isActive)
|
|
118
|
+
return;
|
|
119
|
+
this.isActive = false;
|
|
120
|
+
this.touchStartListeners.forEach((listener, element) => {
|
|
121
|
+
element.removeEventListener('touchstart', listener);
|
|
122
|
+
});
|
|
123
|
+
this.touchStartListeners.clear();
|
|
124
|
+
this.touchEndListeners.forEach((listener, element) => {
|
|
125
|
+
element.removeEventListener('touchend', listener);
|
|
126
|
+
});
|
|
127
|
+
this.touchEndListeners.clear();
|
|
128
|
+
this.activeTouches.clear();
|
|
129
|
+
}
|
|
130
|
+
reset() {
|
|
131
|
+
this.events = [];
|
|
132
|
+
this.activeTouches.clear();
|
|
133
|
+
}
|
|
134
|
+
score() {
|
|
135
|
+
if (this.events.length === 0)
|
|
136
|
+
return undefined;
|
|
137
|
+
let score = 0;
|
|
138
|
+
let factors = 0;
|
|
139
|
+
// 1. Position accuracy
|
|
140
|
+
let positionScore = 0;
|
|
141
|
+
for (const tap of this.events) {
|
|
142
|
+
switch (tap.position) {
|
|
143
|
+
case 'outside':
|
|
144
|
+
positionScore += 0.0; // Bot
|
|
145
|
+
break;
|
|
146
|
+
case 'dead-center':
|
|
147
|
+
positionScore += 0.5; // Suspicious
|
|
148
|
+
break;
|
|
149
|
+
case 'over-element':
|
|
150
|
+
positionScore += 1.0; // Human
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
score += positionScore / this.events.length;
|
|
155
|
+
factors++;
|
|
156
|
+
// 2. Tap duration variation - Humans have natural variation (50-150ms typical)
|
|
157
|
+
const durations = this.events.map(e => e.duration);
|
|
158
|
+
if (durations.length >= 2) {
|
|
159
|
+
const durationAnalysis = analyzeIntervals(durations);
|
|
160
|
+
if (durationAnalysis) {
|
|
161
|
+
const { statistics, allIdentical } = durationAnalysis;
|
|
162
|
+
// All identical = bot
|
|
163
|
+
if (allIdentical) {
|
|
164
|
+
return 0.05;
|
|
165
|
+
}
|
|
166
|
+
// Human tap durations have moderate CV (~0.3-0.5)
|
|
167
|
+
score += scoreCoefficientOfVariation(statistics.cv);
|
|
168
|
+
factors++;
|
|
169
|
+
// Ideal duration: 70-120ms
|
|
170
|
+
score += gaussian(statistics.mean, 95, 40);
|
|
171
|
+
factors++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// 3. Tap movement - Real fingers move slightly (1-5px), bots are often 0px
|
|
175
|
+
const movements = this.events.map(e => e.movement);
|
|
176
|
+
if (movements.length > 0) {
|
|
177
|
+
const avgMovement = movements.reduce((a, b) => a + b, 0) / movements.length;
|
|
178
|
+
// Some movement is natural (1-5px), too much is a swipe, zero is suspicious
|
|
179
|
+
score += gaussian(avgMovement, 2, 3);
|
|
180
|
+
factors++;
|
|
181
|
+
}
|
|
182
|
+
// 4. Tap interval timing - Time between taps should vary naturally
|
|
183
|
+
if (this.events.length >= 3) {
|
|
184
|
+
const intervals = [];
|
|
185
|
+
for (let i = 1; i < this.events.length; i++) {
|
|
186
|
+
intervals.push(this.events[i].timestamp - this.events[i - 1].timestamp);
|
|
187
|
+
}
|
|
188
|
+
const intervalAnalysis = analyzeIntervals(intervals);
|
|
189
|
+
if (intervalAnalysis && !intervalAnalysis.allIdentical) {
|
|
190
|
+
score += scoreCoefficientOfVariation(intervalAnalysis.statistics.cv);
|
|
191
|
+
factors++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return factors > 0 ? score / factors : undefined;
|
|
195
|
+
}
|
|
196
|
+
getDebugInfo() {
|
|
197
|
+
return {
|
|
198
|
+
eventCount: this.events.length,
|
|
199
|
+
positions: {
|
|
200
|
+
outside: this.events.filter(e => e.position === 'outside').length,
|
|
201
|
+
deadCenter: this.events.filter(e => e.position === 'dead-center').length,
|
|
202
|
+
overElement: this.events.filter(e => e.position === 'over-element').length,
|
|
203
|
+
},
|
|
204
|
+
durations: this.events.map(e => e.duration),
|
|
205
|
+
movements: this.events.map(e => e.movement),
|
|
206
|
+
inViewport: this.events.filter(e => e.inViewport).length,
|
|
207
|
+
trackedElements: this.touchStartListeners.size,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection Strategy Interface
|
|
3
|
+
* Each strategy is a fully autonomous module responsible for:
|
|
4
|
+
* - Registering its own event listeners
|
|
5
|
+
* - Managing its own state and events
|
|
6
|
+
* - Responding to lifecycle events (start/stop)
|
|
7
|
+
* - Optionally receiving tick updates for polling-based detection
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Time series data point
|
|
11
|
+
* Used by strategies that track values over time
|
|
12
|
+
*/
|
|
13
|
+
export interface TimeSeriesPoint {
|
|
14
|
+
value: number;
|
|
15
|
+
timestamp: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Event notification from strategy to detector
|
|
19
|
+
*/
|
|
20
|
+
export interface StrategyEvent {
|
|
21
|
+
strategy: string;
|
|
22
|
+
weight: number;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Base class for detection strategies
|
|
27
|
+
* Handles event callback pattern to avoid repetition
|
|
28
|
+
*/
|
|
29
|
+
export declare abstract class BaseStrategy implements DetectionStrategy {
|
|
30
|
+
abstract readonly name: string;
|
|
31
|
+
abstract readonly defaultWeight: number;
|
|
32
|
+
protected eventCallback: ((event: StrategyEvent) => void) | null;
|
|
33
|
+
setEventCallback(callback: (event: StrategyEvent) => void): void;
|
|
34
|
+
protected notifyEvent(weight: number): void;
|
|
35
|
+
abstract start(): void;
|
|
36
|
+
abstract stop(): void;
|
|
37
|
+
abstract reset(): void;
|
|
38
|
+
abstract score(): number | undefined;
|
|
39
|
+
onTick?(timestamp: number): void;
|
|
40
|
+
getDebugInfo?(): any;
|
|
41
|
+
}
|
|
42
|
+
export interface DetectionStrategy {
|
|
43
|
+
/**
|
|
44
|
+
* Unique identifier for this strategy
|
|
45
|
+
*/
|
|
46
|
+
readonly name: string;
|
|
47
|
+
/**
|
|
48
|
+
* Default weight for this strategy in overall score calculation
|
|
49
|
+
*/
|
|
50
|
+
readonly defaultWeight: number;
|
|
51
|
+
/**
|
|
52
|
+
* Start detection - register event listeners
|
|
53
|
+
*/
|
|
54
|
+
start(): void;
|
|
55
|
+
/**
|
|
56
|
+
* Stop detection - remove event listeners, cleanup
|
|
57
|
+
*/
|
|
58
|
+
stop(): void;
|
|
59
|
+
/**
|
|
60
|
+
* Reset collected data
|
|
61
|
+
*/
|
|
62
|
+
reset(): void;
|
|
63
|
+
/**
|
|
64
|
+
* Calculate score based on collected data
|
|
65
|
+
* @returns Score 0-1, or undefined if insufficient data
|
|
66
|
+
*/
|
|
67
|
+
score(): number | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Optional: Called on regular interval for polling-based detection
|
|
70
|
+
* Use this to sample document/window/navigator state
|
|
71
|
+
* @param timestamp - Current timestamp
|
|
72
|
+
*/
|
|
73
|
+
onTick?(timestamp: number): void;
|
|
74
|
+
/**
|
|
75
|
+
* Optional: Get debug information about collected data
|
|
76
|
+
*/
|
|
77
|
+
getDebugInfo?(): any;
|
|
78
|
+
/**
|
|
79
|
+
* Optional: Set callback for when events are received
|
|
80
|
+
* Used to track confidence - detector will be notified when strategy receives data
|
|
81
|
+
* @param callback - Function to call when event is received
|
|
82
|
+
*/
|
|
83
|
+
setEventCallback?(callback: (event: StrategyEvent) => void): void;
|
|
84
|
+
}
|
|
85
|
+
export interface StrategyConfig {
|
|
86
|
+
strategy: DetectionStrategy;
|
|
87
|
+
weight: number;
|
|
88
|
+
enabled: boolean;
|
|
89
|
+
}
|
|
90
|
+
export interface TickOptions {
|
|
91
|
+
/**
|
|
92
|
+
* Tick interval in milliseconds
|
|
93
|
+
* Default: 1000 (1 second)
|
|
94
|
+
*/
|
|
95
|
+
interval?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Whether to start ticking immediately
|
|
98
|
+
* Default: false (start with detector.start())
|
|
99
|
+
*/
|
|
100
|
+
autoStart?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Automatically pause detection when the tab is hidden
|
|
103
|
+
* Uses the Page Visibility API to stop strategies and tick when document.hidden is true
|
|
104
|
+
* Default: true (recommended for better performance and battery life)
|
|
105
|
+
*/
|
|
106
|
+
pauseOnHidden?: boolean;
|
|
107
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection Strategy Interface
|
|
3
|
+
* Each strategy is a fully autonomous module responsible for:
|
|
4
|
+
* - Registering its own event listeners
|
|
5
|
+
* - Managing its own state and events
|
|
6
|
+
* - Responding to lifecycle events (start/stop)
|
|
7
|
+
* - Optionally receiving tick updates for polling-based detection
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Base class for detection strategies
|
|
11
|
+
* Handles event callback pattern to avoid repetition
|
|
12
|
+
*/
|
|
13
|
+
export class BaseStrategy {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.eventCallback = null;
|
|
16
|
+
}
|
|
17
|
+
setEventCallback(callback) {
|
|
18
|
+
this.eventCallback = callback;
|
|
19
|
+
}
|
|
20
|
+
notifyEvent(weight) {
|
|
21
|
+
if (this.eventCallback) {
|
|
22
|
+
this.eventCallback({
|
|
23
|
+
strategy: this.name,
|
|
24
|
+
weight,
|
|
25
|
+
timestamp: Date.now()
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
export type EventType = 'tab-visibility' | 'scroll' | 'resize' | 'mouse-move' | 'click' | 'keypress' | 'environment';
|
|
2
|
+
export interface BaseEvent {
|
|
3
|
+
type: EventType;
|
|
4
|
+
timestamp: number;
|
|
5
|
+
}
|
|
6
|
+
export interface TabVisibilityEvent extends BaseEvent {
|
|
7
|
+
type: 'tab-visibility';
|
|
8
|
+
hidden: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ScrollEvent extends BaseEvent {
|
|
11
|
+
type: 'scroll';
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
elementId?: string;
|
|
15
|
+
deltaY?: number;
|
|
16
|
+
deltaTime?: number;
|
|
17
|
+
velocity?: number;
|
|
18
|
+
isProgrammatic?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface ResizeEvent extends BaseEvent {
|
|
21
|
+
type: 'resize';
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
mouseX?: number;
|
|
25
|
+
mouseY?: number;
|
|
26
|
+
mouseNearEdge?: boolean;
|
|
27
|
+
screenWidth: number;
|
|
28
|
+
screenHeight: number;
|
|
29
|
+
screenAvailWidth: number;
|
|
30
|
+
screenAvailHeight: number;
|
|
31
|
+
devicePixelRatio: number;
|
|
32
|
+
isFullscreen?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface MouseMoveEvent extends BaseEvent {
|
|
35
|
+
type: 'mouse-move';
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
velocityX?: number;
|
|
39
|
+
velocityY?: number;
|
|
40
|
+
directionChange?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface ClickEvent extends BaseEvent {
|
|
43
|
+
type: 'click';
|
|
44
|
+
x: number;
|
|
45
|
+
y: number;
|
|
46
|
+
targetX: number;
|
|
47
|
+
targetY: number;
|
|
48
|
+
targetWidth: number;
|
|
49
|
+
targetHeight: number;
|
|
50
|
+
distanceFromTarget: number;
|
|
51
|
+
mousePositionBeforeClick?: {
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
timestamp: number;
|
|
55
|
+
};
|
|
56
|
+
timeSinceLastMouseMove?: number;
|
|
57
|
+
targetInViewport: boolean;
|
|
58
|
+
targetVisibleArea: number;
|
|
59
|
+
}
|
|
60
|
+
export interface KeypressEvent extends BaseEvent {
|
|
61
|
+
type: 'keypress';
|
|
62
|
+
key: string;
|
|
63
|
+
timeSinceLastKey?: number;
|
|
64
|
+
}
|
|
65
|
+
export interface EnvironmentEvent extends BaseEvent {
|
|
66
|
+
type: 'environment';
|
|
67
|
+
screenWidth: number;
|
|
68
|
+
screenHeight: number;
|
|
69
|
+
screenAvailWidth: number;
|
|
70
|
+
screenAvailHeight: number;
|
|
71
|
+
windowWidth: number;
|
|
72
|
+
windowHeight: number;
|
|
73
|
+
devicePixelRatio: number;
|
|
74
|
+
colorDepth: number;
|
|
75
|
+
pixelDepth: number;
|
|
76
|
+
userAgent: string;
|
|
77
|
+
platform: string;
|
|
78
|
+
language: string;
|
|
79
|
+
languages: string[];
|
|
80
|
+
hardwareConcurrency?: number;
|
|
81
|
+
maxTouchPoints: number;
|
|
82
|
+
vendor: string;
|
|
83
|
+
hasWebGL: boolean;
|
|
84
|
+
hasWebRTC: boolean;
|
|
85
|
+
hasNotifications: boolean;
|
|
86
|
+
hasGeolocation: boolean;
|
|
87
|
+
hasIndexedDB: boolean;
|
|
88
|
+
hasLocalStorage: boolean;
|
|
89
|
+
hasSessionStorage: boolean;
|
|
90
|
+
plugins: number;
|
|
91
|
+
mimeTypes: number;
|
|
92
|
+
suspiciousRatio?: boolean;
|
|
93
|
+
suspiciousDimensions?: boolean;
|
|
94
|
+
featureInconsistency?: boolean;
|
|
95
|
+
}
|
|
96
|
+
export type TrackedEvent = TabVisibilityEvent | ScrollEvent | ResizeEvent | MouseMoveEvent | ClickEvent | KeypressEvent | EnvironmentEvent;
|
|
97
|
+
export interface EventStorage {
|
|
98
|
+
'tab-visibility': TabVisibilityEvent[];
|
|
99
|
+
'scroll': ScrollEvent[];
|
|
100
|
+
'resize': ResizeEvent[];
|
|
101
|
+
'mouse-move': MouseMoveEvent[];
|
|
102
|
+
'click': ClickEvent[];
|
|
103
|
+
'keypress': KeypressEvent[];
|
|
104
|
+
'environment': EnvironmentEvent[];
|
|
105
|
+
}
|
|
106
|
+
export interface ScoreBreakdown {
|
|
107
|
+
overall: number;
|
|
108
|
+
factors: {
|
|
109
|
+
mouseMovement?: number;
|
|
110
|
+
clickAccuracy?: number;
|
|
111
|
+
scrollBehavior?: number;
|
|
112
|
+
keyboardTiming?: number;
|
|
113
|
+
tabActivity?: number;
|
|
114
|
+
resizeBehavior?: number;
|
|
115
|
+
environmentFingerprint?: number;
|
|
116
|
+
};
|
|
117
|
+
weights: {
|
|
118
|
+
mouseMovement: number;
|
|
119
|
+
clickAccuracy: number;
|
|
120
|
+
scrollBehavior: number;
|
|
121
|
+
keyboardTiming: number;
|
|
122
|
+
tabActivity: number;
|
|
123
|
+
resizeBehavior: number;
|
|
124
|
+
environmentFingerprint: number;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
export interface ScoreOptions {
|
|
128
|
+
breakdown?: boolean;
|
|
129
|
+
auditTrail?: boolean;
|
|
130
|
+
}
|
|
131
|
+
export interface ScoreResult {
|
|
132
|
+
score: number;
|
|
133
|
+
breakdown?: ScoreBreakdown;
|
|
134
|
+
auditTrail?: TrackedEvent[];
|
|
135
|
+
}
|
|
136
|
+
export type ScoringFunction = (events: TrackedEvent[]) => number | undefined;
|
|
137
|
+
export interface BehaviorSettings {
|
|
138
|
+
sampleRates?: {
|
|
139
|
+
mouseMove?: number;
|
|
140
|
+
scroll?: number;
|
|
141
|
+
keypress?: number;
|
|
142
|
+
};
|
|
143
|
+
rollingWindows?: {
|
|
144
|
+
mouseMove?: number;
|
|
145
|
+
scroll?: number;
|
|
146
|
+
};
|
|
147
|
+
weights?: {
|
|
148
|
+
mouseMovement?: number;
|
|
149
|
+
clickAccuracy?: number;
|
|
150
|
+
scrollBehavior?: number;
|
|
151
|
+
keyboardTiming?: number;
|
|
152
|
+
tabActivity?: number;
|
|
153
|
+
resizeBehavior?: number;
|
|
154
|
+
environmentFingerprint?: number;
|
|
155
|
+
};
|
|
156
|
+
customScorers?: {
|
|
157
|
+
mouseMovement?: ScoringFunction;
|
|
158
|
+
clickAccuracy?: ScoringFunction;
|
|
159
|
+
scrollBehavior?: ScoringFunction;
|
|
160
|
+
keyboardTiming?: ScoringFunction;
|
|
161
|
+
tabActivity?: ScoringFunction;
|
|
162
|
+
resizeBehavior?: ScoringFunction;
|
|
163
|
+
environmentFingerprint?: ScoringFunction;
|
|
164
|
+
};
|
|
165
|
+
clickMouseHistoryWindow?: number;
|
|
166
|
+
useWebWorker?: boolean;
|
|
167
|
+
}
|
|
168
|
+
export declare const DEFAULT_SETTINGS: Required<BehaviorSettings>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const DEFAULT_SETTINGS = {
|
|
2
|
+
sampleRates: {
|
|
3
|
+
mouseMove: 0.1, // Track 10% of mouse moves
|
|
4
|
+
scroll: 1.0, // Track ALL scrolls - critical for detection
|
|
5
|
+
keypress: 1.0, // Track all keypresses
|
|
6
|
+
},
|
|
7
|
+
rollingWindows: {
|
|
8
|
+
mouseMove: 30000,
|
|
9
|
+
scroll: 30000,
|
|
10
|
+
},
|
|
11
|
+
weights: {
|
|
12
|
+
mouseMovement: 0.30, // Increased - continuous behavioral signal
|
|
13
|
+
clickAccuracy: 0.30, // Increased - critical behavioral signal
|
|
14
|
+
scrollBehavior: 0.15, // Unchanged
|
|
15
|
+
keyboardTiming: 0.10, // Slightly reduced
|
|
16
|
+
tabActivity: 0.05, // Unchanged
|
|
17
|
+
resizeBehavior: 0.02, // Reduced - less reliable
|
|
18
|
+
environmentFingerprint: 0.08, // Reduced - static signal, one-time check
|
|
19
|
+
},
|
|
20
|
+
customScorers: {},
|
|
21
|
+
clickMouseHistoryWindow: 1000,
|
|
22
|
+
useWebWorker: false, // Will implement Web Worker support in phase 2
|
|
23
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @axeptio/behavior-detection
|
|
3
|
+
*
|
|
4
|
+
* Lightweight behavior detection library to assess human likelihood of user sessions
|
|
5
|
+
*
|
|
6
|
+
* @example Settings-based (all strategies included)
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { BehaviorDetector } from '@axeptio/behavior-detection';
|
|
9
|
+
*
|
|
10
|
+
* const detector = new BehaviorDetector({
|
|
11
|
+
* weights: {
|
|
12
|
+
* mouseMovement: 0.3,
|
|
13
|
+
* clickAccuracy: 0.3,
|
|
14
|
+
* },
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* detector.start();
|
|
18
|
+
* const result = await detector.score({ breakdown: true });
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @example Strategy-based (tree-shakeable, import only what you need)
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { BehaviorDetector, Mouse, Click, Keyboard } from '@axeptio/behavior-detection';
|
|
24
|
+
*
|
|
25
|
+
* const detector = new BehaviorDetector()
|
|
26
|
+
* .addStrategy(new Mouse())
|
|
27
|
+
* .addStrategy(new Click())
|
|
28
|
+
* .addStrategy(new Keyboard());
|
|
29
|
+
*
|
|
30
|
+
* detector.start();
|
|
31
|
+
* const result = await detector.score();
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export { BehaviorDetector } from './behavior-detector';
|
|
35
|
+
export type { DetectionStrategy, StrategyConfig } from './strategy';
|
|
36
|
+
export { Mouse, Scroll, Click, Tap, Keyboard, Environment, Resize, MouseStrategy, ScrollStrategy, ClickStrategy, TapStrategy, KeyboardStrategy, EnvironmentStrategy, ResizeStrategy, } from './strategies/index.js';
|
|
37
|
+
export type { BehaviorSettings, ScoreOptions, ScoreResult, ScoreBreakdown, TrackedEvent, EventType, ScoringFunction, } from './types';
|
|
38
|
+
export { DEFAULT_SETTINGS } from './types';
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mathematical utility functions for continuous scoring
|
|
3
|
+
* Replaces magic number if-else chains with smooth mathematical functions
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Sigmoid function - Maps any value to [0, 1] with smooth S-curve
|
|
7
|
+
* @param x - Input value
|
|
8
|
+
* @param midpoint - Value that maps to 0.5
|
|
9
|
+
* @param steepness - How steep the curve is (higher = steeper)
|
|
10
|
+
*/
|
|
11
|
+
export declare function sigmoid(x: number, midpoint?: number, steepness?: number): number;
|
|
12
|
+
/**
|
|
13
|
+
* Inverse sigmoid - High values map to low scores
|
|
14
|
+
*/
|
|
15
|
+
export declare function inverseSigmoid(x: number, midpoint?: number, steepness?: number): number;
|
|
16
|
+
/**
|
|
17
|
+
* Gaussian (bell curve) - Peak at ideal value, falls off on both sides
|
|
18
|
+
* @param x - Input value
|
|
19
|
+
* @param ideal - Optimal value (peak of curve)
|
|
20
|
+
* @param width - How wide the acceptable range is
|
|
21
|
+
*/
|
|
22
|
+
export declare function gaussian(x: number, ideal?: number, width?: number): number;
|
|
23
|
+
/**
|
|
24
|
+
* Exponential decay - High values get penalized exponentially
|
|
25
|
+
*/
|
|
26
|
+
export declare function exponentialDecay(x: number, decayRate?: number): number;
|
|
27
|
+
/**
|
|
28
|
+
* Normalize value to [0, 1] range
|
|
29
|
+
*/
|
|
30
|
+
export declare function normalize(value: number, min?: number, max?: number): number;
|
|
31
|
+
/**
|
|
32
|
+
* Clamp value to [0, 1]
|
|
33
|
+
*/
|
|
34
|
+
export declare function clamp01(value: number): number;
|
|
35
|
+
/**
|
|
36
|
+
* Statistical metrics for an array of numbers
|
|
37
|
+
*/
|
|
38
|
+
export interface Statistics {
|
|
39
|
+
mean: number;
|
|
40
|
+
variance: number;
|
|
41
|
+
stdDev: number;
|
|
42
|
+
cv: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Calculate statistical metrics for an array of numbers
|
|
46
|
+
* @param values - Array of numerical values
|
|
47
|
+
* @returns Statistical metrics (mean, variance, stdDev, cv)
|
|
48
|
+
*/
|
|
49
|
+
export declare function calculateStatistics(values: number[]): Statistics;
|
|
50
|
+
/**
|
|
51
|
+
* Analysis result for interval patterns
|
|
52
|
+
*/
|
|
53
|
+
export interface IntervalAnalysis {
|
|
54
|
+
statistics: Statistics;
|
|
55
|
+
uniqueCount: number;
|
|
56
|
+
allIdentical: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Analyze timing intervals for bot-like patterns
|
|
60
|
+
* Returns raw analysis data without scoring - caller decides heuristics
|
|
61
|
+
*
|
|
62
|
+
* @param intervals - Array of time intervals (ms)
|
|
63
|
+
* @returns Analysis of interval patterns including stats and uniqueness
|
|
64
|
+
*/
|
|
65
|
+
export declare function analyzeIntervals(intervals: number[]): IntervalAnalysis | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Score coefficient of variation
|
|
68
|
+
* Maps CV to human-likeness score using Gaussian
|
|
69
|
+
* Ideal CV is around 0.4-0.5 (moderate variation)
|
|
70
|
+
*/
|
|
71
|
+
export declare function scoreCoefficientOfVariation(cv: number): number;
|
|
72
|
+
/**
|
|
73
|
+
* Score entropy - Higher is better (more random = more human)
|
|
74
|
+
* Uses sigmoid to smoothly map entropy to score
|
|
75
|
+
*/
|
|
76
|
+
export declare function scoreEntropy(normalizedEntropy: number): number;
|
|
77
|
+
/**
|
|
78
|
+
* Score autocorrelation - Lower is better (less periodic = more human)
|
|
79
|
+
*/
|
|
80
|
+
export declare function scoreAutocorrelation(autocorr: number): number;
|
|
81
|
+
/**
|
|
82
|
+
* Score jerk - Lower is better (smoother = more human)
|
|
83
|
+
*/
|
|
84
|
+
export declare function scoreJerk(avgJerk: number): number;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Click Behavior Detection Strategy
|
|
3
|
+
* Monitors specific elements for click positioning patterns
|
|
4
|
+
*/
|
|
5
|
+
import { BaseStrategy } from '../strategy';
|
|
6
|
+
export declare class ClickStrategy extends BaseStrategy {
|
|
7
|
+
readonly name = "click";
|
|
8
|
+
readonly defaultWeight = 0.3;
|
|
9
|
+
private events;
|
|
10
|
+
private targetSelectors;
|
|
11
|
+
private lastMousePosition;
|
|
12
|
+
private mouseListener;
|
|
13
|
+
private clickListeners;
|
|
14
|
+
private isActive;
|
|
15
|
+
constructor(options?: {
|
|
16
|
+
targetSelectors?: string[];
|
|
17
|
+
});
|
|
18
|
+
start(): void;
|
|
19
|
+
/**
|
|
20
|
+
* Add a new target selector at runtime
|
|
21
|
+
*/
|
|
22
|
+
addTarget(selector: string): void;
|
|
23
|
+
private attachClickListeners;
|
|
24
|
+
private attachClickListenersForSelector;
|
|
25
|
+
stop(): void;
|
|
26
|
+
reset(): void;
|
|
27
|
+
score(): number | undefined;
|
|
28
|
+
getDebugInfo(): {
|
|
29
|
+
eventCount: number;
|
|
30
|
+
positions: {
|
|
31
|
+
noMouseData: number;
|
|
32
|
+
outside: number;
|
|
33
|
+
deadCenter: number;
|
|
34
|
+
overElement: number;
|
|
35
|
+
};
|
|
36
|
+
inViewport: number;
|
|
37
|
+
trackedElements: number;
|
|
38
|
+
};
|
|
39
|
+
}
|