@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,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Click Behavior Detection Strategy
|
|
3
|
+
* Monitors specific elements for click positioning patterns
|
|
4
|
+
*/
|
|
5
|
+
import { BaseStrategy } from '../strategy';
|
|
6
|
+
export class ClickStrategy extends BaseStrategy {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
super();
|
|
9
|
+
this.name = 'click';
|
|
10
|
+
this.defaultWeight = 0.30;
|
|
11
|
+
this.events = [];
|
|
12
|
+
this.targetSelectors = ['button', 'a', 'input[type="submit"]', '[role="button"]'];
|
|
13
|
+
this.lastMousePosition = null;
|
|
14
|
+
this.mouseListener = null;
|
|
15
|
+
this.clickListeners = new Map();
|
|
16
|
+
this.isActive = false;
|
|
17
|
+
if (options === null || options === void 0 ? void 0 : options.targetSelectors) {
|
|
18
|
+
this.targetSelectors = options.targetSelectors;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
start() {
|
|
22
|
+
if (this.isActive)
|
|
23
|
+
return;
|
|
24
|
+
this.isActive = true;
|
|
25
|
+
// Track mouse position globally
|
|
26
|
+
this.mouseListener = (e) => {
|
|
27
|
+
const mouseEvent = e;
|
|
28
|
+
this.lastMousePosition = {
|
|
29
|
+
x: mouseEvent.clientX,
|
|
30
|
+
y: mouseEvent.clientY,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
document.addEventListener('mousemove', this.mouseListener, { passive: true });
|
|
34
|
+
// Find all matching elements and attach listeners
|
|
35
|
+
this.attachClickListeners();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Add a new target selector at runtime
|
|
39
|
+
*/
|
|
40
|
+
addTarget(selector) {
|
|
41
|
+
if (!this.targetSelectors.includes(selector)) {
|
|
42
|
+
this.targetSelectors.push(selector);
|
|
43
|
+
// Attach listeners if already active
|
|
44
|
+
if (this.isActive) {
|
|
45
|
+
this.attachClickListenersForSelector(selector);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
attachClickListeners() {
|
|
50
|
+
this.targetSelectors.forEach(selector => {
|
|
51
|
+
this.attachClickListenersForSelector(selector);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
attachClickListenersForSelector(selector) {
|
|
55
|
+
const elements = document.querySelectorAll(selector);
|
|
56
|
+
elements.forEach(element => {
|
|
57
|
+
if (this.clickListeners.has(element))
|
|
58
|
+
return; // Already attached
|
|
59
|
+
const listener = (e) => {
|
|
60
|
+
var _a, _b;
|
|
61
|
+
const clickEvent = e;
|
|
62
|
+
const rect = element.getBoundingClientRect();
|
|
63
|
+
// Check if element is in viewport
|
|
64
|
+
const inViewport = (rect.top >= 0 &&
|
|
65
|
+
rect.left >= 0 &&
|
|
66
|
+
rect.bottom <= window.innerHeight &&
|
|
67
|
+
rect.right <= window.innerWidth);
|
|
68
|
+
// Analyze click position
|
|
69
|
+
let position;
|
|
70
|
+
if (!this.lastMousePosition) {
|
|
71
|
+
position = 'no-mouse-data'; // Bot - no mouse movement
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const mx = this.lastMousePosition.x;
|
|
75
|
+
const my = this.lastMousePosition.y;
|
|
76
|
+
// Check if mouse was over element
|
|
77
|
+
const overElement = (mx >= rect.left &&
|
|
78
|
+
mx <= rect.right &&
|
|
79
|
+
my >= rect.top &&
|
|
80
|
+
my <= rect.bottom);
|
|
81
|
+
if (!overElement) {
|
|
82
|
+
position = 'outside'; // Bot - mouse not over target
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Check if dead center (suspicious)
|
|
86
|
+
const centerX = rect.left + rect.width / 2;
|
|
87
|
+
const centerY = rect.top + rect.height / 2;
|
|
88
|
+
const distanceFromCenter = Math.sqrt((mx - centerX) ** 2 + (my - centerY) ** 2);
|
|
89
|
+
// Dead center = within 2px of center
|
|
90
|
+
if (distanceFromCenter < 2) {
|
|
91
|
+
position = 'dead-center'; // Suspicious - too perfect
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
position = 'over-element'; // Human - somewhere on the button
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
this.events.push({
|
|
99
|
+
clickX: clickEvent.clientX,
|
|
100
|
+
clickY: clickEvent.clientY,
|
|
101
|
+
mouseX: (_a = this.lastMousePosition) === null || _a === void 0 ? void 0 : _a.x,
|
|
102
|
+
mouseY: (_b = this.lastMousePosition) === null || _b === void 0 ? void 0 : _b.y,
|
|
103
|
+
rect,
|
|
104
|
+
inViewport,
|
|
105
|
+
position,
|
|
106
|
+
timestamp: Date.now(),
|
|
107
|
+
});
|
|
108
|
+
// Notify detector - clicks are high-value events
|
|
109
|
+
this.notifyEvent(1.0);
|
|
110
|
+
};
|
|
111
|
+
element.addEventListener('click', listener);
|
|
112
|
+
this.clickListeners.set(element, listener);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
stop() {
|
|
116
|
+
if (!this.isActive)
|
|
117
|
+
return;
|
|
118
|
+
this.isActive = false;
|
|
119
|
+
// Remove mouse listener
|
|
120
|
+
if (this.mouseListener) {
|
|
121
|
+
document.removeEventListener('mousemove', this.mouseListener);
|
|
122
|
+
this.mouseListener = null;
|
|
123
|
+
}
|
|
124
|
+
// Remove all click listeners
|
|
125
|
+
this.clickListeners.forEach((listener, element) => {
|
|
126
|
+
element.removeEventListener('click', listener);
|
|
127
|
+
});
|
|
128
|
+
this.clickListeners.clear();
|
|
129
|
+
}
|
|
130
|
+
reset() {
|
|
131
|
+
this.events = [];
|
|
132
|
+
this.lastMousePosition = null;
|
|
133
|
+
}
|
|
134
|
+
score() {
|
|
135
|
+
if (this.events.length === 0)
|
|
136
|
+
return undefined;
|
|
137
|
+
let totalScore = 0;
|
|
138
|
+
for (const click of this.events) {
|
|
139
|
+
switch (click.position) {
|
|
140
|
+
case 'no-mouse-data':
|
|
141
|
+
totalScore += 0.0; // Bot - no mouse movement
|
|
142
|
+
break;
|
|
143
|
+
case 'outside':
|
|
144
|
+
totalScore += 0.0; // Bot - mouse not over target
|
|
145
|
+
break;
|
|
146
|
+
case 'dead-center':
|
|
147
|
+
totalScore += 0.5; // Suspicious - too perfect
|
|
148
|
+
break;
|
|
149
|
+
case 'over-element':
|
|
150
|
+
totalScore += 1.0; // Human - natural click
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return totalScore / this.events.length;
|
|
155
|
+
}
|
|
156
|
+
getDebugInfo() {
|
|
157
|
+
return {
|
|
158
|
+
eventCount: this.events.length,
|
|
159
|
+
positions: {
|
|
160
|
+
noMouseData: this.events.filter(e => e.position === 'no-mouse-data').length,
|
|
161
|
+
outside: this.events.filter(e => e.position === 'outside').length,
|
|
162
|
+
deadCenter: this.events.filter(e => e.position === 'dead-center').length,
|
|
163
|
+
overElement: this.events.filter(e => e.position === 'over-element').length,
|
|
164
|
+
},
|
|
165
|
+
inViewport: this.events.filter(e => e.inViewport).length,
|
|
166
|
+
trackedElements: this.clickListeners.size,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Fingerprinting Strategy
|
|
3
|
+
* Captures browser environment on initialization and tick updates
|
|
4
|
+
*/
|
|
5
|
+
import { BaseStrategy } from '../strategy';
|
|
6
|
+
interface EnvironmentData {
|
|
7
|
+
screenWidth: number;
|
|
8
|
+
screenHeight: number;
|
|
9
|
+
windowWidth: number;
|
|
10
|
+
windowHeight: number;
|
|
11
|
+
devicePixelRatio: number;
|
|
12
|
+
colorDepth: number;
|
|
13
|
+
userAgent: string;
|
|
14
|
+
platform: string;
|
|
15
|
+
language: string;
|
|
16
|
+
languages: string[];
|
|
17
|
+
hardwareConcurrency?: number;
|
|
18
|
+
maxTouchPoints: number;
|
|
19
|
+
vendor: string;
|
|
20
|
+
hasWebGL: boolean;
|
|
21
|
+
hasWebRTC: boolean;
|
|
22
|
+
hasLocalStorage: boolean;
|
|
23
|
+
hasSessionStorage: boolean;
|
|
24
|
+
hasIndexedDB: boolean;
|
|
25
|
+
plugins: number;
|
|
26
|
+
mimeTypes: number;
|
|
27
|
+
suspiciousRatio: boolean;
|
|
28
|
+
suspiciousDimensions: boolean;
|
|
29
|
+
featureInconsistency: boolean;
|
|
30
|
+
isMobile: boolean;
|
|
31
|
+
timestamp: number;
|
|
32
|
+
}
|
|
33
|
+
export declare class EnvironmentStrategy extends BaseStrategy {
|
|
34
|
+
readonly name = "environment";
|
|
35
|
+
readonly defaultWeight = 0.08;
|
|
36
|
+
private data;
|
|
37
|
+
start(): void;
|
|
38
|
+
stop(): void;
|
|
39
|
+
reset(): void;
|
|
40
|
+
onTick(_timestamp: number): void;
|
|
41
|
+
score(): number | undefined;
|
|
42
|
+
private isMobileDevice;
|
|
43
|
+
private captureEnvironment;
|
|
44
|
+
getDebugInfo(): EnvironmentData | null;
|
|
45
|
+
/**
|
|
46
|
+
* Check if current device is mobile
|
|
47
|
+
* Returns true if mobile, false otherwise
|
|
48
|
+
* Returns null if environment hasn't been captured yet
|
|
49
|
+
*/
|
|
50
|
+
isMobile(): boolean | null;
|
|
51
|
+
}
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Fingerprinting Strategy
|
|
3
|
+
* Captures browser environment on initialization and tick updates
|
|
4
|
+
*/
|
|
5
|
+
import { BaseStrategy } from '../strategy';
|
|
6
|
+
import { inverseSigmoid, gaussian } from '../math-utils';
|
|
7
|
+
export class EnvironmentStrategy extends BaseStrategy {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
this.name = 'environment';
|
|
11
|
+
this.defaultWeight = 0.08;
|
|
12
|
+
this.data = null;
|
|
13
|
+
}
|
|
14
|
+
start() {
|
|
15
|
+
this.captureEnvironment();
|
|
16
|
+
}
|
|
17
|
+
stop() {
|
|
18
|
+
// Environment data persists
|
|
19
|
+
}
|
|
20
|
+
reset() {
|
|
21
|
+
this.data = null;
|
|
22
|
+
}
|
|
23
|
+
onTick(_timestamp) {
|
|
24
|
+
// Re-capture environment periodically to detect changes
|
|
25
|
+
this.captureEnvironment();
|
|
26
|
+
}
|
|
27
|
+
score() {
|
|
28
|
+
if (!this.data)
|
|
29
|
+
return undefined;
|
|
30
|
+
const env = this.data;
|
|
31
|
+
let score = 0;
|
|
32
|
+
let factors = 0;
|
|
33
|
+
// Suspicious indicators
|
|
34
|
+
score += env.suspiciousDimensions ? 0.1 : 1.0;
|
|
35
|
+
score += env.suspiciousRatio ? 0.2 : 1.0;
|
|
36
|
+
score += env.featureInconsistency ? 0.3 : 1.0;
|
|
37
|
+
factors += 3;
|
|
38
|
+
// Browser features
|
|
39
|
+
const featureCount = [
|
|
40
|
+
env.hasWebGL,
|
|
41
|
+
env.hasLocalStorage,
|
|
42
|
+
env.hasSessionStorage,
|
|
43
|
+
env.hasIndexedDB,
|
|
44
|
+
].filter(Boolean).length;
|
|
45
|
+
score += featureCount / 4;
|
|
46
|
+
factors++;
|
|
47
|
+
// Plugins
|
|
48
|
+
score += inverseSigmoid(env.plugins, -2, -0.5);
|
|
49
|
+
score += (env.plugins > 0 ? 1.0 : 0.1);
|
|
50
|
+
factors += 2;
|
|
51
|
+
// Device
|
|
52
|
+
score += gaussian(env.devicePixelRatio, 2, 1.5);
|
|
53
|
+
score += (env.colorDepth === 24 || env.colorDepth === 32) ? 1.0 : 0.4;
|
|
54
|
+
factors += 2;
|
|
55
|
+
return factors > 0 ? score / factors : undefined;
|
|
56
|
+
}
|
|
57
|
+
isMobileDevice() {
|
|
58
|
+
// Multiple checks for mobile detection
|
|
59
|
+
const hasTouchScreen = navigator.maxTouchPoints > 0 || 'ontouchstart' in window;
|
|
60
|
+
const mobileUA = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
61
|
+
const smallScreen = window.innerWidth < 768 && window.innerHeight < 1024;
|
|
62
|
+
return (hasTouchScreen && smallScreen) || mobileUA;
|
|
63
|
+
}
|
|
64
|
+
captureEnvironment() {
|
|
65
|
+
try {
|
|
66
|
+
// WebGL detection
|
|
67
|
+
let hasWebGL = false;
|
|
68
|
+
try {
|
|
69
|
+
const canvas = document.createElement('canvas');
|
|
70
|
+
hasWebGL = !!(canvas.getContext('webgl') || canvas.getContext('experimental-webgl'));
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
hasWebGL = false;
|
|
74
|
+
}
|
|
75
|
+
// WebRTC detection
|
|
76
|
+
const hasWebRTC = !!(window.RTCPeerConnection ||
|
|
77
|
+
window.mozRTCPeerConnection ||
|
|
78
|
+
window.webkitRTCPeerConnection);
|
|
79
|
+
// Mobile detection
|
|
80
|
+
const isMobile = this.isMobileDevice();
|
|
81
|
+
const windowWidth = window.innerWidth;
|
|
82
|
+
const windowHeight = window.innerHeight;
|
|
83
|
+
const screenWidth = window.screen.width;
|
|
84
|
+
const screenHeight = window.screen.height;
|
|
85
|
+
// Suspicious dimensions
|
|
86
|
+
const suspiciousDimensions = (windowWidth === 800 && windowHeight === 600) ||
|
|
87
|
+
(windowWidth === 1024 && windowHeight === 768) ||
|
|
88
|
+
(windowWidth === 1280 && windowHeight === 720) ||
|
|
89
|
+
(screenWidth === 800 && screenHeight === 600);
|
|
90
|
+
// Suspicious ratio
|
|
91
|
+
const windowScreenRatio = (windowWidth * windowHeight) / (screenWidth * screenHeight);
|
|
92
|
+
const suspiciousRatio = windowScreenRatio === 1.0 ||
|
|
93
|
+
windowScreenRatio < 0.1 ||
|
|
94
|
+
windowScreenRatio > 1.0;
|
|
95
|
+
// Feature inconsistency
|
|
96
|
+
const hasStorage = typeof localStorage !== 'undefined' && typeof sessionStorage !== 'undefined';
|
|
97
|
+
const featureInconsistency = (navigator.plugins.length === 0 && navigator.mimeTypes.length === 0) ||
|
|
98
|
+
!hasWebGL ||
|
|
99
|
+
!hasStorage;
|
|
100
|
+
this.data = {
|
|
101
|
+
screenWidth,
|
|
102
|
+
screenHeight,
|
|
103
|
+
windowWidth,
|
|
104
|
+
windowHeight,
|
|
105
|
+
devicePixelRatio: window.devicePixelRatio,
|
|
106
|
+
colorDepth: window.screen.colorDepth,
|
|
107
|
+
userAgent: navigator.userAgent,
|
|
108
|
+
platform: navigator.platform,
|
|
109
|
+
language: navigator.language,
|
|
110
|
+
languages: navigator.languages ? Array.from(navigator.languages) : [navigator.language],
|
|
111
|
+
hardwareConcurrency: navigator.hardwareConcurrency,
|
|
112
|
+
maxTouchPoints: navigator.maxTouchPoints || 0,
|
|
113
|
+
vendor: navigator.vendor,
|
|
114
|
+
hasWebGL,
|
|
115
|
+
hasWebRTC,
|
|
116
|
+
hasLocalStorage: typeof localStorage !== 'undefined',
|
|
117
|
+
hasSessionStorage: typeof sessionStorage !== 'undefined',
|
|
118
|
+
hasIndexedDB: 'indexedDB' in window,
|
|
119
|
+
plugins: navigator.plugins.length,
|
|
120
|
+
mimeTypes: navigator.mimeTypes.length,
|
|
121
|
+
suspiciousRatio,
|
|
122
|
+
suspiciousDimensions,
|
|
123
|
+
featureInconsistency,
|
|
124
|
+
isMobile,
|
|
125
|
+
timestamp: Date.now(),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.warn('Failed to capture environment:', error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
getDebugInfo() {
|
|
133
|
+
return this.data;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Check if current device is mobile
|
|
137
|
+
* Returns true if mobile, false otherwise
|
|
138
|
+
* Returns null if environment hasn't been captured yet
|
|
139
|
+
*/
|
|
140
|
+
isMobile() {
|
|
141
|
+
var _a, _b;
|
|
142
|
+
return (_b = (_a = this.data) === null || _a === void 0 ? void 0 : _a.isMobile) !== null && _b !== void 0 ? _b : null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection Strategies - Modular behavior analysis
|
|
3
|
+
* Import only what you need for optimal bundle size
|
|
4
|
+
*/
|
|
5
|
+
export { MouseStrategy } from './mouse';
|
|
6
|
+
export { ScrollStrategy } from './scroll';
|
|
7
|
+
export { ClickStrategy } from './click';
|
|
8
|
+
export { TapStrategy } from './tap';
|
|
9
|
+
export { KeyboardStrategy } from './keyboard';
|
|
10
|
+
export { EnvironmentStrategy } from './environment';
|
|
11
|
+
export { ResizeStrategy } from './resize';
|
|
12
|
+
export { MouseStrategy as Mouse } from './mouse';
|
|
13
|
+
export { ScrollStrategy as Scroll } from './scroll';
|
|
14
|
+
export { ClickStrategy as Click } from './click';
|
|
15
|
+
export { TapStrategy as Tap } from './tap';
|
|
16
|
+
export { KeyboardStrategy as Keyboard } from './keyboard';
|
|
17
|
+
export { EnvironmentStrategy as Environment } from './environment';
|
|
18
|
+
export { ResizeStrategy as Resize } from './resize';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection Strategies - Modular behavior analysis
|
|
3
|
+
* Import only what you need for optimal bundle size
|
|
4
|
+
*/
|
|
5
|
+
export { MouseStrategy } from './mouse';
|
|
6
|
+
export { ScrollStrategy } from './scroll';
|
|
7
|
+
export { ClickStrategy } from './click';
|
|
8
|
+
export { TapStrategy } from './tap';
|
|
9
|
+
export { KeyboardStrategy } from './keyboard';
|
|
10
|
+
export { EnvironmentStrategy } from './environment';
|
|
11
|
+
export { ResizeStrategy } from './resize';
|
|
12
|
+
// Convenience: All strategies
|
|
13
|
+
export { MouseStrategy as Mouse } from './mouse';
|
|
14
|
+
export { ScrollStrategy as Scroll } from './scroll';
|
|
15
|
+
export { ClickStrategy as Click } from './click';
|
|
16
|
+
export { TapStrategy as Tap } from './tap';
|
|
17
|
+
export { KeyboardStrategy as Keyboard } from './keyboard';
|
|
18
|
+
export { EnvironmentStrategy as Environment } from './environment';
|
|
19
|
+
export { ResizeStrategy as Resize } from './resize';
|
|
@@ -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,229 @@
|
|
|
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
|
+
import { analyzeIntervals, scoreCoefficientOfVariation } from '../math-utils';
|
|
8
|
+
export class KeyboardStrategy extends BaseStrategy {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
super();
|
|
11
|
+
this.name = 'keyboard';
|
|
12
|
+
this.defaultWeight = 0.10;
|
|
13
|
+
this.events = [];
|
|
14
|
+
this.targetSelectors = ['input[type="text"]', 'input[type="email"]', 'textarea'];
|
|
15
|
+
this.focusedElement = null;
|
|
16
|
+
this.focusedElementSelector = '';
|
|
17
|
+
this.lastEventTimestamp = 0;
|
|
18
|
+
this.sessionPauseThreshold = 1000;
|
|
19
|
+
this.downListener = null;
|
|
20
|
+
this.upListener = null;
|
|
21
|
+
this.focusListeners = new Map();
|
|
22
|
+
this.blurListeners = new Map();
|
|
23
|
+
this.isActive = false;
|
|
24
|
+
if (options === null || options === void 0 ? void 0 : options.targetSelectors) {
|
|
25
|
+
this.targetSelectors = options.targetSelectors;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
start() {
|
|
29
|
+
if (this.isActive)
|
|
30
|
+
return;
|
|
31
|
+
this.isActive = true;
|
|
32
|
+
// Attach focus/blur listeners to all matching elements
|
|
33
|
+
this.attachFocusListeners();
|
|
34
|
+
// Key event listeners (only track when an input is focused)
|
|
35
|
+
this.downListener = (e) => {
|
|
36
|
+
if (!this.focusedElement)
|
|
37
|
+
return; // No input focused
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const keyEvent = e;
|
|
40
|
+
// Clear events if pause > 1s (new typing session)
|
|
41
|
+
if (this.lastEventTimestamp > 0 && now - this.lastEventTimestamp > this.sessionPauseThreshold) {
|
|
42
|
+
this.events = [];
|
|
43
|
+
}
|
|
44
|
+
this.events.push({
|
|
45
|
+
key: keyEvent.key,
|
|
46
|
+
type: 'down',
|
|
47
|
+
timestamp: now,
|
|
48
|
+
targetElement: this.focusedElementSelector,
|
|
49
|
+
});
|
|
50
|
+
// Notify detector - keystrokes are valuable events
|
|
51
|
+
this.notifyEvent(0.8);
|
|
52
|
+
this.lastEventTimestamp = now;
|
|
53
|
+
};
|
|
54
|
+
this.upListener = (e) => {
|
|
55
|
+
if (!this.focusedElement)
|
|
56
|
+
return; // No input focused
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const keyEvent = e;
|
|
59
|
+
// Clear events if pause > 1s (new typing session)
|
|
60
|
+
if (this.lastEventTimestamp > 0 && now - this.lastEventTimestamp > this.sessionPauseThreshold) {
|
|
61
|
+
this.events = [];
|
|
62
|
+
}
|
|
63
|
+
this.events.push({
|
|
64
|
+
key: keyEvent.key,
|
|
65
|
+
type: 'up',
|
|
66
|
+
timestamp: now,
|
|
67
|
+
targetElement: this.focusedElementSelector,
|
|
68
|
+
});
|
|
69
|
+
this.lastEventTimestamp = now;
|
|
70
|
+
};
|
|
71
|
+
document.addEventListener('keydown', this.downListener);
|
|
72
|
+
document.addEventListener('keyup', this.upListener);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Add a new target selector at runtime
|
|
76
|
+
*/
|
|
77
|
+
addTarget(selector) {
|
|
78
|
+
if (!this.targetSelectors.includes(selector)) {
|
|
79
|
+
this.targetSelectors.push(selector);
|
|
80
|
+
if (this.isActive) {
|
|
81
|
+
this.attachFocusListenersForSelector(selector);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
attachFocusListeners() {
|
|
86
|
+
this.targetSelectors.forEach(selector => {
|
|
87
|
+
this.attachFocusListenersForSelector(selector);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
attachFocusListenersForSelector(selector) {
|
|
91
|
+
const elements = document.querySelectorAll(selector);
|
|
92
|
+
elements.forEach(element => {
|
|
93
|
+
if (this.focusListeners.has(element))
|
|
94
|
+
return; // Already attached
|
|
95
|
+
const focusListener = () => {
|
|
96
|
+
this.focusedElement = element;
|
|
97
|
+
this.focusedElementSelector = selector;
|
|
98
|
+
};
|
|
99
|
+
const blurListener = () => {
|
|
100
|
+
this.focusedElement = null;
|
|
101
|
+
this.focusedElementSelector = '';
|
|
102
|
+
};
|
|
103
|
+
element.addEventListener('focus', focusListener);
|
|
104
|
+
element.addEventListener('blur', blurListener);
|
|
105
|
+
this.focusListeners.set(element, focusListener);
|
|
106
|
+
this.blurListeners.set(element, blurListener);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
stop() {
|
|
110
|
+
if (!this.isActive)
|
|
111
|
+
return;
|
|
112
|
+
this.isActive = false;
|
|
113
|
+
if (this.downListener) {
|
|
114
|
+
document.removeEventListener('keydown', this.downListener);
|
|
115
|
+
this.downListener = null;
|
|
116
|
+
}
|
|
117
|
+
if (this.upListener) {
|
|
118
|
+
document.removeEventListener('keyup', this.upListener);
|
|
119
|
+
this.upListener = null;
|
|
120
|
+
}
|
|
121
|
+
// Remove focus/blur listeners
|
|
122
|
+
this.focusListeners.forEach((listener, element) => {
|
|
123
|
+
element.removeEventListener('focus', listener);
|
|
124
|
+
});
|
|
125
|
+
this.focusListeners.clear();
|
|
126
|
+
this.blurListeners.forEach((listener, element) => {
|
|
127
|
+
element.removeEventListener('blur', listener);
|
|
128
|
+
});
|
|
129
|
+
this.blurListeners.clear();
|
|
130
|
+
this.focusedElement = null;
|
|
131
|
+
}
|
|
132
|
+
reset() {
|
|
133
|
+
this.events = [];
|
|
134
|
+
this.focusedElement = null;
|
|
135
|
+
this.focusedElementSelector = '';
|
|
136
|
+
this.lastEventTimestamp = 0;
|
|
137
|
+
}
|
|
138
|
+
score() {
|
|
139
|
+
if (this.events.length < 6)
|
|
140
|
+
return undefined; // Need at least 3 key pairs
|
|
141
|
+
const downEvents = this.events.filter(e => e.type === 'down');
|
|
142
|
+
if (downEvents.length < 3)
|
|
143
|
+
return undefined;
|
|
144
|
+
let score = 0;
|
|
145
|
+
let factors = 0;
|
|
146
|
+
// 1. Analyze keystroke intervals (time between keydown events)
|
|
147
|
+
const keystrokeIntervals = [];
|
|
148
|
+
for (let i = 1; i < downEvents.length; i++) {
|
|
149
|
+
keystrokeIntervals.push(downEvents[i].timestamp - downEvents[i - 1].timestamp);
|
|
150
|
+
}
|
|
151
|
+
const keystrokeAnalysis = analyzeIntervals(keystrokeIntervals);
|
|
152
|
+
if (keystrokeAnalysis) {
|
|
153
|
+
const { statistics, allIdentical } = keystrokeAnalysis;
|
|
154
|
+
// Keyboard-specific heuristics for keystroke timing
|
|
155
|
+
if (allIdentical) {
|
|
156
|
+
return 0.1; // All identical - bot!
|
|
157
|
+
}
|
|
158
|
+
const keystrokeScore = scoreCoefficientOfVariation(statistics.cv);
|
|
159
|
+
score += keystrokeScore;
|
|
160
|
+
factors++;
|
|
161
|
+
// Early return if bot detected
|
|
162
|
+
if (keystrokeScore <= 0.1)
|
|
163
|
+
return keystrokeScore;
|
|
164
|
+
}
|
|
165
|
+
// 2. Analyze press durations (keydown-to-keyup duration)
|
|
166
|
+
const pressDurations = [];
|
|
167
|
+
for (let i = 0; i < this.events.length - 1; i++) {
|
|
168
|
+
if (this.events[i].type === 'down' && this.events[i + 1].type === 'up' &&
|
|
169
|
+
this.events[i].key === this.events[i + 1].key) {
|
|
170
|
+
pressDurations.push(this.events[i + 1].timestamp - this.events[i].timestamp);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const pressDurationAnalysis = analyzeIntervals(pressDurations);
|
|
174
|
+
if (pressDurationAnalysis) {
|
|
175
|
+
const { statistics, allIdentical } = pressDurationAnalysis;
|
|
176
|
+
// Keyboard-specific heuristics for press duration
|
|
177
|
+
if (allIdentical) {
|
|
178
|
+
return 0.1; // All identical - bot!
|
|
179
|
+
}
|
|
180
|
+
// Check for suspiciously fast key releases (bots often have <5ms press duration)
|
|
181
|
+
if (statistics.mean < 5) {
|
|
182
|
+
score += 0.1; // Too fast = bot (instant release)
|
|
183
|
+
factors++;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const pressDurationScore = scoreCoefficientOfVariation(statistics.cv);
|
|
187
|
+
score += pressDurationScore;
|
|
188
|
+
factors++;
|
|
189
|
+
// Early return if bot detected
|
|
190
|
+
if (pressDurationScore <= 0.1)
|
|
191
|
+
return pressDurationScore;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// 3. Bonus for backspace usage (human error correction)
|
|
195
|
+
const backspaceCount = this.events.filter(e => e.key === 'Backspace').length;
|
|
196
|
+
if (backspaceCount > 0) {
|
|
197
|
+
// Natural human behavior - making corrections
|
|
198
|
+
const backspaceRatio = backspaceCount / downEvents.length;
|
|
199
|
+
if (backspaceRatio > 0.05 && backspaceRatio < 0.3) {
|
|
200
|
+
score += 1.0; // Reasonable error correction
|
|
201
|
+
factors++;
|
|
202
|
+
}
|
|
203
|
+
else if (backspaceRatio > 0) {
|
|
204
|
+
score += 0.8; // Some backspaces
|
|
205
|
+
factors++;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return factors > 0 ? score / factors : undefined;
|
|
209
|
+
}
|
|
210
|
+
getDebugInfo() {
|
|
211
|
+
// Calculate press durations for visualization
|
|
212
|
+
const pressDurations = [];
|
|
213
|
+
for (let i = 0; i < this.events.length - 1; i++) {
|
|
214
|
+
if (this.events[i].type === 'down' && this.events[i + 1].type === 'up' &&
|
|
215
|
+
this.events[i].key === this.events[i + 1].key) {
|
|
216
|
+
pressDurations.push(this.events[i + 1].timestamp - this.events[i].timestamp);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
eventCount: this.events.length,
|
|
221
|
+
downEvents: this.events.filter(e => e.type === 'down').length,
|
|
222
|
+
upEvents: this.events.filter(e => e.type === 'up').length,
|
|
223
|
+
backspaceCount: this.events.filter(e => e.key === 'Backspace').length,
|
|
224
|
+
pressDurations, // For graphing
|
|
225
|
+
focusedElement: this.focusedElementSelector,
|
|
226
|
+
trackedElements: this.focusListeners.size,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|