@niksbanna/bot-detector 1.0.4 → 1.1.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.
@@ -0,0 +1,474 @@
1
+ /**
2
+ * TypeScript declarations for @niksbanna/bot-detector
3
+ */
4
+
5
+ // ── Primitives ────────────────────────────────────────────────────────────────
6
+
7
+ /** The three possible verdicts produced by the detector. */
8
+ export type VerdictValue = 'human' | 'suspicious' | 'bot';
9
+
10
+ /** Confidence band returned alongside a verdict. */
11
+ export type ConfidenceLevel = 'low' | 'medium' | 'high';
12
+
13
+ /** Possible verdict values, as a const object. */
14
+ export declare const Verdict: {
15
+ readonly HUMAN: 'human';
16
+ readonly SUSPICIOUS: 'suspicious';
17
+ readonly BOT: 'bot';
18
+ };
19
+
20
+ // ── Result shapes ─────────────────────────────────────────────────────────────
21
+
22
+ /** Result returned by a single signal's detect() call. */
23
+ export interface SignalResult {
24
+ /** Whether this signal was triggered (indicates bot behaviour). */
25
+ triggered: boolean;
26
+ /** Optional value or evidence attached to the detection. */
27
+ value: unknown;
28
+ /** Certainty of this result, clamped to [0, 1]. */
29
+ confidence: number;
30
+ /** Present only when detection failed and was swallowed gracefully. */
31
+ error?: string;
32
+ }
33
+
34
+ /** Per-signal entry inside DetectionResult.signals. */
35
+ export interface SignalDetail extends SignalResult {
36
+ category: string;
37
+ weight: number;
38
+ description: string;
39
+ }
40
+
41
+ /** One row in the score-contribution breakdown. */
42
+ export interface ScoreBreakdownEntry {
43
+ signalId: string;
44
+ triggered: boolean;
45
+ confidence: number;
46
+ weight: number;
47
+ contribution: number;
48
+ /** Percentage of the final score contributed by this signal (formatted string). */
49
+ percentOfScore: string;
50
+ }
51
+
52
+ /** Full result returned by BotDetector.detect(). */
53
+ export interface DetectionResult {
54
+ /** The final classification verdict. */
55
+ verdict: VerdictValue;
56
+ /** Weighted bot-probability score, 0–100. */
57
+ score: number;
58
+ /** Confidence band of the verdict. */
59
+ confidence: ConfidenceLevel;
60
+ /** Human-readable explanation of the verdict. */
61
+ reason: string;
62
+ /** Map of signal ID → detailed result for every evaluated signal. */
63
+ signals: Record<string, SignalDetail>;
64
+ /** Score-contribution breakdown, sorted descending by contribution. */
65
+ breakdown: ScoreBreakdownEntry[];
66
+ /** IDs of every signal that was triggered. */
67
+ triggeredSignals: string[];
68
+ /** Total number of signals that were evaluated. */
69
+ totalSignals: number;
70
+ /** Number of signals that were triggered. */
71
+ triggeredCount: number;
72
+ /** Unix timestamp (ms) at which detection completed. */
73
+ timestamp: number;
74
+ /** Wall-clock time taken for the full detection run, in milliseconds. */
75
+ detectionTimeMs: number;
76
+ }
77
+
78
+ // ── Options ───────────────────────────────────────────────────────────────────
79
+
80
+ /** Options accepted by BotDetector and createDetector(). */
81
+ export interface DetectorOptions {
82
+ /**
83
+ * Scores strictly below this value receive a 'human' verdict.
84
+ * @default 20
85
+ */
86
+ humanThreshold?: number;
87
+ /**
88
+ * Scores at or above this value receive a 'bot' verdict.
89
+ * Scores between humanThreshold and suspiciousThreshold are 'suspicious'.
90
+ * @default 50
91
+ */
92
+ suspiciousThreshold?: number;
93
+ /**
94
+ * Maximum time (ms) to wait for a single signal before treating it as
95
+ * non-triggered.
96
+ * @default 5000
97
+ */
98
+ detectionTimeout?: number;
99
+ /**
100
+ * Override the weight of specific signals by their string ID.
101
+ * @example { 'webdriver': 0.5, 'canvas': 0.3 }
102
+ */
103
+ weightOverrides?: Record<string, number>;
104
+ /**
105
+ * Signal IDs that immediately produce a 'bot' verdict when triggered,
106
+ * regardless of the numeric score.
107
+ * @default ['webdriver', 'puppeteer', 'playwright', 'selenium', 'phantomjs']
108
+ */
109
+ instantBotSignals?: string[];
110
+ /**
111
+ * Whether to include mouse/keyboard/scroll behavioural signals.
112
+ * Set to false for instant detection that does not require user interaction.
113
+ * @default true
114
+ */
115
+ includeInteractionSignals?: boolean;
116
+ /** Pre-instantiated signals to register. Used internally by createDetector(). */
117
+ signals?: Signal[];
118
+ }
119
+
120
+ /** Options accepted by the detect() convenience function. */
121
+ export interface DetectOptions extends Omit<DetectorOptions, 'signals'> {
122
+ /**
123
+ * When true, skips signals that require user interaction (mouse, keyboard,
124
+ * scroll). Equivalent to detectInstant().
125
+ */
126
+ skipInteractionSignals?: boolean;
127
+ }
128
+
129
+ // ── Core classes ──────────────────────────────────────────────────────────────
130
+
131
+ /**
132
+ * Abstract base class for all bot-detection signals.
133
+ *
134
+ * Extend this class and implement detect() to create a custom signal:
135
+ *
136
+ * ```ts
137
+ * class MySignal extends Signal {
138
+ * static id = 'my-signal';
139
+ * static category = 'custom';
140
+ * static weight = 0.7;
141
+ * static description = 'Detects my custom bot pattern';
142
+ *
143
+ * async detect(): Promise<SignalResult> {
144
+ * const suspicious = /* your logic *\/;
145
+ * return this.createResult(suspicious, { detail: 'value' }, 0.9);
146
+ * }
147
+ * }
148
+ * ```
149
+ */
150
+ export declare abstract class Signal {
151
+ static id: string;
152
+ static category: string;
153
+ static weight: number;
154
+ static description: string;
155
+ static requiresInteraction: boolean;
156
+
157
+ constructor(options?: Record<string, unknown>);
158
+
159
+ get id(): string;
160
+ get category(): string;
161
+ get weight(): number;
162
+ get description(): string;
163
+ get requiresInteraction(): boolean;
164
+ get lastResult(): SignalResult | null;
165
+
166
+ /** Implement your detection logic here. Must be overridden by subclasses. */
167
+ abstract detect(): Promise<SignalResult>;
168
+
169
+ /**
170
+ * Calls detect() and caches the result. On error, returns a non-triggered
171
+ * result rather than throwing — signals are fail-safe by design.
172
+ */
173
+ run(): Promise<SignalResult>;
174
+
175
+ /** Clears cached state so the signal can be run again cleanly. */
176
+ reset(): void;
177
+
178
+ /**
179
+ * Helper that constructs a well-typed SignalResult.
180
+ * @param triggered - Whether the signal fired.
181
+ * @param value - Optional evidence object. Defaults to null.
182
+ * @param confidence - Certainty in [0, 1]. Defaults to 1.
183
+ */
184
+ protected createResult(
185
+ triggered: boolean,
186
+ value?: unknown,
187
+ confidence?: number,
188
+ ): SignalResult;
189
+ }
190
+
191
+ /** Calculates a weighted bot-probability score from accumulated signal results. */
192
+ export declare class ScoringEngine {
193
+ constructor(options?: {
194
+ weightOverrides?: Record<string, number>;
195
+ maxScore?: number;
196
+ });
197
+
198
+ /** Returns the effective weight for a signal, honouring any override. */
199
+ getWeight(signalId: string, defaultWeight: number): number;
200
+
201
+ /** Accumulates one signal's result into the pending score. */
202
+ addResult(signalId: string, result: SignalResult, weight: number): void;
203
+
204
+ /** Computes and returns the final score in [0, maxScore]. */
205
+ calculate(): number;
206
+
207
+ /** Returns each signal's score contribution, sorted descending. */
208
+ getBreakdown(): ScoreBreakdownEntry[];
209
+
210
+ /** Returns the IDs of every signal that was triggered. */
211
+ getTriggeredSignals(): string[];
212
+
213
+ /** Returns the count of triggered signals. */
214
+ getTriggeredCount(): number;
215
+
216
+ /** Clears all accumulated results. */
217
+ reset(): void;
218
+ }
219
+
220
+ /** Converts a numeric score and triggered-signal list into a final verdict. */
221
+ export declare class VerdictEngine {
222
+ static readonly DEFAULT_THRESHOLDS: { human: number; suspicious: number };
223
+
224
+ constructor(options?: {
225
+ humanThreshold?: number;
226
+ suspiciousThreshold?: number;
227
+ instantBotSignals?: string[];
228
+ });
229
+
230
+ /**
231
+ * Returns the verdict for the given score and triggered-signal set.
232
+ * Instant-bot signals take priority over the numeric score.
233
+ */
234
+ getVerdict(
235
+ score: number,
236
+ triggeredSignals?: string[],
237
+ ): Pick<DetectionResult, 'verdict' | 'score' | 'confidence' | 'reason' | 'triggeredCount'>;
238
+
239
+ isInstantBotSignal(signalId: string): boolean;
240
+ addInstantBotSignal(signalId: string): void;
241
+ setThresholds(thresholds: { human?: number; suspicious?: number }): void;
242
+ }
243
+
244
+ /**
245
+ * Main bot-detection orchestrator.
246
+ *
247
+ * Prefer the createDetector() factory for typical use cases:
248
+ * ```ts
249
+ * const detector = createDetector({ humanThreshold: 15 });
250
+ * const result = await detector.detect();
251
+ * ```
252
+ */
253
+ export declare class BotDetector {
254
+ constructor(options?: DetectorOptions);
255
+
256
+ /** Registers a signal. Throws if a signal with the same ID is already registered. */
257
+ registerSignal(signal: Signal): this;
258
+ /** Registers multiple signals at once. */
259
+ registerSignals(signals: Signal[]): this;
260
+ /** Removes a signal by ID. Returns true if it was found and removed. */
261
+ unregisterSignal(signalId: string): boolean;
262
+ /** Returns a registered signal by ID, or undefined. */
263
+ getSignal(signalId: string): Signal | undefined;
264
+ /** Returns all registered signals. */
265
+ getSignals(): Signal[];
266
+ /** Returns all registered signals that belong to a given category. */
267
+ getSignalsByCategory(category: string): Signal[];
268
+
269
+ /**
270
+ * Runs all registered signals concurrently and returns a DetectionResult.
271
+ * Throws if detection is already in progress on this instance.
272
+ */
273
+ detect(options?: { skipInteractionSignals?: boolean }): Promise<DetectionResult>;
274
+
275
+ /** Returns the result of the most recent detect() call, or null. */
276
+ getLastDetection(): DetectionResult | null;
277
+ /** Returns the score from the last detect() call, or 0. */
278
+ getScore(): number;
279
+ /** Returns the triggered signal IDs from the last detect() call. */
280
+ getTriggeredSignals(): string[];
281
+ /** True while a detect() call is in progress. */
282
+ isRunning(): boolean;
283
+ /** Resets the detector and all registered signals to their initial state. */
284
+ reset(): void;
285
+ /** Updates thresholds or timeout after construction. */
286
+ configure(options: {
287
+ humanThreshold?: number;
288
+ suspiciousThreshold?: number;
289
+ detectionTimeout?: number;
290
+ }): void;
291
+
292
+ /**
293
+ * @deprecated Use createDetector() from '@niksbanna/bot-detector' instead.
294
+ * This method always throws to prevent silent empty-detector bugs.
295
+ */
296
+ static withDefaults(): never;
297
+ }
298
+
299
+ // ── Built-in signal classes ───────────────────────────────────────────────────
300
+
301
+ // Environment
302
+ export declare class WebDriverSignal extends Signal {
303
+ static readonly id: 'webdriver';
304
+ static readonly category: 'environment';
305
+ }
306
+ export declare class HeadlessSignal extends Signal {
307
+ static readonly id: 'headless';
308
+ static readonly category: 'environment';
309
+ }
310
+ export declare class NavigatorAnomalySignal extends Signal {
311
+ static readonly id: 'navigator-anomaly';
312
+ static readonly category: 'environment';
313
+ }
314
+ export declare class PermissionsSignal extends Signal {
315
+ static readonly id: 'permissions';
316
+ static readonly category: 'environment';
317
+ }
318
+
319
+ // Behavioural
320
+ export declare class MouseMovementSignal extends Signal {
321
+ static readonly id: 'mouse-movement';
322
+ static readonly category: 'behavior';
323
+ static readonly requiresInteraction: true;
324
+ startTracking(): void;
325
+ stopTracking(): void;
326
+ }
327
+ export declare class KeyboardPatternSignal extends Signal {
328
+ static readonly id: 'keyboard-pattern';
329
+ static readonly category: 'behavior';
330
+ static readonly requiresInteraction: true;
331
+ startTracking(): void;
332
+ stopTracking(): void;
333
+ }
334
+ export declare class InteractionTimingSignal extends Signal {
335
+ static readonly id: 'interaction-timing';
336
+ static readonly category: 'behavior';
337
+ static readonly requiresInteraction: true;
338
+ startTracking(): void;
339
+ stopTracking(): void;
340
+ }
341
+ export declare class ScrollBehaviorSignal extends Signal {
342
+ static readonly id: 'scroll-behavior';
343
+ static readonly category: 'behavior';
344
+ static readonly requiresInteraction: true;
345
+ startTracking(): void;
346
+ stopTracking(): void;
347
+ }
348
+
349
+ // Fingerprint
350
+ export declare class PluginsSignal extends Signal {
351
+ static readonly id: 'plugins';
352
+ static readonly category: 'fingerprint';
353
+ }
354
+ export declare class WebGLSignal extends Signal {
355
+ static readonly id: 'webgl';
356
+ static readonly category: 'fingerprint';
357
+ }
358
+ export declare class CanvasSignal extends Signal {
359
+ static readonly id: 'canvas';
360
+ static readonly category: 'fingerprint';
361
+ }
362
+ export declare class AudioContextSignal extends Signal {
363
+ static readonly id: 'audio-context';
364
+ static readonly category: 'fingerprint';
365
+ }
366
+ export declare class ScreenSignal extends Signal {
367
+ static readonly id: 'screen';
368
+ static readonly category: 'fingerprint';
369
+ }
370
+
371
+ // Timing
372
+ export declare class PageLoadSignal extends Signal {
373
+ static readonly id: 'page-load';
374
+ static readonly category: 'timing';
375
+ }
376
+ export declare class DOMContentTimingSignal extends Signal {
377
+ static readonly id: 'dom-content-timing';
378
+ static readonly category: 'timing';
379
+ }
380
+
381
+ // Automation frameworks
382
+ export declare class PuppeteerSignal extends Signal {
383
+ static readonly id: 'puppeteer';
384
+ static readonly category: 'automation';
385
+ }
386
+ export declare class PlaywrightSignal extends Signal {
387
+ static readonly id: 'playwright';
388
+ static readonly category: 'automation';
389
+ }
390
+ export declare class SeleniumSignal extends Signal {
391
+ static readonly id: 'selenium';
392
+ static readonly category: 'automation';
393
+ }
394
+ export declare class PhantomJSSignal extends Signal {
395
+ static readonly id: 'phantomjs';
396
+ static readonly category: 'automation';
397
+ }
398
+
399
+ // ── Namespace of all signal classes ──────────────────────────────────────────
400
+
401
+ /** All built-in signal classes, keyed by class name. */
402
+ export declare const Signals: {
403
+ WebDriverSignal: typeof WebDriverSignal;
404
+ HeadlessSignal: typeof HeadlessSignal;
405
+ NavigatorAnomalySignal: typeof NavigatorAnomalySignal;
406
+ PermissionsSignal: typeof PermissionsSignal;
407
+ MouseMovementSignal: typeof MouseMovementSignal;
408
+ KeyboardPatternSignal: typeof KeyboardPatternSignal;
409
+ InteractionTimingSignal: typeof InteractionTimingSignal;
410
+ ScrollBehaviorSignal: typeof ScrollBehaviorSignal;
411
+ PluginsSignal: typeof PluginsSignal;
412
+ WebGLSignal: typeof WebGLSignal;
413
+ CanvasSignal: typeof CanvasSignal;
414
+ AudioContextSignal: typeof AudioContextSignal;
415
+ ScreenSignal: typeof ScreenSignal;
416
+ PageLoadSignal: typeof PageLoadSignal;
417
+ DOMContentTimingSignal: typeof DOMContentTimingSignal;
418
+ PuppeteerSignal: typeof PuppeteerSignal;
419
+ PlaywrightSignal: typeof PlaywrightSignal;
420
+ SeleniumSignal: typeof SeleniumSignal;
421
+ PhantomJSSignal: typeof PhantomJSSignal;
422
+ };
423
+
424
+ /** All default signal instances (instant + interaction). */
425
+ export declare const defaultSignals: Signal[];
426
+ /** Default signal instances that run without user interaction. */
427
+ export declare const defaultInstantSignals: Signal[];
428
+ /** Default signal instances that require user interaction. */
429
+ export declare const defaultInteractionSignals: Signal[];
430
+
431
+ // ── Public API ────────────────────────────────────────────────────────────────
432
+
433
+ /**
434
+ * Create a BotDetector pre-loaded with all built-in signals.
435
+ * This is the recommended entry point for most use cases.
436
+ *
437
+ * @example
438
+ * const detector = createDetector({ humanThreshold: 15, suspiciousThreshold: 40 });
439
+ * const result = await detector.detect();
440
+ */
441
+ export declare function createDetector(options?: DetectorOptions): BotDetector;
442
+
443
+ /**
444
+ * One-shot detection: creates a fresh detector, runs it, and returns the result.
445
+ *
446
+ * @example
447
+ * const result = await detect();
448
+ * if (result.verdict === 'bot') { ... }
449
+ */
450
+ export declare function detect(options?: DetectOptions): Promise<DetectionResult>;
451
+
452
+ /**
453
+ * Runs detection using only instant (non-interaction) signals.
454
+ * Safe to call immediately on page load — does not wait for mouse or keyboard events.
455
+ *
456
+ * @example
457
+ * document.addEventListener('DOMContentLoaded', async () => {
458
+ * const result = await detectInstant();
459
+ * console.log(result.verdict);
460
+ * });
461
+ */
462
+ export declare function detectInstant(): Promise<DetectionResult>;
463
+
464
+ /** Default export for environments that prefer a single namespace import. */
465
+ declare const _default: {
466
+ BotDetector: typeof BotDetector;
467
+ createDetector: typeof createDetector;
468
+ detect: typeof detect;
469
+ detectInstant: typeof detectInstant;
470
+ Signal: typeof Signal;
471
+ Signals: typeof Signals;
472
+ Verdict: typeof Verdict;
473
+ };
474
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@niksbanna/bot-detector",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "Production-grade client-side bot detection system using signal-based scoring",
5
5
  "main": "dist/bot-detector.cjs.js",
6
6
  "module": "dist/bot-detector.esm.js",