@axeptio/behavior-detection 1.0.3 → 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 +920 -22
- 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 +1 -1
package/dist/esm/browser.js
CHANGED
|
@@ -140,7 +140,6 @@ class BehaviorDetectionAPI {
|
|
|
140
140
|
*/
|
|
141
141
|
start() {
|
|
142
142
|
if (!this.detector) {
|
|
143
|
-
console.warn('BehaviorDetector: Call init() before start()');
|
|
144
143
|
return;
|
|
145
144
|
}
|
|
146
145
|
this.detector.start();
|
|
@@ -162,7 +161,6 @@ class BehaviorDetectionAPI {
|
|
|
162
161
|
*/
|
|
163
162
|
async score() {
|
|
164
163
|
if (!this.detector) {
|
|
165
|
-
console.warn('BehaviorDetector: Call init() before score()');
|
|
166
164
|
return null;
|
|
167
165
|
}
|
|
168
166
|
return this.detector.score({ breakdown: true });
|
package/dist/esm/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';
|
package/dist/esm/index.js
CHANGED
|
@@ -32,5 +32,5 @@
|
|
|
32
32
|
* ```
|
|
33
33
|
*/
|
|
34
34
|
export { BehaviorDetector } from './behavior-detector.js';
|
|
35
|
-
export { Mouse, Scroll, Click, Tap, Keyboard, Environment, Resize, MouseStrategy, ScrollStrategy, ClickStrategy, TapStrategy, KeyboardStrategy, EnvironmentStrategy, ResizeStrategy, } from './strategies/index.js';
|
|
35
|
+
export { Mouse, Scroll, Click, Tap, Keyboard, Environment, Resize, Timing, Visibility, MouseStrategy, ScrollStrategy, ClickStrategy, TapStrategy, KeyboardStrategy, EnvironmentStrategy, ResizeStrategy, TimingStrategy, VisibilityStrategy, } from './strategies/index.js';
|
|
36
36
|
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,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 class ClickStrategy extends BaseStrategy {
|
|
@@ -22,12 +27,13 @@ export class ClickStrategy extends BaseStrategy {
|
|
|
22
27
|
if (this.isActive)
|
|
23
28
|
return;
|
|
24
29
|
this.isActive = true;
|
|
25
|
-
// Track mouse position globally
|
|
30
|
+
// Track mouse position globally with timestamp
|
|
26
31
|
this.mouseListener = (e) => {
|
|
27
32
|
const mouseEvent = e;
|
|
28
33
|
this.lastMousePosition = {
|
|
29
34
|
x: mouseEvent.clientX,
|
|
30
35
|
y: mouseEvent.clientY,
|
|
36
|
+
timestamp: Date.now(),
|
|
31
37
|
};
|
|
32
38
|
};
|
|
33
39
|
document.addEventListener('mousemove', this.mouseListener, { passive: true });
|
|
@@ -59,12 +65,23 @@ export class ClickStrategy extends BaseStrategy {
|
|
|
59
65
|
const listener = (e) => {
|
|
60
66
|
var _a, _b;
|
|
61
67
|
const clickEvent = e;
|
|
68
|
+
const now = Date.now();
|
|
62
69
|
const rect = element.getBoundingClientRect();
|
|
63
70
|
// Check if element is in viewport
|
|
64
71
|
const inViewport = (rect.top >= 0 &&
|
|
65
72
|
rect.left >= 0 &&
|
|
66
73
|
rect.bottom <= window.innerHeight &&
|
|
67
74
|
rect.right <= window.innerWidth);
|
|
75
|
+
// Calculate mouse-click position delta
|
|
76
|
+
// Bots using element.click() have click event coordinates different from mouse position
|
|
77
|
+
let mouseClickDelta = 0;
|
|
78
|
+
let timeSinceMouseMove = -1;
|
|
79
|
+
if (this.lastMousePosition) {
|
|
80
|
+
const dx = clickEvent.clientX - this.lastMousePosition.x;
|
|
81
|
+
const dy = clickEvent.clientY - this.lastMousePosition.y;
|
|
82
|
+
mouseClickDelta = Math.sqrt(dx * dx + dy * dy);
|
|
83
|
+
timeSinceMouseMove = now - this.lastMousePosition.timestamp;
|
|
84
|
+
}
|
|
68
85
|
// Analyze click position
|
|
69
86
|
let position;
|
|
70
87
|
if (!this.lastMousePosition) {
|
|
@@ -103,7 +120,10 @@ export class ClickStrategy extends BaseStrategy {
|
|
|
103
120
|
rect,
|
|
104
121
|
inViewport,
|
|
105
122
|
position,
|
|
106
|
-
timestamp:
|
|
123
|
+
timestamp: now,
|
|
124
|
+
mouseClickDelta,
|
|
125
|
+
timeSinceMouseMove,
|
|
126
|
+
isTrusted: clickEvent.isTrusted,
|
|
107
127
|
});
|
|
108
128
|
// Notify detector - clicks are high-value events
|
|
109
129
|
this.notifyEvent(1.0);
|
|
@@ -132,26 +152,102 @@ export class ClickStrategy extends BaseStrategy {
|
|
|
132
152
|
this.lastMousePosition = null;
|
|
133
153
|
}
|
|
134
154
|
score() {
|
|
135
|
-
|
|
155
|
+
const eventCount = this.events.length;
|
|
156
|
+
if (eventCount === 0)
|
|
136
157
|
return undefined;
|
|
137
|
-
|
|
158
|
+
// Pre-compute counts in a single pass
|
|
159
|
+
let positionScore = 0;
|
|
160
|
+
let clicksWithDelta = 0;
|
|
161
|
+
let mismatchCount = 0;
|
|
162
|
+
let clicksWithTiming = 0;
|
|
163
|
+
let suspiciousTimingCount = 0;
|
|
164
|
+
let untrustedCount = 0;
|
|
138
165
|
for (const click of this.events) {
|
|
166
|
+
// Position scoring
|
|
139
167
|
switch (click.position) {
|
|
140
168
|
case 'no-mouse-data':
|
|
141
|
-
|
|
169
|
+
positionScore += 0.0;
|
|
142
170
|
break;
|
|
143
171
|
case 'outside':
|
|
144
|
-
|
|
172
|
+
positionScore += 0.0;
|
|
145
173
|
break;
|
|
146
174
|
case 'dead-center':
|
|
147
|
-
|
|
175
|
+
positionScore += 0.5;
|
|
148
176
|
break;
|
|
149
177
|
case 'over-element':
|
|
150
|
-
|
|
178
|
+
positionScore += 1.0;
|
|
151
179
|
break;
|
|
152
180
|
}
|
|
181
|
+
// Delta scoring
|
|
182
|
+
if (click.mouseClickDelta >= 0) {
|
|
183
|
+
clicksWithDelta++;
|
|
184
|
+
if (click.mouseClickDelta > 5)
|
|
185
|
+
mismatchCount++;
|
|
186
|
+
}
|
|
187
|
+
// Timing scoring
|
|
188
|
+
if (click.timeSinceMouseMove >= 0) {
|
|
189
|
+
clicksWithTiming++;
|
|
190
|
+
if (click.timeSinceMouseMove < 10 || click.timeSinceMouseMove > 2000) {
|
|
191
|
+
suspiciousTimingCount++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Trusted scoring
|
|
195
|
+
if (!click.isTrusted)
|
|
196
|
+
untrustedCount++;
|
|
197
|
+
}
|
|
198
|
+
let score = 0;
|
|
199
|
+
let factors = 0;
|
|
200
|
+
// 1. Position-based scoring
|
|
201
|
+
score += positionScore / eventCount;
|
|
202
|
+
factors++;
|
|
203
|
+
// 2. Mouse-click delta scoring
|
|
204
|
+
if (clicksWithDelta >= 2) {
|
|
205
|
+
const mismatchRatio = mismatchCount / clicksWithDelta;
|
|
206
|
+
let deltaScore;
|
|
207
|
+
if (mismatchRatio >= 0.8)
|
|
208
|
+
deltaScore = 0.1;
|
|
209
|
+
else if (mismatchRatio >= 0.5)
|
|
210
|
+
deltaScore = 0.3;
|
|
211
|
+
else if (mismatchRatio >= 0.3)
|
|
212
|
+
deltaScore = 0.5;
|
|
213
|
+
else if (mismatchRatio > 0)
|
|
214
|
+
deltaScore = 0.7;
|
|
215
|
+
else
|
|
216
|
+
deltaScore = 1.0;
|
|
217
|
+
score += deltaScore;
|
|
218
|
+
factors++;
|
|
219
|
+
}
|
|
220
|
+
// 3. Mouse-to-click timing
|
|
221
|
+
if (clicksWithTiming >= 2) {
|
|
222
|
+
const suspiciousRatio = suspiciousTimingCount / clicksWithTiming;
|
|
223
|
+
let timingScore;
|
|
224
|
+
if (suspiciousRatio >= 0.8)
|
|
225
|
+
timingScore = 0.2;
|
|
226
|
+
else if (suspiciousRatio >= 0.5)
|
|
227
|
+
timingScore = 0.4;
|
|
228
|
+
else if (suspiciousRatio >= 0.3)
|
|
229
|
+
timingScore = 0.6;
|
|
230
|
+
else if (suspiciousRatio > 0)
|
|
231
|
+
timingScore = 0.8;
|
|
232
|
+
else
|
|
233
|
+
timingScore = 1.0;
|
|
234
|
+
score += timingScore;
|
|
235
|
+
factors++;
|
|
236
|
+
}
|
|
237
|
+
// 4. Trusted event scoring
|
|
238
|
+
if (eventCount >= 2) {
|
|
239
|
+
const untrustedRatio = untrustedCount / eventCount;
|
|
240
|
+
let trustedScore;
|
|
241
|
+
if (untrustedRatio >= 0.5)
|
|
242
|
+
trustedScore = 0.1;
|
|
243
|
+
else if (untrustedRatio > 0)
|
|
244
|
+
trustedScore = 0.3;
|
|
245
|
+
else
|
|
246
|
+
trustedScore = 1.0;
|
|
247
|
+
score += trustedScore;
|
|
248
|
+
factors++;
|
|
153
249
|
}
|
|
154
|
-
return
|
|
250
|
+
return factors > 0 ? score / factors : undefined;
|
|
155
251
|
}
|
|
156
252
|
getDebugInfo() {
|
|
157
253
|
return {
|
|
@@ -164,6 +260,9 @@ export class ClickStrategy extends BaseStrategy {
|
|
|
164
260
|
},
|
|
165
261
|
inViewport: this.events.filter(e => e.inViewport).length,
|
|
166
262
|
trackedElements: this.clickListeners.size,
|
|
263
|
+
mouseClickDeltas: this.events.map(e => e.mouseClickDelta),
|
|
264
|
+
timeSinceMouseMoves: this.events.map(e => e.timeSinceMouseMove),
|
|
265
|
+
untrustedClicks: this.events.filter(e => !e.isTrusted).length,
|
|
167
266
|
};
|
|
168
267
|
}
|
|
169
268
|
}
|
|
@@ -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 {};
|
|
@@ -1,9 +1,35 @@
|
|
|
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';
|
|
6
7
|
import { inverseSigmoid, gaussian } from '../math-utils.js';
|
|
8
|
+
/**
|
|
9
|
+
* Automation framework globals that indicate bot activity
|
|
10
|
+
* These are injected by various testing/automation frameworks
|
|
11
|
+
*/
|
|
12
|
+
const AUTOMATION_GLOBALS = [
|
|
13
|
+
'__nightmare',
|
|
14
|
+
'_phantom',
|
|
15
|
+
'callPhantom',
|
|
16
|
+
'__selenium_evaluate',
|
|
17
|
+
'__webdriver_evaluate',
|
|
18
|
+
'__driver_evaluate',
|
|
19
|
+
'__webdriver_script_function',
|
|
20
|
+
'__lastWatirAlert',
|
|
21
|
+
'__lastWatirConfirm',
|
|
22
|
+
'__lastWatirPrompt',
|
|
23
|
+
'_Selenium_IDE_Recorder',
|
|
24
|
+
'domAutomation',
|
|
25
|
+
'domAutomationController',
|
|
26
|
+
'webdriver',
|
|
27
|
+
'_webdriver_script_fn',
|
|
28
|
+
'__webdriver_script_func',
|
|
29
|
+
'__fxdriver_evaluate',
|
|
30
|
+
'__fxdriver_unwrapped',
|
|
31
|
+
'__selenium_unwrapped',
|
|
32
|
+
];
|
|
7
33
|
export class EnvironmentStrategy extends BaseStrategy {
|
|
8
34
|
constructor() {
|
|
9
35
|
super(...arguments);
|
|
@@ -28,6 +54,40 @@ export class EnvironmentStrategy extends BaseStrategy {
|
|
|
28
54
|
if (!this.data)
|
|
29
55
|
return undefined;
|
|
30
56
|
const env = this.data;
|
|
57
|
+
const auto = env.automation;
|
|
58
|
+
// ============================================
|
|
59
|
+
// IMMEDIATE DISQUALIFICATION - HIGH CONFIDENCE BOT SIGNALS
|
|
60
|
+
// ============================================
|
|
61
|
+
// P0: navigator.webdriver === true (HIGHEST PRIORITY)
|
|
62
|
+
// This is the most reliable automation indicator
|
|
63
|
+
if (auto.isWebdriver) {
|
|
64
|
+
return 0.05;
|
|
65
|
+
}
|
|
66
|
+
// P0: Automation framework globals detected
|
|
67
|
+
if (auto.hasAutomationGlobals) {
|
|
68
|
+
return 0.1;
|
|
69
|
+
}
|
|
70
|
+
// P0: CDP injection detected (Chrome DevTools Protocol)
|
|
71
|
+
if (auto.hasCDPInjection) {
|
|
72
|
+
return 0.1;
|
|
73
|
+
}
|
|
74
|
+
// P2: Headless browser detection
|
|
75
|
+
// Multiple headless indicators = very likely bot
|
|
76
|
+
const headlessIndicatorCount = [
|
|
77
|
+
auto.hasHeadlessUA,
|
|
78
|
+
auto.hasChromelessRuntime,
|
|
79
|
+
auto.hasSoftwareRenderer,
|
|
80
|
+
].filter(Boolean).length;
|
|
81
|
+
if (headlessIndicatorCount >= 2) {
|
|
82
|
+
return 0.15;
|
|
83
|
+
}
|
|
84
|
+
// Single strong headless indicator
|
|
85
|
+
if (auto.hasHeadlessUA) {
|
|
86
|
+
return 0.2;
|
|
87
|
+
}
|
|
88
|
+
// ============================================
|
|
89
|
+
// WEIGHTED SCORING FOR OTHER SIGNALS
|
|
90
|
+
// ============================================
|
|
31
91
|
let score = 0;
|
|
32
92
|
let factors = 0;
|
|
33
93
|
// Suspicious indicators
|
|
@@ -44,10 +104,27 @@ export class EnvironmentStrategy extends BaseStrategy {
|
|
|
44
104
|
].filter(Boolean).length;
|
|
45
105
|
score += featureCount / 4;
|
|
46
106
|
factors++;
|
|
47
|
-
// Plugins
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
107
|
+
// Plugins (only suspicious on desktop)
|
|
108
|
+
if (!env.isMobile) {
|
|
109
|
+
score += inverseSigmoid(env.plugins, -2, -0.5);
|
|
110
|
+
score += (env.plugins > 0 ? 1.0 : 0.1);
|
|
111
|
+
factors += 2;
|
|
112
|
+
// No plugins on desktop Chrome = suspicious (but not conclusive)
|
|
113
|
+
if (auto.hasNoPlugins && env.vendor.includes('Google')) {
|
|
114
|
+
score += 0.3;
|
|
115
|
+
factors++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Chromeless runtime (window.chrome but no chrome.runtime)
|
|
119
|
+
if (auto.hasChromelessRuntime) {
|
|
120
|
+
score += 0.3;
|
|
121
|
+
factors++;
|
|
122
|
+
}
|
|
123
|
+
// Software renderer (SwiftShader)
|
|
124
|
+
if (auto.hasSoftwareRenderer) {
|
|
125
|
+
score += 0.4;
|
|
126
|
+
factors++;
|
|
127
|
+
}
|
|
51
128
|
// Device
|
|
52
129
|
score += gaussian(env.devicePixelRatio, 2, 1.5);
|
|
53
130
|
score += (env.colorDepth === 24 || env.colorDepth === 32) ? 1.0 : 0.4;
|
|
@@ -61,13 +138,92 @@ export class EnvironmentStrategy extends BaseStrategy {
|
|
|
61
138
|
const smallScreen = window.innerWidth < 768 && window.innerHeight < 1024;
|
|
62
139
|
return (hasTouchScreen && smallScreen) || mobileUA;
|
|
63
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Detect automation indicators
|
|
143
|
+
*/
|
|
144
|
+
detectAutomation() {
|
|
145
|
+
const isMobile = this.isMobileDevice();
|
|
146
|
+
// 1. navigator.webdriver - HIGHEST PRIORITY
|
|
147
|
+
const isWebdriver = navigator.webdriver === true;
|
|
148
|
+
// 2. Headless UA detection
|
|
149
|
+
const ua = navigator.userAgent;
|
|
150
|
+
const hasHeadlessUA = /HeadlessChrome|Headless/i.test(ua);
|
|
151
|
+
// 3. Chrome without runtime (headless Chrome indicator)
|
|
152
|
+
const win = window;
|
|
153
|
+
const hasChromelessRuntime = !!(win.chrome &&
|
|
154
|
+
typeof win.chrome === 'object' &&
|
|
155
|
+
!win.chrome.runtime);
|
|
156
|
+
// 4. Automation framework globals
|
|
157
|
+
const detectedGlobals = [];
|
|
158
|
+
for (const global of AUTOMATION_GLOBALS) {
|
|
159
|
+
if (global in win) {
|
|
160
|
+
detectedGlobals.push(global);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const hasAutomationGlobals = detectedGlobals.length > 0;
|
|
164
|
+
// 5. CDP (Chrome DevTools Protocol) injection detection
|
|
165
|
+
// CDP injects properties matching pattern: cdc_ or __cdc_
|
|
166
|
+
const detectedCDPKeys = [];
|
|
167
|
+
try {
|
|
168
|
+
for (const key of Object.keys(win)) {
|
|
169
|
+
if (/^cdc_|^__cdc_/.test(key)) {
|
|
170
|
+
detectedCDPKeys.push(key);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (_a) {
|
|
175
|
+
// Some environments throw on Object.keys(window)
|
|
176
|
+
}
|
|
177
|
+
const hasCDPInjection = detectedCDPKeys.length > 0;
|
|
178
|
+
// 6. No plugins on desktop (suspicious)
|
|
179
|
+
const hasNoPlugins = !isMobile && navigator.plugins.length === 0;
|
|
180
|
+
// 7. Software renderer detection (SwiftShader, ANGLE without GPU)
|
|
181
|
+
let hasSoftwareRenderer = false;
|
|
182
|
+
try {
|
|
183
|
+
const canvas = document.createElement('canvas');
|
|
184
|
+
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
185
|
+
if (gl) {
|
|
186
|
+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
187
|
+
if (debugInfo) {
|
|
188
|
+
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
189
|
+
// SwiftShader = headless Chrome software renderer
|
|
190
|
+
// ANGLE without real GPU = suspicious
|
|
191
|
+
hasSoftwareRenderer = /SwiftShader|Software|LLVMpipe/i.test(renderer);
|
|
192
|
+
// Store for debugging
|
|
193
|
+
this._webglRenderer = renderer;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (_b) {
|
|
198
|
+
// WebGL not available
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
isWebdriver,
|
|
202
|
+
hasHeadlessUA,
|
|
203
|
+
hasChromelessRuntime,
|
|
204
|
+
hasSoftwareRenderer,
|
|
205
|
+
hasNoPlugins,
|
|
206
|
+
hasAutomationGlobals,
|
|
207
|
+
detectedGlobals,
|
|
208
|
+
hasCDPInjection,
|
|
209
|
+
detectedCDPKeys,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
64
212
|
captureEnvironment() {
|
|
65
213
|
try {
|
|
66
214
|
// WebGL detection
|
|
67
215
|
let hasWebGL = false;
|
|
216
|
+
let webglRenderer;
|
|
68
217
|
try {
|
|
69
218
|
const canvas = document.createElement('canvas');
|
|
70
|
-
|
|
219
|
+
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
220
|
+
hasWebGL = !!gl;
|
|
221
|
+
if (gl) {
|
|
222
|
+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
223
|
+
if (debugInfo) {
|
|
224
|
+
webglRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
71
227
|
}
|
|
72
228
|
catch (e) {
|
|
73
229
|
hasWebGL = false;
|
|
@@ -97,6 +253,8 @@ export class EnvironmentStrategy extends BaseStrategy {
|
|
|
97
253
|
const featureInconsistency = (navigator.plugins.length === 0 && navigator.mimeTypes.length === 0) ||
|
|
98
254
|
!hasWebGL ||
|
|
99
255
|
!hasStorage;
|
|
256
|
+
// Detect automation indicators
|
|
257
|
+
const automation = this.detectAutomation();
|
|
100
258
|
this.data = {
|
|
101
259
|
screenWidth,
|
|
102
260
|
screenHeight,
|
|
@@ -123,10 +281,12 @@ export class EnvironmentStrategy extends BaseStrategy {
|
|
|
123
281
|
featureInconsistency,
|
|
124
282
|
isMobile,
|
|
125
283
|
timestamp: Date.now(),
|
|
284
|
+
automation,
|
|
285
|
+
webglRenderer,
|
|
126
286
|
};
|
|
127
287
|
}
|
|
128
|
-
catch (
|
|
129
|
-
|
|
288
|
+
catch (_a) {
|
|
289
|
+
// Silent fail - environment capture is best effort
|
|
130
290
|
}
|
|
131
291
|
}
|
|
132
292
|
getDebugInfo() {
|
|
@@ -141,4 +301,25 @@ export class EnvironmentStrategy extends BaseStrategy {
|
|
|
141
301
|
var _a, _b;
|
|
142
302
|
return (_b = (_a = this.data) === null || _a === void 0 ? void 0 : _a.isMobile) !== null && _b !== void 0 ? _b : null;
|
|
143
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Get automation detection results
|
|
306
|
+
* Returns null if environment hasn't been captured yet
|
|
307
|
+
*/
|
|
308
|
+
getAutomationIndicators() {
|
|
309
|
+
var _a, _b;
|
|
310
|
+
return (_b = (_a = this.data) === null || _a === void 0 ? void 0 : _a.automation) !== null && _b !== void 0 ? _b : null;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Quick check if any strong automation indicator is present
|
|
314
|
+
* Returns true if likely a bot, false if likely human, null if not checked yet
|
|
315
|
+
*/
|
|
316
|
+
isLikelyBot() {
|
|
317
|
+
if (!this.data)
|
|
318
|
+
return null;
|
|
319
|
+
const auto = this.data.automation;
|
|
320
|
+
return (auto.isWebdriver ||
|
|
321
|
+
auto.hasAutomationGlobals ||
|
|
322
|
+
auto.hasCDPInjection ||
|
|
323
|
+
auto.hasHeadlessUA);
|
|
324
|
+
}
|
|
144
325
|
}
|
|
@@ -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';
|
|
@@ -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
|
// Convenience: All strategies
|
|
13
15
|
export { MouseStrategy as Mouse } from './mouse.js';
|
|
14
16
|
export { ScrollStrategy as Scroll } from './scroll.js';
|
|
@@ -17,3 +19,5 @@ export { TapStrategy as Tap } from './tap.js';
|
|
|
17
19
|
export { KeyboardStrategy as Keyboard } from './keyboard.js';
|
|
18
20
|
export { EnvironmentStrategy as Environment } from './environment.js';
|
|
19
21
|
export { ResizeStrategy as Resize } from './resize.js';
|
|
22
|
+
export { TimingStrategy as Timing } from './timing.js';
|
|
23
|
+
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 {};
|