@niksbanna/bot-detector 1.0.3 → 1.0.5

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 (37) hide show
  1. package/README.md +3 -0
  2. package/dist/bot-detector.cjs.js +0 -1
  3. package/dist/bot-detector.esm.js +0 -1
  4. package/dist/bot-detector.iife.js +0 -1
  5. package/package.json +1 -2
  6. package/dist/bot-detector.cjs.js.map +0 -7
  7. package/dist/bot-detector.esm.js.map +0 -7
  8. package/dist/bot-detector.iife.js.map +0 -7
  9. package/src/core/BotDetector.js +0 -295
  10. package/src/core/ScoringEngine.js +0 -135
  11. package/src/core/Signal.js +0 -181
  12. package/src/core/VerdictEngine.js +0 -132
  13. package/src/index.js +0 -281
  14. package/src/signals/automation/PhantomJSSignal.js +0 -135
  15. package/src/signals/automation/PlaywrightSignal.js +0 -123
  16. package/src/signals/automation/PuppeteerSignal.js +0 -123
  17. package/src/signals/automation/SeleniumSignal.js +0 -148
  18. package/src/signals/automation/index.js +0 -8
  19. package/src/signals/behavior/InteractionTimingSignal.js +0 -170
  20. package/src/signals/behavior/KeyboardPatternSignal.js +0 -235
  21. package/src/signals/behavior/MouseMovementSignal.js +0 -215
  22. package/src/signals/behavior/ScrollBehaviorSignal.js +0 -236
  23. package/src/signals/behavior/index.js +0 -8
  24. package/src/signals/environment/HeadlessSignal.js +0 -97
  25. package/src/signals/environment/NavigatorAnomalySignal.js +0 -118
  26. package/src/signals/environment/PermissionsSignal.js +0 -76
  27. package/src/signals/environment/WebDriverSignal.js +0 -58
  28. package/src/signals/environment/index.js +0 -8
  29. package/src/signals/fingerprint/AudioContextSignal.js +0 -158
  30. package/src/signals/fingerprint/CanvasSignal.js +0 -133
  31. package/src/signals/fingerprint/PluginsSignal.js +0 -106
  32. package/src/signals/fingerprint/ScreenSignal.js +0 -157
  33. package/src/signals/fingerprint/WebGLSignal.js +0 -146
  34. package/src/signals/fingerprint/index.js +0 -9
  35. package/src/signals/timing/DOMContentTimingSignal.js +0 -160
  36. package/src/signals/timing/PageLoadSignal.js +0 -183
  37. package/src/signals/timing/index.js +0 -6
@@ -1,123 +0,0 @@
1
- /**
2
- * @fileoverview Detects Puppeteer-specific artifacts.
3
- */
4
-
5
- import { Signal } from '../../core/Signal.js';
6
-
7
- /**
8
- * Detects artifacts left by Puppeteer automation.
9
- * Puppeteer leaves various fingerprints in the browser context.
10
- */
11
- class PuppeteerSignal extends Signal {
12
- static id = 'puppeteer';
13
- static category = 'automation';
14
- static weight = 1.0;
15
- static description = 'Detects Puppeteer automation artifacts';
16
-
17
- async detect() {
18
- const indicators = [];
19
- let confidence = 0;
20
-
21
- // Check for Puppeteer evaluation script marker
22
- if (window.__puppeteer_evaluation_script__) {
23
- indicators.push('puppeteer-evaluation-script');
24
- confidence = Math.max(confidence, 1.0);
25
- }
26
-
27
- // Check for Puppeteer-injected functions
28
- const puppeteerGlobals = [
29
- '__puppeteer_evaluation_script__',
30
- '__puppeteer',
31
- 'puppeteer',
32
- ];
33
-
34
- for (const global of puppeteerGlobals) {
35
- if (global in window) {
36
- indicators.push(`global-${global}`);
37
- confidence = Math.max(confidence, 1.0);
38
- }
39
- }
40
-
41
- // Check for HeadlessChrome in user agent (common with Puppeteer)
42
- const ua = navigator.userAgent || '';
43
- if (ua.includes('HeadlessChrome')) {
44
- indicators.push('headless-chrome-ua');
45
- confidence = Math.max(confidence, 0.9);
46
- }
47
-
48
- // Check for Puppeteer's typical Chrome DevTools Protocol artifacts
49
- if (window.cdc_adoQpoasnfa76pfcZLmcfl_Array ||
50
- window.cdc_adoQpoasnfa76pfcZLmcfl_Promise ||
51
- window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol) {
52
- indicators.push('cdp-artifacts');
53
- confidence = Math.max(confidence, 1.0);
54
- }
55
-
56
- // Check for DevTools protocol detection
57
- try {
58
- // Puppeteer often leaves eval traces
59
- const evalTest = window.eval.toString();
60
- if (evalTest.includes('puppeteer')) {
61
- indicators.push('eval-puppeteer');
62
- confidence = Math.max(confidence, 0.9);
63
- }
64
- } catch (e) {
65
- // Ignore errors
66
- }
67
-
68
- // Check for typical Puppeteer page.evaluate patterns in stack traces
69
- try {
70
- throw new Error('stack trace test');
71
- } catch (e) {
72
- const stack = e.stack || '';
73
- if (stack.includes('puppeteer') || stack.includes('pptr')) {
74
- indicators.push('stack-trace-puppeteer');
75
- confidence = Math.max(confidence, 0.8);
76
- }
77
- }
78
-
79
- // Check for Puppeteer's default viewport (800x600)
80
- if (window.innerWidth === 800 && window.innerHeight === 600) {
81
- // Only weak indicator - could be coincidence
82
- indicators.push('default-viewport');
83
- confidence = Math.max(confidence, 0.3);
84
- }
85
-
86
- // navigator.webdriver is already exclusively checked by WebDriverSignal (environment).
87
- // Duplicating it here causes triple-counting of the same property across signals.
88
-
89
- // Check for binding injection pattern
90
- // Puppeteer's exposeFunction creates window bindings
91
- // (Next.js, webpack, React DevTools, Angular) push __* count past the
92
- // old threshold of 5 on perfectly normal pages.
93
- const FRAMEWORK_PREFIXES = [
94
- '__zone_symbol__', // Angular / Zone.js
95
- '__next', // Next.js
96
- '__webpack', // webpack
97
- '__react', // React DevTools
98
- '__REACT',
99
- '__vite', // Vite
100
- '__nuxt', // Nuxt.js
101
- ];
102
- const suspiciousBindings = Object.keys(window).filter(key => {
103
- if (!key.startsWith('__')) return false;
104
- if (typeof window[key] !== 'function') return false;
105
- return !FRAMEWORK_PREFIXES.some(prefix => key.startsWith(prefix));
106
- });
107
-
108
- if (suspiciousBindings.length > 10) {
109
- indicators.push('suspicious-bindings');
110
- confidence = Math.max(confidence, 0.5);
111
- }
112
-
113
- // window.chrome.runtime is ONLY populated inside Chrome extensions.
114
- // Its absence is completely normal for all real Chrome users.
115
- // This check was causing every non-extension Chrome user to be flagged as Puppeteer.
116
-
117
- const triggered = indicators.length > 0;
118
-
119
- return this.createResult(triggered, { indicators }, confidence);
120
- }
121
- }
122
-
123
- export { PuppeteerSignal };
@@ -1,148 +0,0 @@
1
- /**
2
- * @fileoverview Detects Selenium WebDriver artifacts.
3
- */
4
-
5
- import { Signal } from '../../core/Signal.js';
6
-
7
- /**
8
- * Detects artifacts left by Selenium WebDriver.
9
- * Selenium leaves various fingerprints in the browser context.
10
- */
11
- class SeleniumSignal extends Signal {
12
- static id = 'selenium';
13
- static category = 'automation';
14
- static weight = 1.0;
15
- static description = 'Detects Selenium WebDriver artifacts';
16
-
17
- async detect() {
18
- const indicators = [];
19
- let confidence = 0;
20
-
21
- // navigator.webdriver is already exclusively checked by WebDriverSignal (environment).
22
- // Duplicating it here causes triple-counting of the same property across signals.
23
-
24
- // Check for Selenium-specific globals
25
- const seleniumGlobals = [
26
- '_selenium',
27
- 'callSelenium',
28
- '_Selenium_IDE_Recorder',
29
- '__selenium_evaluate',
30
- '__selenium_unwrap',
31
- '__webdriver_evaluate',
32
- '__webdriver_unwrap',
33
- '__webdriver_script_function',
34
- '__webdriver_script_func',
35
- '__fxdriver_evaluate',
36
- '__fxdriver_unwrap',
37
- 'webdriver',
38
- ];
39
-
40
- for (const global of seleniumGlobals) {
41
- if (global in window) {
42
- indicators.push(`global-${global}`);
43
- confidence = Math.max(confidence, 1.0);
44
- }
45
- }
46
-
47
- // Check for Selenium document properties
48
- const seleniumDocProps = [
49
- '__webdriver_script_fn',
50
- '__driver_evaluate',
51
- '__webdriver_evaluate',
52
- '__selenium_evaluate',
53
- '__fxdriver_evaluate',
54
- '__driver_unwrap',
55
- '__webdriver_unwrap',
56
- '__selenium_unwrap',
57
- '__fxdriver_unwrap',
58
- ];
59
-
60
- for (const prop of seleniumDocProps) {
61
- if (prop in document) {
62
- indicators.push(`document-${prop}`);
63
- confidence = Math.max(confidence, 1.0);
64
- }
65
- }
66
-
67
- // Check for ChromeDriver artifacts ($cdc variables)
68
- const windowKeys = Object.keys(window);
69
-
70
- // ChromeDriver injects variables starting with $cdc_ or $wdc_
71
- const cdcVars = windowKeys.filter(key =>
72
- key.startsWith('$cdc_') ||
73
- key.startsWith('$wdc_') ||
74
- key.startsWith('$chrome_asyncScriptInfo')
75
- );
76
-
77
- if (cdcVars.length > 0) {
78
- indicators.push('chromedriver-variables');
79
- confidence = Math.max(confidence, 1.0);
80
- }
81
-
82
- // Check for GeckoDriver (Firefox) artifacts
83
- if (window.webdriverCallback || document.documentElement.getAttribute('webdriver')) {
84
- indicators.push('geckodriver-artifacts');
85
- confidence = Math.max(confidence, 1.0);
86
- }
87
-
88
- // Check for webdriver in document element attributes
89
- try {
90
- const docElement = document.documentElement;
91
- if (docElement.hasAttribute('webdriver') ||
92
- docElement.getAttribute('selenium') ||
93
- docElement.getAttribute('driver')) {
94
- indicators.push('document-webdriver-attr');
95
- confidence = Math.max(confidence, 1.0);
96
- }
97
- } catch (e) {
98
- // Ignore errors
99
- }
100
-
101
- // Check for Selenium IDE artifacts
102
- if (window.selenium || window.sideex) {
103
- indicators.push('selenium-ide');
104
- confidence = Math.max(confidence, 1.0);
105
- }
106
-
107
- // Check for navigator.webdriver property descriptor anomalies
108
- try {
109
- const descriptor = Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver');
110
- if (descriptor) {
111
- // Check if the getter has been modified
112
- if (descriptor.get) {
113
- const getterStr = descriptor.get.toString();
114
- if (!getterStr.includes('[native code]')) {
115
- indicators.push('webdriver-getter-modified');
116
- confidence = Math.max(confidence, 0.7);
117
- }
118
- }
119
- }
120
- } catch (e) {
121
- // Ignore errors
122
- }
123
-
124
- // Check for driver command executor
125
- if (window.domAutomation || window.domAutomationController) {
126
- indicators.push('dom-automation');
127
- confidence = Math.max(confidence, 1.0);
128
- }
129
-
130
- // Check for callPhantom alternative used by some Selenium setups
131
- if (window.awesomium) {
132
- indicators.push('awesomium');
133
- confidence = Math.max(confidence, 0.9);
134
- }
135
-
136
- // Check for external interface (used by some automation tools)
137
- if (window.external && window.external.toString().includes('Selenium')) {
138
- indicators.push('external-selenium');
139
- confidence = Math.max(confidence, 1.0);
140
- }
141
-
142
- const triggered = indicators.length > 0;
143
-
144
- return this.createResult(triggered, { indicators }, confidence);
145
- }
146
- }
147
-
148
- export { SeleniumSignal };
@@ -1,8 +0,0 @@
1
- /**
2
- * @fileoverview Automation signals index.
3
- */
4
-
5
- export { PuppeteerSignal } from './PuppeteerSignal.js';
6
- export { PlaywrightSignal } from './PlaywrightSignal.js';
7
- export { SeleniumSignal } from './SeleniumSignal.js';
8
- export { PhantomJSSignal } from './PhantomJSSignal.js';
@@ -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 = Math.min(options.trackingDuration || 2500, 2500);
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 };