@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,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Scroll Behavior Detection Strategy
|
|
4
|
+
* Autonomous module managing scroll event listeners
|
|
5
|
+
* Tracks normalized scroll distance and velocity patterns
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.ScrollStrategy = void 0;
|
|
9
|
+
const strategy_1 = require("../strategy");
|
|
10
|
+
const math_utils_1 = require("../math-utils");
|
|
11
|
+
class ScrollStrategy extends strategy_1.BaseStrategy {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
super();
|
|
14
|
+
this.name = 'scroll';
|
|
15
|
+
this.defaultWeight = 0.15;
|
|
16
|
+
this.distanceSeries = [];
|
|
17
|
+
this.velocitySeries = [];
|
|
18
|
+
this.lastScrollY = null;
|
|
19
|
+
this.lastTimestamp = 0;
|
|
20
|
+
this.rollingWindowMs = 5000;
|
|
21
|
+
this.documentHeight = 1;
|
|
22
|
+
this.listener = null;
|
|
23
|
+
this.isActive = false;
|
|
24
|
+
if ((options === null || options === void 0 ? void 0 : options.rollingWindow) !== undefined)
|
|
25
|
+
this.rollingWindowMs = options.rollingWindow;
|
|
26
|
+
}
|
|
27
|
+
start() {
|
|
28
|
+
if (this.isActive)
|
|
29
|
+
return;
|
|
30
|
+
this.isActive = true;
|
|
31
|
+
// Calculate document height for normalization
|
|
32
|
+
this.documentHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, 1);
|
|
33
|
+
this.listener = (_e) => {
|
|
34
|
+
const scrollY = window.scrollY;
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
// Always update last position on first event
|
|
37
|
+
if (this.lastScrollY === null) {
|
|
38
|
+
this.lastScrollY = scrollY;
|
|
39
|
+
this.lastTimestamp = now;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const pixelDelta = scrollY - this.lastScrollY;
|
|
43
|
+
const deltaTime = now - this.lastTimestamp;
|
|
44
|
+
// Skip if no actual scroll happened
|
|
45
|
+
if (pixelDelta === 0) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Normalize scroll distance to document height (0-1 range)
|
|
49
|
+
const normalizedDistance = Math.abs(pixelDelta) / this.documentHeight;
|
|
50
|
+
// Record all scroll movements (no threshold - let pattern detection decide)
|
|
51
|
+
if (deltaTime > 0) {
|
|
52
|
+
const velocity = normalizedDistance / deltaTime; // normalized units per ms
|
|
53
|
+
// Store as time series
|
|
54
|
+
this.distanceSeries.push({ value: normalizedDistance, timestamp: now });
|
|
55
|
+
this.velocitySeries.push({ value: velocity, timestamp: now });
|
|
56
|
+
// Notify detector for confidence tracking
|
|
57
|
+
this.notifyEvent(Math.min(1, normalizedDistance * 10));
|
|
58
|
+
// Apply rolling window efficiently
|
|
59
|
+
const cutoff = now - this.rollingWindowMs;
|
|
60
|
+
while (this.distanceSeries.length > 0 && this.distanceSeries[0].timestamp < cutoff) {
|
|
61
|
+
this.distanceSeries.shift();
|
|
62
|
+
this.velocitySeries.shift();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
this.lastScrollY = scrollY;
|
|
66
|
+
this.lastTimestamp = now;
|
|
67
|
+
};
|
|
68
|
+
// Listen on window for scroll events (document scrolling)
|
|
69
|
+
window.addEventListener('scroll', this.listener, { passive: true });
|
|
70
|
+
}
|
|
71
|
+
stop() {
|
|
72
|
+
if (!this.isActive)
|
|
73
|
+
return;
|
|
74
|
+
this.isActive = false;
|
|
75
|
+
if (this.listener) {
|
|
76
|
+
window.removeEventListener('scroll', this.listener);
|
|
77
|
+
this.listener = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
reset() {
|
|
81
|
+
this.distanceSeries = [];
|
|
82
|
+
this.velocitySeries = [];
|
|
83
|
+
this.lastScrollY = null;
|
|
84
|
+
this.lastTimestamp = 0;
|
|
85
|
+
}
|
|
86
|
+
score() {
|
|
87
|
+
if (this.distanceSeries.length < 2)
|
|
88
|
+
return undefined;
|
|
89
|
+
// Scroll-specific pattern detection
|
|
90
|
+
return this.detectScrollPatterns();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Scroll-specific pattern detection
|
|
94
|
+
* Detects bot-like patterns: constant deltas, instant jumps, too smooth
|
|
95
|
+
*/
|
|
96
|
+
detectScrollPatterns() {
|
|
97
|
+
if (this.distanceSeries.length < 2)
|
|
98
|
+
return undefined;
|
|
99
|
+
let score = 0;
|
|
100
|
+
let factors = 0;
|
|
101
|
+
// 1. IDENTICAL DELTAS - Smoking gun for bots
|
|
102
|
+
const distances = this.distanceSeries.map(p => p.value);
|
|
103
|
+
if (distances.length >= 2) {
|
|
104
|
+
// Check if all distances are identical (tolerance for floating point)
|
|
105
|
+
const uniqueDistances = new Set(distances.map(d => Math.round(d * 1000))).size;
|
|
106
|
+
if (uniqueDistances === 1) {
|
|
107
|
+
// All scroll amounts identical = definite bot
|
|
108
|
+
return 0.05;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// 2. DISTANCE CONSISTENCY - Bots often scroll same amount repeatedly
|
|
112
|
+
if (distances.length >= 2) {
|
|
113
|
+
const stats = (0, math_utils_1.calculateStatistics)(distances);
|
|
114
|
+
// Real humans have high CV (>0.8) due to natural scroll acceleration/deceleration
|
|
115
|
+
// Simple automation has lower CV even with varied amounts
|
|
116
|
+
// Gaussian centered high with narrow width - be strict
|
|
117
|
+
score += (0, math_utils_1.gaussian)(stats.cv, 1.0, 0.4);
|
|
118
|
+
factors++;
|
|
119
|
+
}
|
|
120
|
+
// 3. VELOCITY VARIATION - Humans have highly variable scroll speeds
|
|
121
|
+
const velocities = this.velocitySeries.map(p => p.value);
|
|
122
|
+
if (velocities.length >= 2) {
|
|
123
|
+
const stats = (0, math_utils_1.calculateStatistics)(velocities);
|
|
124
|
+
// Human velocity is very chaotic (acceleration/deceleration)
|
|
125
|
+
// Automation has more consistent velocity even with delays
|
|
126
|
+
score += (0, math_utils_1.gaussian)(stats.cv, 1.2, 0.5);
|
|
127
|
+
factors++;
|
|
128
|
+
}
|
|
129
|
+
// 4. INSTANT JUMPS - Detect programmatic scrollTo
|
|
130
|
+
const instantJumps = distances.filter(d => d > 0.1).length; // >10% of document in one event
|
|
131
|
+
if (distances.length > 0) {
|
|
132
|
+
const jumpRatio = instantJumps / distances.length;
|
|
133
|
+
// Penalize high jump ratio (programmatic scrollTo)
|
|
134
|
+
score += (0, math_utils_1.inverseSigmoid)(jumpRatio, 0.3, 15);
|
|
135
|
+
factors++;
|
|
136
|
+
}
|
|
137
|
+
return factors > 0 ? score / factors : undefined;
|
|
138
|
+
}
|
|
139
|
+
getDebugInfo() {
|
|
140
|
+
return {
|
|
141
|
+
eventCount: this.distanceSeries.length,
|
|
142
|
+
rollingWindow: this.rollingWindowMs,
|
|
143
|
+
isActive: this.isActive,
|
|
144
|
+
distanceSeries: this.distanceSeries,
|
|
145
|
+
velocitySeries: this.velocitySeries,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
exports.ScrollStrategy = ScrollStrategy;
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
export declare class TapStrategy extends BaseStrategy {
|
|
8
|
+
readonly name = "tap";
|
|
9
|
+
readonly defaultWeight = 0.35;
|
|
10
|
+
private events;
|
|
11
|
+
private targetSelectors;
|
|
12
|
+
private touchStartListeners;
|
|
13
|
+
private touchEndListeners;
|
|
14
|
+
private activeTouches;
|
|
15
|
+
private isActive;
|
|
16
|
+
constructor(options?: {
|
|
17
|
+
targetSelectors?: string[];
|
|
18
|
+
});
|
|
19
|
+
start(): void;
|
|
20
|
+
addTarget(selector: string): void;
|
|
21
|
+
private attachTouchListeners;
|
|
22
|
+
private attachTouchListenersForSelector;
|
|
23
|
+
stop(): void;
|
|
24
|
+
reset(): void;
|
|
25
|
+
score(): number | undefined;
|
|
26
|
+
getDebugInfo(): {
|
|
27
|
+
eventCount: number;
|
|
28
|
+
positions: {
|
|
29
|
+
outside: number;
|
|
30
|
+
deadCenter: number;
|
|
31
|
+
overElement: number;
|
|
32
|
+
};
|
|
33
|
+
durations: number[];
|
|
34
|
+
movements: number[];
|
|
35
|
+
inViewport: number;
|
|
36
|
+
trackedElements: number;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tap Behavior Detection Strategy (Mobile)
|
|
4
|
+
* Monitors touch interactions on specific elements
|
|
5
|
+
* Analyzes tap duration, precision, movement, and timing patterns
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.TapStrategy = void 0;
|
|
9
|
+
const strategy_1 = require("../strategy");
|
|
10
|
+
const math_utils_1 = require("../math-utils");
|
|
11
|
+
class TapStrategy extends strategy_1.BaseStrategy {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
super();
|
|
14
|
+
this.name = 'tap';
|
|
15
|
+
this.defaultWeight = 0.35;
|
|
16
|
+
this.events = [];
|
|
17
|
+
this.targetSelectors = ['button', 'a', 'input[type="submit"]', '[role="button"]'];
|
|
18
|
+
this.touchStartListeners = new Map();
|
|
19
|
+
this.touchEndListeners = new Map();
|
|
20
|
+
this.activeTouches = new Map();
|
|
21
|
+
this.isActive = false;
|
|
22
|
+
if (options === null || options === void 0 ? void 0 : options.targetSelectors) {
|
|
23
|
+
this.targetSelectors = options.targetSelectors;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
start() {
|
|
27
|
+
if (this.isActive)
|
|
28
|
+
return;
|
|
29
|
+
this.isActive = true;
|
|
30
|
+
this.attachTouchListeners();
|
|
31
|
+
}
|
|
32
|
+
addTarget(selector) {
|
|
33
|
+
if (!this.targetSelectors.includes(selector)) {
|
|
34
|
+
this.targetSelectors.push(selector);
|
|
35
|
+
if (this.isActive) {
|
|
36
|
+
this.attachTouchListenersForSelector(selector);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
attachTouchListeners() {
|
|
41
|
+
this.targetSelectors.forEach(selector => {
|
|
42
|
+
this.attachTouchListenersForSelector(selector);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
attachTouchListenersForSelector(selector) {
|
|
46
|
+
const elements = document.querySelectorAll(selector);
|
|
47
|
+
elements.forEach(element => {
|
|
48
|
+
if (this.touchStartListeners.has(element))
|
|
49
|
+
return;
|
|
50
|
+
const startListener = (e) => {
|
|
51
|
+
const touchEvent = e;
|
|
52
|
+
// Only track first touch (ignore multi-touch)
|
|
53
|
+
if (touchEvent.touches.length !== 1)
|
|
54
|
+
return;
|
|
55
|
+
const touch = touchEvent.touches[0];
|
|
56
|
+
this.activeTouches.set(touch.identifier, {
|
|
57
|
+
x: touch.clientX,
|
|
58
|
+
y: touch.clientY,
|
|
59
|
+
timestamp: Date.now(),
|
|
60
|
+
element,
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
const endListener = (e) => {
|
|
64
|
+
const touchEvent = e;
|
|
65
|
+
if (touchEvent.changedTouches.length !== 1)
|
|
66
|
+
return;
|
|
67
|
+
const touch = touchEvent.changedTouches[0];
|
|
68
|
+
const startData = this.activeTouches.get(touch.identifier);
|
|
69
|
+
if (!startData || startData.element !== element)
|
|
70
|
+
return;
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
const duration = now - startData.timestamp;
|
|
73
|
+
const dx = touch.clientX - startData.x;
|
|
74
|
+
const dy = touch.clientY - startData.y;
|
|
75
|
+
const movement = Math.sqrt(dx * dx + dy * dy);
|
|
76
|
+
const rect = element.getBoundingClientRect();
|
|
77
|
+
const inViewport = (rect.top >= 0 &&
|
|
78
|
+
rect.left >= 0 &&
|
|
79
|
+
rect.bottom <= window.innerHeight &&
|
|
80
|
+
rect.right <= window.innerWidth);
|
|
81
|
+
// Analyze tap position
|
|
82
|
+
const tx = touch.clientX;
|
|
83
|
+
const ty = touch.clientY;
|
|
84
|
+
const overElement = (tx >= rect.left &&
|
|
85
|
+
tx <= rect.right &&
|
|
86
|
+
ty >= rect.top &&
|
|
87
|
+
ty <= rect.bottom);
|
|
88
|
+
let position;
|
|
89
|
+
if (!overElement) {
|
|
90
|
+
position = 'outside';
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const centerX = rect.left + rect.width / 2;
|
|
94
|
+
const centerY = rect.top + rect.height / 2;
|
|
95
|
+
const distanceFromCenter = Math.sqrt((tx - centerX) ** 2 + (ty - centerY) ** 2);
|
|
96
|
+
// Dead center = within 2px
|
|
97
|
+
position = distanceFromCenter < 2 ? 'dead-center' : 'over-element';
|
|
98
|
+
}
|
|
99
|
+
this.events.push({
|
|
100
|
+
x: tx,
|
|
101
|
+
y: ty,
|
|
102
|
+
rect,
|
|
103
|
+
inViewport,
|
|
104
|
+
position,
|
|
105
|
+
duration,
|
|
106
|
+
movement,
|
|
107
|
+
timestamp: now,
|
|
108
|
+
});
|
|
109
|
+
// Notify detector - taps are high-value events
|
|
110
|
+
this.notifyEvent(1.0);
|
|
111
|
+
this.activeTouches.delete(touch.identifier);
|
|
112
|
+
};
|
|
113
|
+
element.addEventListener('touchstart', startListener, { passive: true });
|
|
114
|
+
element.addEventListener('touchend', endListener, { passive: true });
|
|
115
|
+
this.touchStartListeners.set(element, startListener);
|
|
116
|
+
this.touchEndListeners.set(element, endListener);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
stop() {
|
|
120
|
+
if (!this.isActive)
|
|
121
|
+
return;
|
|
122
|
+
this.isActive = false;
|
|
123
|
+
this.touchStartListeners.forEach((listener, element) => {
|
|
124
|
+
element.removeEventListener('touchstart', listener);
|
|
125
|
+
});
|
|
126
|
+
this.touchStartListeners.clear();
|
|
127
|
+
this.touchEndListeners.forEach((listener, element) => {
|
|
128
|
+
element.removeEventListener('touchend', listener);
|
|
129
|
+
});
|
|
130
|
+
this.touchEndListeners.clear();
|
|
131
|
+
this.activeTouches.clear();
|
|
132
|
+
}
|
|
133
|
+
reset() {
|
|
134
|
+
this.events = [];
|
|
135
|
+
this.activeTouches.clear();
|
|
136
|
+
}
|
|
137
|
+
score() {
|
|
138
|
+
if (this.events.length === 0)
|
|
139
|
+
return undefined;
|
|
140
|
+
let score = 0;
|
|
141
|
+
let factors = 0;
|
|
142
|
+
// 1. Position accuracy
|
|
143
|
+
let positionScore = 0;
|
|
144
|
+
for (const tap of this.events) {
|
|
145
|
+
switch (tap.position) {
|
|
146
|
+
case 'outside':
|
|
147
|
+
positionScore += 0.0; // Bot
|
|
148
|
+
break;
|
|
149
|
+
case 'dead-center':
|
|
150
|
+
positionScore += 0.5; // Suspicious
|
|
151
|
+
break;
|
|
152
|
+
case 'over-element':
|
|
153
|
+
positionScore += 1.0; // Human
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
score += positionScore / this.events.length;
|
|
158
|
+
factors++;
|
|
159
|
+
// 2. Tap duration variation - Humans have natural variation (50-150ms typical)
|
|
160
|
+
const durations = this.events.map(e => e.duration);
|
|
161
|
+
if (durations.length >= 2) {
|
|
162
|
+
const durationAnalysis = (0, math_utils_1.analyzeIntervals)(durations);
|
|
163
|
+
if (durationAnalysis) {
|
|
164
|
+
const { statistics, allIdentical } = durationAnalysis;
|
|
165
|
+
// All identical = bot
|
|
166
|
+
if (allIdentical) {
|
|
167
|
+
return 0.05;
|
|
168
|
+
}
|
|
169
|
+
// Human tap durations have moderate CV (~0.3-0.5)
|
|
170
|
+
score += (0, math_utils_1.scoreCoefficientOfVariation)(statistics.cv);
|
|
171
|
+
factors++;
|
|
172
|
+
// Ideal duration: 70-120ms
|
|
173
|
+
score += (0, math_utils_1.gaussian)(statistics.mean, 95, 40);
|
|
174
|
+
factors++;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// 3. Tap movement - Real fingers move slightly (1-5px), bots are often 0px
|
|
178
|
+
const movements = this.events.map(e => e.movement);
|
|
179
|
+
if (movements.length > 0) {
|
|
180
|
+
const avgMovement = movements.reduce((a, b) => a + b, 0) / movements.length;
|
|
181
|
+
// Some movement is natural (1-5px), too much is a swipe, zero is suspicious
|
|
182
|
+
score += (0, math_utils_1.gaussian)(avgMovement, 2, 3);
|
|
183
|
+
factors++;
|
|
184
|
+
}
|
|
185
|
+
// 4. Tap interval timing - Time between taps should vary naturally
|
|
186
|
+
if (this.events.length >= 3) {
|
|
187
|
+
const intervals = [];
|
|
188
|
+
for (let i = 1; i < this.events.length; i++) {
|
|
189
|
+
intervals.push(this.events[i].timestamp - this.events[i - 1].timestamp);
|
|
190
|
+
}
|
|
191
|
+
const intervalAnalysis = (0, math_utils_1.analyzeIntervals)(intervals);
|
|
192
|
+
if (intervalAnalysis && !intervalAnalysis.allIdentical) {
|
|
193
|
+
score += (0, math_utils_1.scoreCoefficientOfVariation)(intervalAnalysis.statistics.cv);
|
|
194
|
+
factors++;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return factors > 0 ? score / factors : undefined;
|
|
198
|
+
}
|
|
199
|
+
getDebugInfo() {
|
|
200
|
+
return {
|
|
201
|
+
eventCount: this.events.length,
|
|
202
|
+
positions: {
|
|
203
|
+
outside: this.events.filter(e => e.position === 'outside').length,
|
|
204
|
+
deadCenter: this.events.filter(e => e.position === 'dead-center').length,
|
|
205
|
+
overElement: this.events.filter(e => e.position === 'over-element').length,
|
|
206
|
+
},
|
|
207
|
+
durations: this.events.map(e => e.duration),
|
|
208
|
+
movements: this.events.map(e => e.movement),
|
|
209
|
+
inViewport: this.events.filter(e => e.inViewport).length,
|
|
210
|
+
trackedElements: this.touchStartListeners.size,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
exports.TapStrategy = TapStrategy;
|
|
@@ -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,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Detection Strategy Interface
|
|
4
|
+
* Each strategy is a fully autonomous module responsible for:
|
|
5
|
+
* - Registering its own event listeners
|
|
6
|
+
* - Managing its own state and events
|
|
7
|
+
* - Responding to lifecycle events (start/stop)
|
|
8
|
+
* - Optionally receiving tick updates for polling-based detection
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.BaseStrategy = void 0;
|
|
12
|
+
/**
|
|
13
|
+
* Base class for detection strategies
|
|
14
|
+
* Handles event callback pattern to avoid repetition
|
|
15
|
+
*/
|
|
16
|
+
class BaseStrategy {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.eventCallback = null;
|
|
19
|
+
}
|
|
20
|
+
setEventCallback(callback) {
|
|
21
|
+
this.eventCallback = callback;
|
|
22
|
+
}
|
|
23
|
+
notifyEvent(weight) {
|
|
24
|
+
if (this.eventCallback) {
|
|
25
|
+
this.eventCallback({
|
|
26
|
+
strategy: this.name,
|
|
27
|
+
weight,
|
|
28
|
+
timestamp: Date.now()
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.BaseStrategy = BaseStrategy;
|