@niksbanna/bot-detector 1.0.2 → 1.0.4

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 (38) hide show
  1. package/README.md +22 -0
  2. package/dist/bot-detector.cjs.js +115 -75
  3. package/dist/bot-detector.cjs.js.map +2 -2
  4. package/dist/bot-detector.esm.js +115 -75
  5. package/dist/bot-detector.esm.js.map +2 -2
  6. package/dist/bot-detector.iife.js +115 -75
  7. package/dist/bot-detector.iife.js.map +2 -2
  8. package/dist/bot-detector.iife.min.js +3 -1
  9. package/package.json +1 -2
  10. package/src/core/BotDetector.js +0 -284
  11. package/src/core/ScoringEngine.js +0 -134
  12. package/src/core/Signal.js +0 -181
  13. package/src/core/VerdictEngine.js +0 -132
  14. package/src/index.js +0 -273
  15. package/src/signals/automation/PhantomJSSignal.js +0 -137
  16. package/src/signals/automation/PlaywrightSignal.js +0 -129
  17. package/src/signals/automation/PuppeteerSignal.js +0 -122
  18. package/src/signals/automation/SeleniumSignal.js +0 -151
  19. package/src/signals/automation/index.js +0 -8
  20. package/src/signals/behavior/InteractionTimingSignal.js +0 -170
  21. package/src/signals/behavior/KeyboardPatternSignal.js +0 -235
  22. package/src/signals/behavior/MouseMovementSignal.js +0 -215
  23. package/src/signals/behavior/ScrollBehaviorSignal.js +0 -236
  24. package/src/signals/behavior/index.js +0 -8
  25. package/src/signals/environment/HeadlessSignal.js +0 -97
  26. package/src/signals/environment/NavigatorAnomalySignal.js +0 -117
  27. package/src/signals/environment/PermissionsSignal.js +0 -76
  28. package/src/signals/environment/WebDriverSignal.js +0 -58
  29. package/src/signals/environment/index.js +0 -8
  30. package/src/signals/fingerprint/AudioContextSignal.js +0 -158
  31. package/src/signals/fingerprint/CanvasSignal.js +0 -133
  32. package/src/signals/fingerprint/PluginsSignal.js +0 -106
  33. package/src/signals/fingerprint/ScreenSignal.js +0 -157
  34. package/src/signals/fingerprint/WebGLSignal.js +0 -146
  35. package/src/signals/fingerprint/index.js +0 -9
  36. package/src/signals/timing/DOMContentTimingSignal.js +0 -159
  37. package/src/signals/timing/PageLoadSignal.js +0 -165
  38. package/src/signals/timing/index.js +0 -6
@@ -1,170 +0,0 @@
1
- /**
2
- * @fileoverview Detects suspicious interaction timing patterns.
3
- */
4
-
5
- import { Signal } from '../../core/Signal.js';
6
-
7
- /**
8
- * Measures timing between page load and first interaction.
9
- * Bots often interact too fast or with perfect timing patterns.
10
- */
11
- class InteractionTimingSignal extends Signal {
12
- static id = 'interaction-timing';
13
- static category = 'behavior';
14
- static weight = 0.6;
15
- static description = 'Detects suspicious interaction timing';
16
- static requiresInteraction = true;
17
-
18
- constructor(options = {}) {
19
- super(options);
20
- this._pageLoadTime = performance.now();
21
- this._firstInteractionTime = null;
22
- this._interactions = [];
23
- this._isTracking = false;
24
- this._trackingDuration = options.trackingDuration || 5000;
25
- this._boundHandler = null;
26
- }
27
-
28
- /**
29
- * Start tracking interactions.
30
- */
31
- startTracking() {
32
- if (this._isTracking) return;
33
-
34
- this._interactions = [];
35
- this._isTracking = true;
36
-
37
- const interactionEvents = ['click', 'mousedown', 'touchstart', 'keydown', 'scroll'];
38
-
39
- this._boundHandler = (e) => {
40
- const now = performance.now();
41
- if (this._firstInteractionTime === null) {
42
- this._firstInteractionTime = now;
43
- }
44
- this._interactions.push({
45
- type: e.type,
46
- t: now,
47
- timeSinceLoad: now - this._pageLoadTime,
48
- });
49
- };
50
-
51
- for (const event of interactionEvents) {
52
- document.addEventListener(event, this._boundHandler, { passive: true, capture: true });
53
- }
54
- }
55
-
56
- /**
57
- * Stop tracking interactions.
58
- */
59
- stopTracking() {
60
- if (!this._isTracking) return;
61
-
62
- this._isTracking = false;
63
- const interactionEvents = ['click', 'mousedown', 'touchstart', 'keydown', 'scroll'];
64
-
65
- if (this._boundHandler) {
66
- for (const event of interactionEvents) {
67
- document.removeEventListener(event, this._boundHandler, { capture: true });
68
- }
69
- this._boundHandler = null;
70
- }
71
- }
72
-
73
- async detect() {
74
- const anomalies = [];
75
- let confidence = 0;
76
-
77
- // Start tracking if not already
78
- if (!this._isTracking && this._interactions.length === 0) {
79
- this.startTracking();
80
- await new Promise(resolve => setTimeout(resolve, this._trackingDuration));
81
- this.stopTracking();
82
- }
83
-
84
- const interactions = this._interactions;
85
-
86
- // No interactions - not necessarily suspicious, could be passive viewing
87
- if (interactions.length === 0) {
88
- return this.createResult(false, {
89
- reason: 'no-interactions',
90
- }, 0);
91
- }
92
-
93
- // Check time to first interaction
94
- const firstInteraction = interactions[0];
95
-
96
- // Suspiciously fast first interaction (< 100ms after page load)
97
- if (firstInteraction.timeSinceLoad < 100) {
98
- anomalies.push('instant-interaction');
99
- confidence = Math.max(confidence, 0.9);
100
- }
101
- // Very fast interaction (< 300ms)
102
- else if (firstInteraction.timeSinceLoad < 300) {
103
- anomalies.push('very-fast-interaction');
104
- confidence = Math.max(confidence, 0.6);
105
- }
106
-
107
- // Analyze interaction intervals
108
- if (interactions.length > 3) {
109
- const intervals = [];
110
- for (let i = 1; i < interactions.length; i++) {
111
- intervals.push(interactions[i].t - interactions[i - 1].t);
112
- }
113
-
114
- const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
115
- const variance = intervals.reduce((acc, t) =>
116
- acc + Math.pow(t - avgInterval, 2), 0) / intervals.length;
117
-
118
- // Perfectly timed intervals (robotic)
119
- if (variance < 10 && interactions.length > 5) {
120
- anomalies.push('robotic-intervals');
121
- confidence = Math.max(confidence, 0.8);
122
- }
123
-
124
- // Check for burst interactions (many in short time)
125
- const burstThreshold = 50; // ms
126
- let burstCount = 0;
127
- for (const interval of intervals) {
128
- if (interval < burstThreshold) burstCount++;
129
- }
130
- if (burstCount > intervals.length * 0.7) {
131
- anomalies.push('burst-interactions');
132
- confidence = Math.max(confidence, 0.7);
133
- }
134
- }
135
-
136
- // Check interaction sequence (bots often follow predictable patterns)
137
- const typeSequence = interactions.map(i => i.type).join(',');
138
-
139
- // Repeated identical sequences
140
- if (interactions.length >= 6) {
141
- const halfLength = Math.floor(interactions.length / 2);
142
- const firstHalf = interactions.slice(0, halfLength).map(i => i.type).join(',');
143
- const secondHalf = interactions.slice(halfLength, halfLength * 2).map(i => i.type).join(',');
144
-
145
- if (firstHalf === secondHalf && firstHalf.length > 0) {
146
- anomalies.push('repeated-sequence');
147
- confidence = Math.max(confidence, 0.6);
148
- }
149
- }
150
-
151
- const triggered = anomalies.length > 0;
152
-
153
- return this.createResult(triggered, {
154
- anomalies,
155
- interactionCount: interactions.length,
156
- timeToFirstInteraction: firstInteraction.timeSinceLoad,
157
- firstInteractionType: firstInteraction.type,
158
- }, confidence);
159
- }
160
-
161
- reset() {
162
- super.reset();
163
- this.stopTracking();
164
- this._pageLoadTime = performance.now();
165
- this._firstInteractionTime = null;
166
- this._interactions = [];
167
- }
168
- }
169
-
170
- export { InteractionTimingSignal };
@@ -1,235 +0,0 @@
1
- /**
2
- * @fileoverview Detects non-human keyboard input patterns.
3
- */
4
-
5
- import { Signal } from '../../core/Signal.js';
6
-
7
- /**
8
- * Analyzes keystroke timing patterns.
9
- * Bots often type with unnatural consistency or inhuman speeds.
10
- */
11
- class KeyboardPatternSignal extends Signal {
12
- static id = 'keyboard-pattern';
13
- static category = 'behavior';
14
- static weight = 0.8;
15
- static description = 'Detects non-human keystroke patterns';
16
- static requiresInteraction = true;
17
-
18
- constructor(options = {}) {
19
- super(options);
20
- this._keystrokes = [];
21
- this._isTracking = false;
22
- this._trackingDuration = options.trackingDuration || 5000;
23
- this._minKeystrokes = options.minKeystrokes || 10;
24
- this._boundKeydownHandler = null;
25
- this._boundKeyupHandler = null;
26
- }
27
-
28
- /**
29
- * Start tracking keyboard events.
30
- */
31
- startTracking() {
32
- if (this._isTracking) return;
33
-
34
- this._keystrokes = [];
35
- this._isTracking = true;
36
-
37
- this._boundKeydownHandler = (e) => {
38
- this._keystrokes.push({
39
- type: 'down',
40
- key: e.key,
41
- code: e.code,
42
- t: performance.now(),
43
- });
44
- };
45
-
46
- this._boundKeyupHandler = (e) => {
47
- this._keystrokes.push({
48
- type: 'up',
49
- key: e.key,
50
- code: e.code,
51
- t: performance.now(),
52
- });
53
- };
54
-
55
- document.addEventListener('keydown', this._boundKeydownHandler, { passive: true });
56
- document.addEventListener('keyup', this._boundKeyupHandler, { passive: true });
57
- }
58
-
59
- /**
60
- * Stop tracking keyboard events.
61
- */
62
- stopTracking() {
63
- if (!this._isTracking) return;
64
-
65
- this._isTracking = false;
66
- if (this._boundKeydownHandler) {
67
- document.removeEventListener('keydown', this._boundKeydownHandler);
68
- this._boundKeydownHandler = null;
69
- }
70
- if (this._boundKeyupHandler) {
71
- document.removeEventListener('keyup', this._boundKeyupHandler);
72
- this._boundKeyupHandler = null;
73
- }
74
- }
75
-
76
- async detect() {
77
- const anomalies = [];
78
- let confidence = 0;
79
-
80
- // If no tracking has occurred, start it
81
- if (this._keystrokes.length === 0) {
82
- this.startTracking();
83
- await new Promise(resolve => setTimeout(resolve, this._trackingDuration));
84
- this.stopTracking();
85
- }
86
-
87
- const keystrokes = this._keystrokes;
88
- const keydowns = keystrokes.filter(k => k.type === 'down');
89
-
90
- // No keyboard activity
91
- if (keydowns.length < this._minKeystrokes) {
92
- // Not necessarily a bot - could just be no typing needed
93
- return this.createResult(false, {
94
- reason: 'insufficient-data',
95
- keystrokes: keydowns.length
96
- }, 0);
97
- }
98
-
99
- const analysis = this._analyzeKeystrokes(keystrokes);
100
-
101
- // Check for inhuman typing speed (> 20 chars/second sustained)
102
- if (analysis.avgInterKeystrokeTime < 50 && keydowns.length > 20) {
103
- anomalies.push('inhuman-speed');
104
- confidence = Math.max(confidence, 0.9);
105
- }
106
-
107
- // Check for too-consistent timing (robotic)
108
- if (analysis.timingVariance < 5 && keydowns.length > 15) {
109
- anomalies.push('robotic-timing');
110
- confidence = Math.max(confidence, 0.8);
111
- }
112
-
113
- // Check for missing key-up events (programmatic input)
114
- if (analysis.missingKeyups > keydowns.length * 0.5) {
115
- anomalies.push('missing-keyups');
116
- confidence = Math.max(confidence, 0.7);
117
- }
118
-
119
- // Check for perfect key hold times
120
- if (analysis.holdTimeVariance < 2 && analysis.holdTimes.length > 10) {
121
- anomalies.push('constant-hold-time');
122
- confidence = Math.max(confidence, 0.6);
123
- }
124
-
125
- // Check for sequential key codes (batch input)
126
- if (analysis.sequentialKeys > keydowns.length * 0.8 && keydowns.length > 10) {
127
- anomalies.push('sequential-input');
128
- confidence = Math.max(confidence, 0.5);
129
- }
130
-
131
- // Check for no typing rhythm variation
132
- if (analysis.rhythmScore < 0.1 && keydowns.length > 20) {
133
- anomalies.push('no-rhythm-variation');
134
- confidence = Math.max(confidence, 0.6);
135
- }
136
-
137
- const triggered = anomalies.length > 0;
138
-
139
- return this.createResult(triggered, {
140
- anomalies,
141
- keystrokeCount: keydowns.length,
142
- analysis,
143
- }, confidence);
144
- }
145
-
146
- /**
147
- * Analyze keystroke patterns.
148
- * @param {Array} keystrokes - Array of keystroke events
149
- * @returns {Object} Analysis results
150
- */
151
- _analyzeKeystrokes(keystrokes) {
152
- const keydowns = keystrokes.filter(k => k.type === 'down');
153
- const keyups = keystrokes.filter(k => k.type === 'up');
154
-
155
- if (keydowns.length < 2) {
156
- return {
157
- avgInterKeystrokeTime: Infinity,
158
- timingVariance: Infinity,
159
- missingKeyups: 0,
160
- holdTimeVariance: Infinity,
161
- holdTimes: [],
162
- sequentialKeys: 0,
163
- rhythmScore: 1,
164
- };
165
- }
166
-
167
- // Calculate inter-keystroke times
168
- const interTimes = [];
169
- for (let i = 1; i < keydowns.length; i++) {
170
- interTimes.push(keydowns[i].t - keydowns[i - 1].t);
171
- }
172
-
173
- const avgInterKeystrokeTime = interTimes.reduce((a, b) => a + b, 0) / interTimes.length;
174
- const timingVariance = interTimes.reduce((acc, t) =>
175
- acc + Math.pow(t - avgInterKeystrokeTime, 2), 0) / interTimes.length;
176
-
177
- // Calculate hold times (time between keydown and keyup for same key)
178
- const holdTimes = [];
179
- for (const down of keydowns) {
180
- const up = keyups.find(u => u.key === down.key && u.t > down.t);
181
- if (up) {
182
- holdTimes.push(up.t - down.t);
183
- }
184
- }
185
-
186
- const avgHoldTime = holdTimes.length > 0
187
- ? holdTimes.reduce((a, b) => a + b, 0) / holdTimes.length
188
- : 0;
189
- const holdTimeVariance = holdTimes.length > 0
190
- ? holdTimes.reduce((acc, t) => acc + Math.pow(t - avgHoldTime, 2), 0) / holdTimes.length
191
- : Infinity;
192
-
193
- // Count missing keyups
194
- const missingKeyups = keydowns.length - holdTimes.length;
195
-
196
- // Count sequential keys (chars typed in order, like 'abc' or '123')
197
- let sequentialKeys = 0;
198
- for (let i = 1; i < keydowns.length; i++) {
199
- const prevCode = keydowns[i - 1].key.charCodeAt(0);
200
- const currCode = keydowns[i].key.charCodeAt(0);
201
- if (Math.abs(currCode - prevCode) === 1) {
202
- sequentialKeys++;
203
- }
204
- }
205
-
206
- // Calculate typing rhythm score (variation in timing patterns)
207
- // Humans have natural rhythm variations (pause after words, faster for common patterns)
208
- let rhythmScore = 0;
209
- if (interTimes.length > 5) {
210
- const sortedTimes = [...interTimes].sort((a, b) => a - b);
211
- const median = sortedTimes[Math.floor(sortedTimes.length / 2)];
212
- // Count how many timings deviate significantly from median
213
- const deviations = interTimes.filter(t => Math.abs(t - median) > median * 0.3).length;
214
- rhythmScore = deviations / interTimes.length;
215
- }
216
-
217
- return {
218
- avgInterKeystrokeTime,
219
- timingVariance,
220
- missingKeyups,
221
- holdTimeVariance,
222
- holdTimes,
223
- sequentialKeys,
224
- rhythmScore,
225
- };
226
- }
227
-
228
- reset() {
229
- super.reset();
230
- this.stopTracking();
231
- this._keystrokes = [];
232
- }
233
- }
234
-
235
- export { KeyboardPatternSignal };
@@ -1,215 +0,0 @@
1
- /**
2
- * @fileoverview Detects non-human mouse movement patterns.
3
- */
4
-
5
- import { Signal } from '../../core/Signal.js';
6
-
7
- /**
8
- * Tracks and analyzes mouse movement patterns.
9
- * Bots often have perfectly linear paths, no movement, or teleportation.
10
- */
11
- class MouseMovementSignal extends Signal {
12
- static id = 'mouse-movement';
13
- static category = 'behavior';
14
- static weight = 0.9;
15
- static description = 'Detects non-human mouse movement patterns';
16
- static requiresInteraction = true;
17
-
18
- constructor(options = {}) {
19
- super(options);
20
- this._movements = [];
21
- this._isTracking = false;
22
- this._trackingDuration = options.trackingDuration || 3000; // Default 3 seconds
23
- this._minMovements = options.minMovements || 5;
24
- this._boundHandler = null;
25
- }
26
-
27
- /**
28
- * Start tracking mouse movements.
29
- * @returns {Promise<void>}
30
- */
31
- startTracking() {
32
- if (this._isTracking) return;
33
-
34
- this._movements = [];
35
- this._isTracking = true;
36
-
37
- this._boundHandler = (e) => {
38
- this._movements.push({
39
- x: e.clientX,
40
- y: e.clientY,
41
- t: performance.now(),
42
- });
43
- };
44
-
45
- document.addEventListener('mousemove', this._boundHandler, { passive: true });
46
- }
47
-
48
- /**
49
- * Stop tracking mouse movements.
50
- */
51
- stopTracking() {
52
- if (!this._isTracking) return;
53
-
54
- this._isTracking = false;
55
- if (this._boundHandler) {
56
- document.removeEventListener('mousemove', this._boundHandler);
57
- this._boundHandler = null;
58
- }
59
- }
60
-
61
- async detect() {
62
- const anomalies = [];
63
- let confidence = 0;
64
-
65
- // If no tracking has occurred, check if we have movement data
66
- if (this._movements.length === 0) {
67
- // Start tracking for a period, then analyze
68
- this.startTracking();
69
-
70
- await new Promise(resolve => setTimeout(resolve, this._trackingDuration));
71
-
72
- this.stopTracking();
73
- }
74
-
75
- const movements = this._movements;
76
-
77
- // No mouse movements detected
78
- if (movements.length < this._minMovements) {
79
- anomalies.push('no-mouse-movement');
80
- confidence = Math.max(confidence, 0.6);
81
- return this.createResult(true, { anomalies, movements: movements.length }, confidence);
82
- }
83
-
84
- // Analyze movement patterns
85
- const analysis = this._analyzeMovements(movements);
86
-
87
- // Check for teleportation (large instant jumps)
88
- if (analysis.teleportCount > 0) {
89
- anomalies.push('mouse-teleportation');
90
- confidence = Math.max(confidence, 0.7);
91
- }
92
-
93
- // Check for perfect linear paths
94
- if (analysis.linearPathRatio > 0.9) {
95
- anomalies.push('linear-path');
96
- confidence = Math.max(confidence, 0.8);
97
- }
98
-
99
- // Check for constant velocity (too perfect)
100
- if (analysis.velocityVariance < 0.01 && movements.length > 10) {
101
- anomalies.push('constant-velocity');
102
- confidence = Math.max(confidence, 0.7);
103
- }
104
-
105
- // Check for zero acceleration changes
106
- if (analysis.accelerationChanges === 0 && movements.length > 10) {
107
- anomalies.push('no-acceleration-variance');
108
- confidence = Math.max(confidence, 0.6);
109
- }
110
-
111
- // Check for robotic timing (perfect intervals)
112
- if (analysis.timingVariance < 1 && movements.length > 10) {
113
- anomalies.push('robotic-timing');
114
- confidence = Math.max(confidence, 0.8);
115
- }
116
-
117
- const triggered = anomalies.length > 0;
118
-
119
- return this.createResult(triggered, {
120
- anomalies,
121
- movementCount: movements.length,
122
- analysis,
123
- }, confidence);
124
- }
125
-
126
- /**
127
- * Analyze movement patterns for anomalies.
128
- * @param {Array} movements - Array of movement points
129
- * @returns {Object} Analysis results
130
- */
131
- _analyzeMovements(movements) {
132
- if (movements.length < 3) {
133
- return {
134
- teleportCount: 0,
135
- linearPathRatio: 0,
136
- velocityVariance: 0,
137
- accelerationChanges: 0,
138
- timingVariance: 0,
139
- };
140
- }
141
-
142
- let teleportCount = 0;
143
- const velocities = [];
144
- const angles = [];
145
- const timeIntervals = [];
146
-
147
- for (let i = 1; i < movements.length; i++) {
148
- const prev = movements[i - 1];
149
- const curr = movements[i];
150
-
151
- const dx = curr.x - prev.x;
152
- const dy = curr.y - prev.y;
153
- const dt = curr.t - prev.t;
154
-
155
- if (dt === 0) continue;
156
-
157
- const distance = Math.sqrt(dx * dx + dy * dy);
158
- const velocity = distance / dt;
159
-
160
- velocities.push(velocity);
161
- angles.push(Math.atan2(dy, dx));
162
- timeIntervals.push(dt);
163
-
164
- // Teleportation: large jump in very short time
165
- if (distance > 300 && dt < 10) {
166
- teleportCount++;
167
- }
168
- }
169
-
170
- // Calculate velocity variance
171
- const avgVelocity = velocities.reduce((a, b) => a + b, 0) / velocities.length;
172
- const velocityVariance = velocities.reduce((acc, v) =>
173
- acc + Math.pow(v - avgVelocity, 2), 0) / velocities.length;
174
-
175
- // Calculate angle consistency (linear path detection)
176
- let angleConsistency = 0;
177
- if (angles.length > 1) {
178
- let consistentAngles = 0;
179
- for (let i = 1; i < angles.length; i++) {
180
- const angleDiff = Math.abs(angles[i] - angles[i - 1]);
181
- if (angleDiff < 0.1) consistentAngles++;
182
- }
183
- angleConsistency = consistentAngles / (angles.length - 1);
184
- }
185
-
186
- // Calculate timing variance
187
- const avgInterval = timeIntervals.reduce((a, b) => a + b, 0) / timeIntervals.length;
188
- const timingVariance = timeIntervals.reduce((acc, t) =>
189
- acc + Math.pow(t - avgInterval, 2), 0) / timeIntervals.length;
190
-
191
- // Count acceleration changes
192
- let accelerationChanges = 0;
193
- for (let i = 1; i < velocities.length; i++) {
194
- if ((velocities[i] - velocities[i - 1]) * (velocities[i - 1] - (velocities[i - 2] || 0)) < 0) {
195
- accelerationChanges++;
196
- }
197
- }
198
-
199
- return {
200
- teleportCount,
201
- linearPathRatio: angleConsistency,
202
- velocityVariance,
203
- accelerationChanges,
204
- timingVariance,
205
- };
206
- }
207
-
208
- reset() {
209
- super.reset();
210
- this.stopTracking();
211
- this._movements = [];
212
- }
213
- }
214
-
215
- export { MouseMovementSignal };