@niksbanna/bot-detector 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 (38) hide show
  1. package/README.md +245 -0
  2. package/dist/bot-detector.cjs.js +2629 -0
  3. package/dist/bot-detector.cjs.js.map +7 -0
  4. package/dist/bot-detector.esm.js +2609 -0
  5. package/dist/bot-detector.esm.js.map +7 -0
  6. package/dist/bot-detector.iife.js +2631 -0
  7. package/dist/bot-detector.iife.js.map +7 -0
  8. package/dist/bot-detector.iife.min.js +1 -0
  9. package/package.json +57 -0
  10. package/src/core/BotDetector.js +284 -0
  11. package/src/core/ScoringEngine.js +134 -0
  12. package/src/core/Signal.js +181 -0
  13. package/src/core/VerdictEngine.js +132 -0
  14. package/src/index.js +273 -0
  15. package/src/signals/automation/PhantomJSSignal.js +137 -0
  16. package/src/signals/automation/PlaywrightSignal.js +129 -0
  17. package/src/signals/automation/PuppeteerSignal.js +117 -0
  18. package/src/signals/automation/SeleniumSignal.js +151 -0
  19. package/src/signals/automation/index.js +8 -0
  20. package/src/signals/behavior/InteractionTimingSignal.js +170 -0
  21. package/src/signals/behavior/KeyboardPatternSignal.js +235 -0
  22. package/src/signals/behavior/MouseMovementSignal.js +215 -0
  23. package/src/signals/behavior/ScrollBehaviorSignal.js +236 -0
  24. package/src/signals/behavior/index.js +8 -0
  25. package/src/signals/environment/HeadlessSignal.js +97 -0
  26. package/src/signals/environment/NavigatorAnomalySignal.js +117 -0
  27. package/src/signals/environment/PermissionsSignal.js +76 -0
  28. package/src/signals/environment/WebDriverSignal.js +58 -0
  29. package/src/signals/environment/index.js +8 -0
  30. package/src/signals/fingerprint/AudioContextSignal.js +158 -0
  31. package/src/signals/fingerprint/CanvasSignal.js +133 -0
  32. package/src/signals/fingerprint/PluginsSignal.js +106 -0
  33. package/src/signals/fingerprint/ScreenSignal.js +157 -0
  34. package/src/signals/fingerprint/WebGLSignal.js +146 -0
  35. package/src/signals/fingerprint/index.js +9 -0
  36. package/src/signals/timing/DOMContentTimingSignal.js +159 -0
  37. package/src/signals/timing/PageLoadSignal.js +165 -0
  38. package/src/signals/timing/index.js +6 -0
@@ -0,0 +1,215 @@
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 };
@@ -0,0 +1,236 @@
1
+ /**
2
+ * @fileoverview Detects non-human scroll patterns.
3
+ */
4
+
5
+ import { Signal } from '../../core/Signal.js';
6
+
7
+ /**
8
+ * Tracks and analyzes scroll behavior patterns.
9
+ * Detects programmatic scrolling and non-human scroll patterns.
10
+ */
11
+ class ScrollBehaviorSignal extends Signal {
12
+ static id = 'scroll-behavior';
13
+ static category = 'behavior';
14
+ static weight = 0.5;
15
+ static description = 'Detects programmatic scroll patterns';
16
+ static requiresInteraction = true;
17
+
18
+ constructor(options = {}) {
19
+ super(options);
20
+ this._scrollEvents = [];
21
+ this._isTracking = false;
22
+ this._trackingDuration = options.trackingDuration || 3000;
23
+ this._boundHandler = null;
24
+ }
25
+
26
+ /**
27
+ * Start tracking scroll events.
28
+ */
29
+ startTracking() {
30
+ if (this._isTracking) return;
31
+
32
+ this._scrollEvents = [];
33
+ this._isTracking = true;
34
+
35
+ this._boundHandler = () => {
36
+ this._scrollEvents.push({
37
+ scrollY: window.scrollY,
38
+ scrollX: window.scrollX,
39
+ t: performance.now(),
40
+ });
41
+ };
42
+
43
+ window.addEventListener('scroll', this._boundHandler, { passive: true });
44
+ }
45
+
46
+ /**
47
+ * Stop tracking scroll events.
48
+ */
49
+ stopTracking() {
50
+ if (!this._isTracking) return;
51
+
52
+ this._isTracking = false;
53
+ if (this._boundHandler) {
54
+ window.removeEventListener('scroll', this._boundHandler);
55
+ this._boundHandler = null;
56
+ }
57
+ }
58
+
59
+ async detect() {
60
+ const anomalies = [];
61
+ let confidence = 0;
62
+
63
+ // Start tracking if needed
64
+ if (this._scrollEvents.length === 0) {
65
+ this.startTracking();
66
+ await new Promise(resolve => setTimeout(resolve, this._trackingDuration));
67
+ this.stopTracking();
68
+ }
69
+
70
+ const events = this._scrollEvents;
71
+
72
+ // No scroll events - not necessarily suspicious
73
+ if (events.length < 3) {
74
+ return this.createResult(false, {
75
+ reason: 'insufficient-scroll-data',
76
+ scrollEvents: events.length,
77
+ }, 0);
78
+ }
79
+
80
+ const analysis = this._analyzeScrollPatterns(events);
81
+
82
+ // Check for instant jumps (scrollTo without animation)
83
+ if (analysis.instantJumps > 0) {
84
+ anomalies.push('instant-scroll-jumps');
85
+ confidence = Math.max(confidence, 0.7);
86
+ }
87
+
88
+ // Check for perfectly consistent scroll speed
89
+ if (analysis.velocityVariance < 0.1 && events.length > 10) {
90
+ anomalies.push('constant-scroll-velocity');
91
+ confidence = Math.max(confidence, 0.6);
92
+ }
93
+
94
+ // Check for no momentum (instant stops)
95
+ if (analysis.momentumEvents === 0 && events.length > 5) {
96
+ anomalies.push('no-scroll-momentum');
97
+ confidence = Math.max(confidence, 0.5);
98
+ }
99
+
100
+ // Check for perfectly even scroll intervals
101
+ if (analysis.intervalVariance < 5 && events.length > 10) {
102
+ anomalies.push('robotic-scroll-timing');
103
+ confidence = Math.max(confidence, 0.7);
104
+ }
105
+
106
+ // Check for exclusively vertical or horizontal scroll
107
+ if (analysis.scrollDirections === 1 && Math.abs(analysis.totalScrollY) > 1000) {
108
+ // Could be normal, but combined with other factors is suspicious
109
+ if (analysis.velocityVariance < 1) {
110
+ anomalies.push('one-dimensional-scroll');
111
+ confidence = Math.max(confidence, 0.4);
112
+ }
113
+ }
114
+
115
+ // Check for scroll-to-element patterns (exact positions)
116
+ if (analysis.exactPositionScrolls > 2) {
117
+ anomalies.push('exact-position-scrolls');
118
+ confidence = Math.max(confidence, 0.6);
119
+ }
120
+
121
+ const triggered = anomalies.length > 0;
122
+
123
+ return this.createResult(triggered, {
124
+ anomalies,
125
+ scrollEventCount: events.length,
126
+ analysis,
127
+ }, confidence);
128
+ }
129
+
130
+ /**
131
+ * Analyze scroll patterns for anomalies.
132
+ * @param {Array} events - Array of scroll events
133
+ * @returns {Object} Analysis results
134
+ */
135
+ _analyzeScrollPatterns(events) {
136
+ if (events.length < 2) {
137
+ return {
138
+ instantJumps: 0,
139
+ velocityVariance: 0,
140
+ momentumEvents: 0,
141
+ intervalVariance: 0,
142
+ scrollDirections: 0,
143
+ totalScrollY: 0,
144
+ exactPositionScrolls: 0,
145
+ };
146
+ }
147
+
148
+ let instantJumps = 0;
149
+ let momentumEvents = 0;
150
+ const velocities = [];
151
+ const intervals = [];
152
+ let hasVertical = false;
153
+ let hasHorizontal = false;
154
+ let exactPositionScrolls = 0;
155
+
156
+ // Common scroll targets (element heights, percentages)
157
+ const commonPositions = [0, 100, 200, 300, 400, 500, 600, 800, 1000];
158
+
159
+ for (let i = 1; i < events.length; i++) {
160
+ const prev = events[i - 1];
161
+ const curr = events[i];
162
+
163
+ const dy = curr.scrollY - prev.scrollY;
164
+ const dx = curr.scrollX - prev.scrollX;
165
+ const dt = curr.t - prev.t;
166
+
167
+ intervals.push(dt);
168
+
169
+ if (Math.abs(dy) > 0) hasVertical = true;
170
+ if (Math.abs(dx) > 0) hasHorizontal = true;
171
+
172
+ if (dt === 0) continue;
173
+
174
+ const velocity = Math.sqrt(dy * dy + dx * dx) / dt;
175
+ velocities.push(velocity);
176
+
177
+ // Instant jump: large distance in very short time (< 16ms, one frame)
178
+ const distance = Math.abs(dy) + Math.abs(dx);
179
+ if (distance > 200 && dt < 20) {
180
+ instantJumps++;
181
+ }
182
+
183
+ // Momentum: decreasing velocity (natural scroll deceleration)
184
+ if (i > 1 && velocities.length > 1) {
185
+ const prevVelocity = velocities[velocities.length - 2];
186
+ if (velocity < prevVelocity * 0.9 && velocity > 0) {
187
+ momentumEvents++;
188
+ }
189
+ }
190
+
191
+ // Check for exact positions
192
+ if (commonPositions.includes(Math.round(curr.scrollY))) {
193
+ exactPositionScrolls++;
194
+ }
195
+ }
196
+
197
+ // Calculate velocity variance
198
+ const avgVelocity = velocities.length > 0
199
+ ? velocities.reduce((a, b) => a + b, 0) / velocities.length
200
+ : 0;
201
+ const velocityVariance = velocities.length > 0
202
+ ? velocities.reduce((acc, v) => acc + Math.pow(v - avgVelocity, 2), 0) / velocities.length
203
+ : 0;
204
+
205
+ // Calculate interval variance
206
+ const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
207
+ const intervalVariance = intervals.reduce((acc, t) =>
208
+ acc + Math.pow(t - avgInterval, 2), 0) / intervals.length;
209
+
210
+ // Count scroll directions
211
+ let scrollDirections = 0;
212
+ if (hasVertical) scrollDirections++;
213
+ if (hasHorizontal) scrollDirections++;
214
+
215
+ // Total scroll distance
216
+ const totalScrollY = events[events.length - 1].scrollY - events[0].scrollY;
217
+
218
+ return {
219
+ instantJumps,
220
+ velocityVariance,
221
+ momentumEvents,
222
+ intervalVariance,
223
+ scrollDirections,
224
+ totalScrollY,
225
+ exactPositionScrolls,
226
+ };
227
+ }
228
+
229
+ reset() {
230
+ super.reset();
231
+ this.stopTracking();
232
+ this._scrollEvents = [];
233
+ }
234
+ }
235
+
236
+ export { ScrollBehaviorSignal };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @fileoverview Behavioral signals index.
3
+ */
4
+
5
+ export { MouseMovementSignal } from './MouseMovementSignal.js';
6
+ export { KeyboardPatternSignal } from './KeyboardPatternSignal.js';
7
+ export { InteractionTimingSignal } from './InteractionTimingSignal.js';
8
+ export { ScrollBehaviorSignal } from './ScrollBehaviorSignal.js';
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @fileoverview Detects headless browser indicators.
3
+ */
4
+
5
+ import { Signal } from '../../core/Signal.js';
6
+
7
+ /**
8
+ * Detects indicators of headless browser execution.
9
+ * Headless browsers often have missing or inconsistent features.
10
+ */
11
+ class HeadlessSignal extends Signal {
12
+ static id = 'headless';
13
+ static category = 'environment';
14
+ static weight = 0.8;
15
+ static description = 'Detects headless browser indicators';
16
+
17
+ async detect() {
18
+ const indicators = [];
19
+ let confidence = 0;
20
+
21
+ // Check for HeadlessChrome in user agent
22
+ const ua = navigator.userAgent || '';
23
+ if (ua.includes('HeadlessChrome')) {
24
+ indicators.push('headless-ua');
25
+ confidence = Math.max(confidence, 1.0);
26
+ }
27
+
28
+ // Check for missing chrome.runtime in Chrome
29
+ if (ua.includes('Chrome') && !ua.includes('Chromium')) {
30
+ if (typeof window.chrome === 'undefined') {
31
+ indicators.push('missing-chrome-object');
32
+ confidence = Math.max(confidence, 0.6);
33
+ } else if (!window.chrome.runtime) {
34
+ indicators.push('missing-chrome-runtime');
35
+ confidence = Math.max(confidence, 0.4);
36
+ }
37
+ }
38
+
39
+ // Check for empty plugins (common in headless)
40
+ if (navigator.plugins && navigator.plugins.length === 0) {
41
+ indicators.push('no-plugins');
42
+ confidence = Math.max(confidence, 0.5);
43
+ }
44
+
45
+ // Check for missing languages
46
+ if (!navigator.languages || navigator.languages.length === 0) {
47
+ indicators.push('no-languages');
48
+ confidence = Math.max(confidence, 0.6);
49
+ }
50
+
51
+ // Check window dimensions anomalies
52
+ if (window.outerWidth === 0 && window.outerHeight === 0) {
53
+ indicators.push('zero-outer-dimensions');
54
+ confidence = Math.max(confidence, 0.7);
55
+ }
56
+
57
+ // Check for missing connection info
58
+ if (typeof navigator.connection === 'undefined' && ua.includes('Chrome')) {
59
+ indicators.push('missing-connection-api');
60
+ confidence = Math.max(confidence, 0.3);
61
+ }
62
+
63
+ // Check for Notification API permission inconsistency
64
+ try {
65
+ if (typeof Notification !== 'undefined' && Notification.permission === 'denied' &&
66
+ window.outerWidth === 0) {
67
+ indicators.push('notification-headless-pattern');
68
+ confidence = Math.max(confidence, 0.5);
69
+ }
70
+ } catch (e) {
71
+ // Ignore errors
72
+ }
73
+
74
+ // Phantom.js specific check
75
+ if (window.callPhantom || window._phantom) {
76
+ indicators.push('phantomjs');
77
+ confidence = Math.max(confidence, 1.0);
78
+ }
79
+
80
+ // Nightmare.js check
81
+ if (window.__nightmare) {
82
+ indicators.push('nightmare');
83
+ confidence = Math.max(confidence, 1.0);
84
+ }
85
+
86
+ const triggered = indicators.length > 0;
87
+
88
+ // Increase confidence if multiple indicators are present
89
+ if (indicators.length >= 3) {
90
+ confidence = Math.min(1.0, confidence + 0.2);
91
+ }
92
+
93
+ return this.createResult(triggered, { indicators }, confidence);
94
+ }
95
+ }
96
+
97
+ export { HeadlessSignal };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @fileoverview Detects navigator property anomalies and inconsistencies.
3
+ */
4
+
5
+ import { Signal } from '../../core/Signal.js';
6
+
7
+ /**
8
+ * Detects inconsistencies in navigator properties.
9
+ * Bots often have mismatched or spoofed navigator values.
10
+ */
11
+ class NavigatorAnomalySignal extends Signal {
12
+ static id = 'navigator-anomaly';
13
+ static category = 'environment';
14
+ static weight = 0.7;
15
+ static description = 'Detects navigator property inconsistencies';
16
+
17
+ async detect() {
18
+ const anomalies = [];
19
+ let totalScore = 0;
20
+ let checksPerformed = 0;
21
+
22
+ const ua = navigator.userAgent || '';
23
+ const platform = navigator.platform || '';
24
+
25
+ // Platform vs UserAgent consistency check
26
+ checksPerformed++;
27
+ if (platform.includes('Win') && !ua.includes('Windows')) {
28
+ anomalies.push('platform-ua-mismatch-windows');
29
+ totalScore += 1;
30
+ } else if (platform.includes('Mac') && !ua.includes('Mac')) {
31
+ anomalies.push('platform-ua-mismatch-mac');
32
+ totalScore += 1;
33
+ } else if (platform.includes('Linux') && !ua.includes('Linux') && !ua.includes('Android')) {
34
+ anomalies.push('platform-ua-mismatch-linux');
35
+ totalScore += 1;
36
+ }
37
+
38
+ // Check for empty or suspicious platform
39
+ checksPerformed++;
40
+ if (!platform || platform === '' || platform === 'undefined') {
41
+ anomalies.push('empty-platform');
42
+ totalScore += 1;
43
+ }
44
+
45
+ // Language consistency
46
+ checksPerformed++;
47
+ if (navigator.language && navigator.languages) {
48
+ if (!navigator.languages.includes(navigator.language)) {
49
+ anomalies.push('language-mismatch');
50
+ totalScore += 0.5;
51
+ }
52
+ }
53
+
54
+ // Check vendor consistency with browser
55
+ checksPerformed++;
56
+ if (ua.includes('Chrome') && navigator.vendor !== 'Google Inc.') {
57
+ anomalies.push('vendor-mismatch-chrome');
58
+ totalScore += 0.5;
59
+ } else if (ua.includes('Firefox') && navigator.vendor !== '') {
60
+ anomalies.push('vendor-mismatch-firefox');
61
+ totalScore += 0.5;
62
+ } else if (ua.includes('Safari') && !ua.includes('Chrome') &&
63
+ navigator.vendor !== 'Apple Computer, Inc.') {
64
+ anomalies.push('vendor-mismatch-safari');
65
+ totalScore += 0.5;
66
+ }
67
+
68
+ // Check for hardwareConcurrency anomaly
69
+ checksPerformed++;
70
+ if (typeof navigator.hardwareConcurrency !== 'undefined') {
71
+ if (navigator.hardwareConcurrency === 0 || navigator.hardwareConcurrency > 128) {
72
+ anomalies.push('suspicious-hardware-concurrency');
73
+ totalScore += 0.5;
74
+ }
75
+ }
76
+
77
+ // Check deviceMemory if available
78
+ checksPerformed++;
79
+ if (typeof navigator.deviceMemory !== 'undefined') {
80
+ if (navigator.deviceMemory === 0 || navigator.deviceMemory > 512) {
81
+ anomalies.push('suspicious-device-memory');
82
+ totalScore += 0.5;
83
+ }
84
+ }
85
+
86
+ // Check maxTouchPoints consistency
87
+ checksPerformed++;
88
+ const isMobileUA = /Android|iPhone|iPad|iPod|Mobile/i.test(ua);
89
+ const hasTouchPoints = navigator.maxTouchPoints > 0;
90
+
91
+ // Desktop claiming touch or mobile with no touch
92
+ if (!isMobileUA && navigator.maxTouchPoints > 5) {
93
+ anomalies.push('desktop-high-touch-points');
94
+ totalScore += 0.3;
95
+ }
96
+
97
+ // Check for overridden properties (common in spoofing)
98
+ checksPerformed++;
99
+ try {
100
+ const desc = Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgent');
101
+ if (desc && desc.get && desc.get.toString().includes('native code') === false) {
102
+ anomalies.push('spoofed-user-agent');
103
+ totalScore += 1;
104
+ }
105
+ } catch (e) {
106
+ // Ignore errors
107
+ }
108
+
109
+ // Calculate confidence based on number and severity of anomalies
110
+ const triggered = anomalies.length > 0;
111
+ const confidence = Math.min(1, totalScore / Math.max(1, checksPerformed));
112
+
113
+ return this.createResult(triggered, { anomalies }, confidence);
114
+ }
115
+ }
116
+
117
+ export { NavigatorAnomalySignal };