@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.
- package/README.md +22 -0
- package/dist/bot-detector.cjs.js +115 -75
- package/dist/bot-detector.cjs.js.map +2 -2
- package/dist/bot-detector.esm.js +115 -75
- package/dist/bot-detector.esm.js.map +2 -2
- package/dist/bot-detector.iife.js +115 -75
- package/dist/bot-detector.iife.js.map +2 -2
- package/dist/bot-detector.iife.min.js +3 -1
- package/package.json +1 -2
- package/src/core/BotDetector.js +0 -284
- package/src/core/ScoringEngine.js +0 -134
- package/src/core/Signal.js +0 -181
- package/src/core/VerdictEngine.js +0 -132
- package/src/index.js +0 -273
- package/src/signals/automation/PhantomJSSignal.js +0 -137
- package/src/signals/automation/PlaywrightSignal.js +0 -129
- package/src/signals/automation/PuppeteerSignal.js +0 -122
- package/src/signals/automation/SeleniumSignal.js +0 -151
- package/src/signals/automation/index.js +0 -8
- package/src/signals/behavior/InteractionTimingSignal.js +0 -170
- package/src/signals/behavior/KeyboardPatternSignal.js +0 -235
- package/src/signals/behavior/MouseMovementSignal.js +0 -215
- package/src/signals/behavior/ScrollBehaviorSignal.js +0 -236
- package/src/signals/behavior/index.js +0 -8
- package/src/signals/environment/HeadlessSignal.js +0 -97
- package/src/signals/environment/NavigatorAnomalySignal.js +0 -117
- package/src/signals/environment/PermissionsSignal.js +0 -76
- package/src/signals/environment/WebDriverSignal.js +0 -58
- package/src/signals/environment/index.js +0 -8
- package/src/signals/fingerprint/AudioContextSignal.js +0 -158
- package/src/signals/fingerprint/CanvasSignal.js +0 -133
- package/src/signals/fingerprint/PluginsSignal.js +0 -106
- package/src/signals/fingerprint/ScreenSignal.js +0 -157
- package/src/signals/fingerprint/WebGLSignal.js +0 -146
- package/src/signals/fingerprint/index.js +0 -9
- package/src/signals/timing/DOMContentTimingSignal.js +0 -159
- package/src/signals/timing/PageLoadSignal.js +0 -165
- package/src/signals/timing/index.js +0 -6
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Scoring engine that calculates bot probability from signals.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Calculates a weighted score from signal results.
|
|
7
|
-
* Score represents the probability of the visitor being a bot.
|
|
8
|
-
*/
|
|
9
|
-
class ScoringEngine {
|
|
10
|
-
/**
|
|
11
|
-
* Creates a new ScoringEngine instance.
|
|
12
|
-
* @param {Object} [options={}] - Configuration options
|
|
13
|
-
* @param {Object.<string, number>} [options.weightOverrides={}] - Override weights by signal ID
|
|
14
|
-
* @param {number} [options.maxScore=100] - Maximum possible score
|
|
15
|
-
*/
|
|
16
|
-
constructor(options = {}) {
|
|
17
|
-
this.weightOverrides = options.weightOverrides || {};
|
|
18
|
-
this.maxScore = options.maxScore || 100;
|
|
19
|
-
this._results = new Map();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Get the effective weight for a signal.
|
|
24
|
-
* @param {string} signalId - Signal identifier
|
|
25
|
-
* @param {number} defaultWeight - Default weight from signal definition
|
|
26
|
-
* @returns {number}
|
|
27
|
-
*/
|
|
28
|
-
getWeight(signalId, defaultWeight) {
|
|
29
|
-
return this.weightOverrides[signalId] ?? defaultWeight;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Add a signal result to the scoring calculation.
|
|
34
|
-
* @param {string} signalId - Signal identifier
|
|
35
|
-
* @param {Object} result - Signal detection result
|
|
36
|
-
* @param {boolean} result.triggered - Whether signal was triggered
|
|
37
|
-
* @param {number} result.confidence - Confidence level (0-1)
|
|
38
|
-
* @param {number} weight - Signal weight (0.1-1.0)
|
|
39
|
-
*/
|
|
40
|
-
addResult(signalId, result, weight) {
|
|
41
|
-
const effectiveWeight = this.getWeight(signalId, weight);
|
|
42
|
-
this._results.set(signalId, {
|
|
43
|
-
...result,
|
|
44
|
-
weight: effectiveWeight,
|
|
45
|
-
contribution: result.triggered ? effectiveWeight * result.confidence : 0,
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Calculate the final score from all added results.
|
|
51
|
-
* Formula: (sum of contributions / sum of weights) * maxScore
|
|
52
|
-
*
|
|
53
|
-
* @returns {number} Score between 0 and maxScore
|
|
54
|
-
*/
|
|
55
|
-
calculate() {
|
|
56
|
-
if (this._results.size === 0) {
|
|
57
|
-
return 0;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
let totalWeight = 0;
|
|
61
|
-
let totalContribution = 0;
|
|
62
|
-
|
|
63
|
-
for (const [, data] of this._results) {
|
|
64
|
-
totalWeight += data.weight;
|
|
65
|
-
totalContribution += data.contribution;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (totalWeight === 0) {
|
|
69
|
-
return 0;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const score = (totalContribution / totalWeight) * this.maxScore;
|
|
73
|
-
return Math.round(score * 100) / 100; // Round to 2 decimal places
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Get detailed breakdown of score contributions.
|
|
78
|
-
* @returns {Array<Object>} Array of signal contributions
|
|
79
|
-
*/
|
|
80
|
-
getBreakdown() {
|
|
81
|
-
const breakdown = [];
|
|
82
|
-
|
|
83
|
-
for (const [signalId, data] of this._results) {
|
|
84
|
-
breakdown.push({
|
|
85
|
-
signalId,
|
|
86
|
-
triggered: data.triggered,
|
|
87
|
-
confidence: data.confidence,
|
|
88
|
-
weight: data.weight,
|
|
89
|
-
contribution: data.contribution,
|
|
90
|
-
percentOfScore: this.calculate() > 0
|
|
91
|
-
? (data.contribution / this.calculate() * 100).toFixed(1)
|
|
92
|
-
: '0.0',
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Sort by contribution (highest first)
|
|
97
|
-
return breakdown.sort((a, b) => b.contribution - a.contribution);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Get all triggered signals.
|
|
102
|
-
* @returns {Array<string>} Array of triggered signal IDs
|
|
103
|
-
*/
|
|
104
|
-
getTriggeredSignals() {
|
|
105
|
-
const triggered = [];
|
|
106
|
-
for (const [signalId, data] of this._results) {
|
|
107
|
-
if (data.triggered) {
|
|
108
|
-
triggered.push(signalId);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return triggered;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Get the number of triggered signals.
|
|
116
|
-
* @returns {number}
|
|
117
|
-
*/
|
|
118
|
-
getTriggeredCount() {
|
|
119
|
-
let count = 0;
|
|
120
|
-
for (const [, data] of this._results) {
|
|
121
|
-
if (data.triggered) count++;
|
|
122
|
-
}
|
|
123
|
-
return count;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Reset all stored results.
|
|
128
|
-
*/
|
|
129
|
-
reset() {
|
|
130
|
-
this._results.clear();
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export { ScoringEngine };
|
package/src/core/Signal.js
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Base Signal class for bot detection signals.
|
|
3
|
-
* All signal detectors must extend this class.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Base class for all signal detectors.
|
|
8
|
-
* Signals detect specific indicators of automated browser behavior.
|
|
9
|
-
*
|
|
10
|
-
* @abstract
|
|
11
|
-
* @example
|
|
12
|
-
* class CustomSignal extends Signal {
|
|
13
|
-
* static id = 'custom-signal';
|
|
14
|
-
* static category = 'custom';
|
|
15
|
-
* static weight = 0.5;
|
|
16
|
-
* static description = 'Detects custom bot behavior';
|
|
17
|
-
*
|
|
18
|
-
* async detect() {
|
|
19
|
-
* const isSuspicious = // ... detection logic
|
|
20
|
-
* return {
|
|
21
|
-
* triggered: isSuspicious,
|
|
22
|
-
* value: someValue,
|
|
23
|
-
* confidence: 0.8
|
|
24
|
-
* };
|
|
25
|
-
* }
|
|
26
|
-
* }
|
|
27
|
-
*/
|
|
28
|
-
class Signal {
|
|
29
|
-
/**
|
|
30
|
-
* Unique identifier for this signal.
|
|
31
|
-
* @type {string}
|
|
32
|
-
*/
|
|
33
|
-
static id = 'base-signal';
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Category this signal belongs to.
|
|
37
|
-
* Categories: 'environment', 'behavior', 'fingerprint', 'timing', 'automation'
|
|
38
|
-
* @type {string}
|
|
39
|
-
*/
|
|
40
|
-
static category = 'uncategorized';
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Weight of this signal in the scoring calculation.
|
|
44
|
-
* Range: 0.1 (low importance) to 1.0 (high importance)
|
|
45
|
-
* @type {number}
|
|
46
|
-
*/
|
|
47
|
-
static weight = 0.5;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Human-readable description of what this signal detects.
|
|
51
|
-
* @type {string}
|
|
52
|
-
*/
|
|
53
|
-
static description = 'Base signal class';
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Whether this signal requires user interaction before it can detect.
|
|
57
|
-
* @type {boolean}
|
|
58
|
-
*/
|
|
59
|
-
static requiresInteraction = false;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Creates a new Signal instance.
|
|
63
|
-
* @param {Object} [options={}] - Configuration options for this signal.
|
|
64
|
-
*/
|
|
65
|
-
constructor(options = {}) {
|
|
66
|
-
this.options = options;
|
|
67
|
-
this._lastResult = null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Get the signal's unique identifier.
|
|
72
|
-
* @returns {string}
|
|
73
|
-
*/
|
|
74
|
-
get id() {
|
|
75
|
-
return this.constructor.id;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Get the signal's category.
|
|
80
|
-
* @returns {string}
|
|
81
|
-
*/
|
|
82
|
-
get category() {
|
|
83
|
-
return this.constructor.category;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Get the signal's weight.
|
|
88
|
-
* @returns {number}
|
|
89
|
-
*/
|
|
90
|
-
get weight() {
|
|
91
|
-
return this.options.weight ?? this.constructor.weight;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Get the signal's description.
|
|
96
|
-
* @returns {string}
|
|
97
|
-
*/
|
|
98
|
-
get description() {
|
|
99
|
-
return this.constructor.description;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Check if this signal requires interaction.
|
|
104
|
-
* @returns {boolean}
|
|
105
|
-
*/
|
|
106
|
-
get requiresInteraction() {
|
|
107
|
-
return this.constructor.requiresInteraction;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Get the last detection result.
|
|
112
|
-
* @returns {SignalResult|null}
|
|
113
|
-
*/
|
|
114
|
-
get lastResult() {
|
|
115
|
-
return this._lastResult;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Perform the detection check.
|
|
120
|
-
* Must be overridden by subclasses.
|
|
121
|
-
*
|
|
122
|
-
* @abstract
|
|
123
|
-
* @returns {Promise<SignalResult>} Detection result
|
|
124
|
-
* @throws {Error} If not implemented by subclass
|
|
125
|
-
*/
|
|
126
|
-
async detect() {
|
|
127
|
-
throw new Error(`Signal.detect() must be implemented by ${this.constructor.name}`);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Run detection and cache the result.
|
|
132
|
-
* @returns {Promise<SignalResult>}
|
|
133
|
-
*/
|
|
134
|
-
async run() {
|
|
135
|
-
try {
|
|
136
|
-
this._lastResult = await this.detect();
|
|
137
|
-
return this._lastResult;
|
|
138
|
-
} catch (error) {
|
|
139
|
-
// Fail-safe: if detection throws, treat as not triggered
|
|
140
|
-
this._lastResult = {
|
|
141
|
-
triggered: false,
|
|
142
|
-
value: null,
|
|
143
|
-
confidence: 0,
|
|
144
|
-
error: error.message,
|
|
145
|
-
};
|
|
146
|
-
return this._lastResult;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Reset the signal state.
|
|
152
|
-
*/
|
|
153
|
-
reset() {
|
|
154
|
-
this._lastResult = null;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Create a result object with defaults.
|
|
159
|
-
* @param {boolean} triggered - Whether the signal was triggered
|
|
160
|
-
* @param {*} [value=null] - Optional value associated with the detection
|
|
161
|
-
* @param {number} [confidence=1] - Confidence level (0-1)
|
|
162
|
-
* @returns {SignalResult}
|
|
163
|
-
*/
|
|
164
|
-
createResult(triggered, value = null, confidence = 1) {
|
|
165
|
-
return {
|
|
166
|
-
triggered: Boolean(triggered),
|
|
167
|
-
value,
|
|
168
|
-
confidence: Math.max(0, Math.min(1, confidence)),
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* @typedef {Object} SignalResult
|
|
175
|
-
* @property {boolean} triggered - Whether the signal detected bot behavior
|
|
176
|
-
* @property {*} value - Associated value or evidence
|
|
177
|
-
* @property {number} confidence - Confidence level between 0 and 1
|
|
178
|
-
* @property {string} [error] - Error message if detection failed
|
|
179
|
-
*/
|
|
180
|
-
|
|
181
|
-
export { Signal };
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Verdict engine that determines bot/human classification.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Possible verdicts from detection.
|
|
7
|
-
* @enum {string}
|
|
8
|
-
*/
|
|
9
|
-
const Verdict = {
|
|
10
|
-
HUMAN: 'human',
|
|
11
|
-
SUSPICIOUS: 'suspicious',
|
|
12
|
-
BOT: 'bot',
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Determines the final verdict based on score and triggered signals.
|
|
17
|
-
*/
|
|
18
|
-
class VerdictEngine {
|
|
19
|
-
/**
|
|
20
|
-
* Default threshold configuration.
|
|
21
|
-
* @type {Object}
|
|
22
|
-
*/
|
|
23
|
-
static DEFAULT_THRESHOLDS = {
|
|
24
|
-
human: 20, // score < 20 = human
|
|
25
|
-
suspicious: 50, // 20 <= score < 50 = suspicious
|
|
26
|
-
// score >= 50 = bot
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Creates a new VerdictEngine instance.
|
|
31
|
-
* @param {Object} [options={}] - Configuration options
|
|
32
|
-
* @param {number} [options.humanThreshold=20] - Max score for human verdict
|
|
33
|
-
* @param {number} [options.suspiciousThreshold=50] - Max score for suspicious verdict
|
|
34
|
-
* @param {Array<string>} [options.instantBotSignals=[]] - Signal IDs that instantly flag as bot
|
|
35
|
-
*/
|
|
36
|
-
constructor(options = {}) {
|
|
37
|
-
this.humanThreshold = options.humanThreshold ?? VerdictEngine.DEFAULT_THRESHOLDS.human;
|
|
38
|
-
this.suspiciousThreshold = options.suspiciousThreshold ?? VerdictEngine.DEFAULT_THRESHOLDS.suspicious;
|
|
39
|
-
this.instantBotSignals = new Set(options.instantBotSignals || []);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Get the verdict based on score and triggered signals.
|
|
44
|
-
* @param {number} score - Calculated score (0-100)
|
|
45
|
-
* @param {Array<string>} triggeredSignals - List of triggered signal IDs
|
|
46
|
-
* @returns {VerdictResult}
|
|
47
|
-
*/
|
|
48
|
-
getVerdict(score, triggeredSignals = []) {
|
|
49
|
-
// Check for instant bot signals first
|
|
50
|
-
for (const signalId of triggeredSignals) {
|
|
51
|
-
if (this.instantBotSignals.has(signalId)) {
|
|
52
|
-
return {
|
|
53
|
-
verdict: Verdict.BOT,
|
|
54
|
-
score,
|
|
55
|
-
confidence: 'high',
|
|
56
|
-
reason: `Instant bot signal triggered: ${signalId}`,
|
|
57
|
-
triggeredCount: triggeredSignals.length,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Score-based verdict
|
|
63
|
-
let verdict;
|
|
64
|
-
let confidence;
|
|
65
|
-
let reason;
|
|
66
|
-
|
|
67
|
-
if (score < this.humanThreshold) {
|
|
68
|
-
verdict = Verdict.HUMAN;
|
|
69
|
-
confidence = score < 10 ? 'high' : 'medium';
|
|
70
|
-
reason = 'Low bot score';
|
|
71
|
-
} else if (score < this.suspiciousThreshold) {
|
|
72
|
-
verdict = Verdict.SUSPICIOUS;
|
|
73
|
-
confidence = 'medium';
|
|
74
|
-
reason = 'Moderate bot indicators detected';
|
|
75
|
-
} else {
|
|
76
|
-
verdict = Verdict.BOT;
|
|
77
|
-
confidence = score >= 75 ? 'high' : 'medium';
|
|
78
|
-
reason = 'High accumulation of bot indicators';
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
verdict,
|
|
83
|
-
score,
|
|
84
|
-
confidence,
|
|
85
|
-
reason,
|
|
86
|
-
triggeredCount: triggeredSignals.length,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Check if signal should instantly flag as bot.
|
|
92
|
-
* @param {string} signalId - Signal identifier
|
|
93
|
-
* @returns {boolean}
|
|
94
|
-
*/
|
|
95
|
-
isInstantBotSignal(signalId) {
|
|
96
|
-
return this.instantBotSignals.has(signalId);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Add a signal to the instant bot list.
|
|
101
|
-
* @param {string} signalId - Signal identifier
|
|
102
|
-
*/
|
|
103
|
-
addInstantBotSignal(signalId) {
|
|
104
|
-
this.instantBotSignals.add(signalId);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Update thresholds.
|
|
109
|
-
* @param {Object} thresholds - New threshold values
|
|
110
|
-
* @param {number} [thresholds.human] - New human threshold
|
|
111
|
-
* @param {number} [thresholds.suspicious] - New suspicious threshold
|
|
112
|
-
*/
|
|
113
|
-
setThresholds(thresholds) {
|
|
114
|
-
if (thresholds.human !== undefined) {
|
|
115
|
-
this.humanThreshold = thresholds.human;
|
|
116
|
-
}
|
|
117
|
-
if (thresholds.suspicious !== undefined) {
|
|
118
|
-
this.suspiciousThreshold = thresholds.suspicious;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* @typedef {Object} VerdictResult
|
|
125
|
-
* @property {string} verdict - 'human', 'suspicious', or 'bot'
|
|
126
|
-
* @property {number} score - The calculated score
|
|
127
|
-
* @property {string} confidence - 'low', 'medium', or 'high'
|
|
128
|
-
* @property {string} reason - Human-readable reason for verdict
|
|
129
|
-
* @property {number} triggeredCount - Number of triggered signals
|
|
130
|
-
*/
|
|
131
|
-
|
|
132
|
-
export { VerdictEngine, Verdict };
|
package/src/index.js
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Main entry point for the bot detection library.
|
|
3
|
-
* @module @niksbanna/bot-detector
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// Core classes
|
|
7
|
-
import { BotDetector, Verdict } from './core/BotDetector.js';
|
|
8
|
-
import { Signal } from './core/Signal.js';
|
|
9
|
-
import { ScoringEngine } from './core/ScoringEngine.js';
|
|
10
|
-
import { VerdictEngine } from './core/VerdictEngine.js';
|
|
11
|
-
|
|
12
|
-
// Environment signals
|
|
13
|
-
import {
|
|
14
|
-
WebDriverSignal,
|
|
15
|
-
HeadlessSignal,
|
|
16
|
-
NavigatorAnomalySignal,
|
|
17
|
-
PermissionsSignal,
|
|
18
|
-
} from './signals/environment/index.js';
|
|
19
|
-
|
|
20
|
-
// Behavioral signals
|
|
21
|
-
import {
|
|
22
|
-
MouseMovementSignal,
|
|
23
|
-
KeyboardPatternSignal,
|
|
24
|
-
InteractionTimingSignal,
|
|
25
|
-
ScrollBehaviorSignal,
|
|
26
|
-
} from './signals/behavior/index.js';
|
|
27
|
-
|
|
28
|
-
// Fingerprint signals
|
|
29
|
-
import {
|
|
30
|
-
PluginsSignal,
|
|
31
|
-
WebGLSignal,
|
|
32
|
-
CanvasSignal,
|
|
33
|
-
AudioContextSignal,
|
|
34
|
-
ScreenSignal,
|
|
35
|
-
} from './signals/fingerprint/index.js';
|
|
36
|
-
|
|
37
|
-
// Timing signals
|
|
38
|
-
import {
|
|
39
|
-
PageLoadSignal,
|
|
40
|
-
DOMContentTimingSignal,
|
|
41
|
-
} from './signals/timing/index.js';
|
|
42
|
-
|
|
43
|
-
// Automation framework signals
|
|
44
|
-
import {
|
|
45
|
-
PuppeteerSignal,
|
|
46
|
-
PlaywrightSignal,
|
|
47
|
-
SeleniumSignal,
|
|
48
|
-
PhantomJSSignal,
|
|
49
|
-
} from './signals/automation/index.js';
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* All built-in signal classes organized by category.
|
|
53
|
-
*/
|
|
54
|
-
const Signals = {
|
|
55
|
-
// Environment signals
|
|
56
|
-
WebDriverSignal,
|
|
57
|
-
HeadlessSignal,
|
|
58
|
-
NavigatorAnomalySignal,
|
|
59
|
-
PermissionsSignal,
|
|
60
|
-
|
|
61
|
-
// Behavioral signals
|
|
62
|
-
MouseMovementSignal,
|
|
63
|
-
KeyboardPatternSignal,
|
|
64
|
-
InteractionTimingSignal,
|
|
65
|
-
ScrollBehaviorSignal,
|
|
66
|
-
|
|
67
|
-
// Fingerprint signals
|
|
68
|
-
PluginsSignal,
|
|
69
|
-
WebGLSignal,
|
|
70
|
-
CanvasSignal,
|
|
71
|
-
AudioContextSignal,
|
|
72
|
-
ScreenSignal,
|
|
73
|
-
|
|
74
|
-
// Timing signals
|
|
75
|
-
PageLoadSignal,
|
|
76
|
-
DOMContentTimingSignal,
|
|
77
|
-
|
|
78
|
-
// Automation framework signals
|
|
79
|
-
PuppeteerSignal,
|
|
80
|
-
PlaywrightSignal,
|
|
81
|
-
SeleniumSignal,
|
|
82
|
-
PhantomJSSignal,
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Default signal instances that don't require user interaction.
|
|
87
|
-
* These are suitable for immediate detection on page load.
|
|
88
|
-
*/
|
|
89
|
-
const defaultInstantSignals = [
|
|
90
|
-
new WebDriverSignal(),
|
|
91
|
-
new HeadlessSignal(),
|
|
92
|
-
new NavigatorAnomalySignal(),
|
|
93
|
-
new PermissionsSignal(),
|
|
94
|
-
new PluginsSignal(),
|
|
95
|
-
new WebGLSignal(),
|
|
96
|
-
new CanvasSignal(),
|
|
97
|
-
new AudioContextSignal(),
|
|
98
|
-
new ScreenSignal(),
|
|
99
|
-
new PageLoadSignal(),
|
|
100
|
-
new DOMContentTimingSignal(),
|
|
101
|
-
new PuppeteerSignal(),
|
|
102
|
-
new PlaywrightSignal(),
|
|
103
|
-
new SeleniumSignal(),
|
|
104
|
-
new PhantomJSSignal(),
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Signal instances that require user interaction to be meaningful.
|
|
109
|
-
* These track mouse, keyboard, and scroll behavior over time.
|
|
110
|
-
*/
|
|
111
|
-
const defaultInteractionSignals = [
|
|
112
|
-
new MouseMovementSignal(),
|
|
113
|
-
new KeyboardPatternSignal(),
|
|
114
|
-
new InteractionTimingSignal(),
|
|
115
|
-
new ScrollBehaviorSignal(),
|
|
116
|
-
];
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* All default signal instances.
|
|
120
|
-
*/
|
|
121
|
-
const defaultSignals = [...defaultInstantSignals, ...defaultInteractionSignals];
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Create a BotDetector with all default signals registered.
|
|
125
|
-
* This is the recommended way to create a detector for most use cases.
|
|
126
|
-
*
|
|
127
|
-
* @param {Object} [options={}] - Configuration options
|
|
128
|
-
* @param {Object.<string, number>} [options.weightOverrides={}] - Override signal weights
|
|
129
|
-
* @param {number} [options.humanThreshold=20] - Score threshold for human verdict
|
|
130
|
-
* @param {number} [options.suspiciousThreshold=50] - Score threshold for suspicious verdict
|
|
131
|
-
* @param {Array<string>} [options.instantBotSignals=['webdriver', 'puppeteer', 'playwright', 'selenium', 'phantomjs']] - Signals that instantly flag as bot
|
|
132
|
-
* @param {boolean} [options.includeInteractionSignals=true] - Include signals requiring interaction
|
|
133
|
-
* @param {number} [options.detectionTimeout=5000] - Timeout for detection in ms
|
|
134
|
-
* @returns {BotDetector}
|
|
135
|
-
*
|
|
136
|
-
* @example
|
|
137
|
-
* // Basic usage
|
|
138
|
-
* const detector = createDetector();
|
|
139
|
-
* const result = await detector.detect();
|
|
140
|
-
*
|
|
141
|
-
* @example
|
|
142
|
-
* // Custom thresholds
|
|
143
|
-
* const detector = createDetector({
|
|
144
|
-
* humanThreshold: 15,
|
|
145
|
-
* suspiciousThreshold: 40,
|
|
146
|
-
* });
|
|
147
|
-
*
|
|
148
|
-
* @example
|
|
149
|
-
* // Skip interaction signals for instant detection
|
|
150
|
-
* const detector = createDetector({
|
|
151
|
-
* includeInteractionSignals: false,
|
|
152
|
-
* });
|
|
153
|
-
*/
|
|
154
|
-
function createDetector(options = {}) {
|
|
155
|
-
const {
|
|
156
|
-
includeInteractionSignals = true,
|
|
157
|
-
instantBotSignals = ['webdriver', 'puppeteer', 'playwright', 'selenium', 'phantomjs'],
|
|
158
|
-
...detectorOptions
|
|
159
|
-
} = options;
|
|
160
|
-
|
|
161
|
-
const signals = includeInteractionSignals
|
|
162
|
-
? [...defaultInstantSignals, ...defaultInteractionSignals.map(s => {
|
|
163
|
-
// Create fresh instances for interaction signals
|
|
164
|
-
const SignalClass = s.constructor;
|
|
165
|
-
return new SignalClass(s.options);
|
|
166
|
-
})]
|
|
167
|
-
: defaultInstantSignals.map(s => {
|
|
168
|
-
const SignalClass = s.constructor;
|
|
169
|
-
return new SignalClass(s.options);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
return new BotDetector({
|
|
173
|
-
signals,
|
|
174
|
-
instantBotSignals,
|
|
175
|
-
...detectorOptions,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Quick detection function for simple use cases.
|
|
181
|
-
* Creates a detector, runs detection, and returns result.
|
|
182
|
-
*
|
|
183
|
-
* @param {Object} [options={}] - Detection options
|
|
184
|
-
* @param {boolean} [options.skipInteractionSignals=false] - Skip signals requiring interaction
|
|
185
|
-
* @returns {Promise<DetectionResult>}
|
|
186
|
-
*
|
|
187
|
-
* @example
|
|
188
|
-
* const result = await detect();
|
|
189
|
-
* if (result.verdict === 'bot') {
|
|
190
|
-
* console.log('Bot detected!', result.score);
|
|
191
|
-
* }
|
|
192
|
-
*/
|
|
193
|
-
async function detect(options = {}) {
|
|
194
|
-
const detector = createDetector({
|
|
195
|
-
includeInteractionSignals: !options.skipInteractionSignals,
|
|
196
|
-
});
|
|
197
|
-
return detector.detect(options);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Quick detection that only runs instant signals (no interaction required).
|
|
202
|
-
* Suitable for immediate detection on page load.
|
|
203
|
-
*
|
|
204
|
-
* @returns {Promise<DetectionResult>}
|
|
205
|
-
*
|
|
206
|
-
* @example
|
|
207
|
-
* document.addEventListener('DOMContentLoaded', async () => {
|
|
208
|
-
* const result = await detectInstant();
|
|
209
|
-
* console.log('Verdict:', result.verdict);
|
|
210
|
-
* });
|
|
211
|
-
*/
|
|
212
|
-
async function detectInstant() {
|
|
213
|
-
return detect({ skipInteractionSignals: true });
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Export everything
|
|
217
|
-
export {
|
|
218
|
-
// Main class
|
|
219
|
-
BotDetector,
|
|
220
|
-
|
|
221
|
-
// Factory functions
|
|
222
|
-
createDetector,
|
|
223
|
-
detect,
|
|
224
|
-
detectInstant,
|
|
225
|
-
|
|
226
|
-
// Base classes
|
|
227
|
-
Signal,
|
|
228
|
-
ScoringEngine,
|
|
229
|
-
VerdictEngine,
|
|
230
|
-
|
|
231
|
-
// Enums
|
|
232
|
-
Verdict,
|
|
233
|
-
|
|
234
|
-
// Signal classes (for custom registration)
|
|
235
|
-
Signals,
|
|
236
|
-
|
|
237
|
-
// Individual signals
|
|
238
|
-
WebDriverSignal,
|
|
239
|
-
HeadlessSignal,
|
|
240
|
-
NavigatorAnomalySignal,
|
|
241
|
-
PermissionsSignal,
|
|
242
|
-
MouseMovementSignal,
|
|
243
|
-
KeyboardPatternSignal,
|
|
244
|
-
InteractionTimingSignal,
|
|
245
|
-
ScrollBehaviorSignal,
|
|
246
|
-
PluginsSignal,
|
|
247
|
-
WebGLSignal,
|
|
248
|
-
CanvasSignal,
|
|
249
|
-
AudioContextSignal,
|
|
250
|
-
ScreenSignal,
|
|
251
|
-
PageLoadSignal,
|
|
252
|
-
DOMContentTimingSignal,
|
|
253
|
-
PuppeteerSignal,
|
|
254
|
-
PlaywrightSignal,
|
|
255
|
-
SeleniumSignal,
|
|
256
|
-
PhantomJSSignal,
|
|
257
|
-
|
|
258
|
-
// Default signal instances
|
|
259
|
-
defaultSignals,
|
|
260
|
-
defaultInstantSignals,
|
|
261
|
-
defaultInteractionSignals,
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
// Default export for convenience
|
|
265
|
-
export default {
|
|
266
|
-
BotDetector,
|
|
267
|
-
createDetector,
|
|
268
|
-
detect,
|
|
269
|
-
detectInstant,
|
|
270
|
-
Signal,
|
|
271
|
-
Signals,
|
|
272
|
-
Verdict,
|
|
273
|
-
};
|