@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.
Files changed (76) hide show
  1. package/README.md +828 -0
  2. package/dist/behavior-detection.esm.min.js +2 -0
  3. package/dist/behavior-detection.esm.min.js.map +7 -0
  4. package/dist/behavior-detection.min.js +2 -0
  5. package/dist/behavior-detection.min.js.map +7 -0
  6. package/dist/behavior-detector.d.ts +102 -0
  7. package/dist/browser.d.ts +33 -0
  8. package/dist/cjs/behavior-detector.d.ts +102 -0
  9. package/dist/cjs/behavior-detector.js +315 -0
  10. package/dist/cjs/browser.d.ts +33 -0
  11. package/dist/cjs/browser.js +226 -0
  12. package/dist/cjs/index.d.ts +38 -0
  13. package/dist/cjs/index.js +55 -0
  14. package/dist/cjs/math-utils.d.ts +84 -0
  15. package/dist/cjs/math-utils.js +141 -0
  16. package/dist/cjs/strategies/click.d.ts +39 -0
  17. package/dist/cjs/strategies/click.js +173 -0
  18. package/dist/cjs/strategies/environment.d.ts +52 -0
  19. package/dist/cjs/strategies/environment.js +148 -0
  20. package/dist/cjs/strategies/index.d.ts +18 -0
  21. package/dist/cjs/strategies/index.js +36 -0
  22. package/dist/cjs/strategies/keyboard.d.ts +43 -0
  23. package/dist/cjs/strategies/keyboard.js +233 -0
  24. package/dist/cjs/strategies/mouse.d.ts +39 -0
  25. package/dist/cjs/strategies/mouse.js +159 -0
  26. package/dist/cjs/strategies/resize.d.ts +21 -0
  27. package/dist/cjs/strategies/resize.js +97 -0
  28. package/dist/cjs/strategies/scroll.d.ts +37 -0
  29. package/dist/cjs/strategies/scroll.js +149 -0
  30. package/dist/cjs/strategies/tap.d.ts +38 -0
  31. package/dist/cjs/strategies/tap.js +214 -0
  32. package/dist/cjs/strategy.d.ts +107 -0
  33. package/dist/cjs/strategy.js +33 -0
  34. package/dist/cjs/types.d.ts +168 -0
  35. package/dist/cjs/types.js +26 -0
  36. package/dist/esm/behavior-detector.d.ts +102 -0
  37. package/dist/esm/behavior-detector.js +311 -0
  38. package/dist/esm/browser.d.ts +33 -0
  39. package/dist/esm/browser.js +224 -0
  40. package/dist/esm/index.d.ts +38 -0
  41. package/dist/esm/index.js +36 -0
  42. package/dist/esm/math-utils.d.ts +84 -0
  43. package/dist/esm/math-utils.js +127 -0
  44. package/dist/esm/strategies/click.d.ts +39 -0
  45. package/dist/esm/strategies/click.js +169 -0
  46. package/dist/esm/strategies/environment.d.ts +52 -0
  47. package/dist/esm/strategies/environment.js +144 -0
  48. package/dist/esm/strategies/index.d.ts +18 -0
  49. package/dist/esm/strategies/index.js +19 -0
  50. package/dist/esm/strategies/keyboard.d.ts +43 -0
  51. package/dist/esm/strategies/keyboard.js +229 -0
  52. package/dist/esm/strategies/mouse.d.ts +39 -0
  53. package/dist/esm/strategies/mouse.js +155 -0
  54. package/dist/esm/strategies/resize.d.ts +21 -0
  55. package/dist/esm/strategies/resize.js +93 -0
  56. package/dist/esm/strategies/scroll.d.ts +37 -0
  57. package/dist/esm/strategies/scroll.js +145 -0
  58. package/dist/esm/strategies/tap.d.ts +38 -0
  59. package/dist/esm/strategies/tap.js +210 -0
  60. package/dist/esm/strategy.d.ts +107 -0
  61. package/dist/esm/strategy.js +29 -0
  62. package/dist/esm/types.d.ts +168 -0
  63. package/dist/esm/types.js +23 -0
  64. package/dist/index.d.ts +38 -0
  65. package/dist/math-utils.d.ts +84 -0
  66. package/dist/strategies/click.d.ts +39 -0
  67. package/dist/strategies/environment.d.ts +52 -0
  68. package/dist/strategies/index.d.ts +18 -0
  69. package/dist/strategies/keyboard.d.ts +43 -0
  70. package/dist/strategies/mouse.d.ts +39 -0
  71. package/dist/strategies/resize.d.ts +21 -0
  72. package/dist/strategies/scroll.d.ts +37 -0
  73. package/dist/strategies/tap.d.ts +38 -0
  74. package/dist/strategy.d.ts +107 -0
  75. package/dist/types.d.ts +168 -0
  76. 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
+ }