@niksbanna/bot-detector 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) 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/src/core/BotDetector.js +0 -295
  7. package/src/core/ScoringEngine.js +0 -135
  8. package/src/core/Signal.js +0 -181
  9. package/src/core/VerdictEngine.js +0 -132
  10. package/src/index.js +0 -281
  11. package/src/signals/automation/PhantomJSSignal.js +0 -135
  12. package/src/signals/automation/PlaywrightSignal.js +0 -123
  13. package/src/signals/automation/PuppeteerSignal.js +0 -123
  14. package/src/signals/automation/SeleniumSignal.js +0 -148
  15. package/src/signals/automation/index.js +0 -8
  16. package/src/signals/behavior/InteractionTimingSignal.js +0 -170
  17. package/src/signals/behavior/KeyboardPatternSignal.js +0 -235
  18. package/src/signals/behavior/MouseMovementSignal.js +0 -215
  19. package/src/signals/behavior/ScrollBehaviorSignal.js +0 -236
  20. package/src/signals/behavior/index.js +0 -8
  21. package/src/signals/environment/HeadlessSignal.js +0 -97
  22. package/src/signals/environment/NavigatorAnomalySignal.js +0 -118
  23. package/src/signals/environment/PermissionsSignal.js +0 -76
  24. package/src/signals/environment/WebDriverSignal.js +0 -58
  25. package/src/signals/environment/index.js +0 -8
  26. package/src/signals/fingerprint/AudioContextSignal.js +0 -158
  27. package/src/signals/fingerprint/CanvasSignal.js +0 -133
  28. package/src/signals/fingerprint/PluginsSignal.js +0 -106
  29. package/src/signals/fingerprint/ScreenSignal.js +0 -157
  30. package/src/signals/fingerprint/WebGLSignal.js +0 -146
  31. package/src/signals/fingerprint/index.js +0 -9
  32. package/src/signals/timing/DOMContentTimingSignal.js +0 -160
  33. package/src/signals/timing/PageLoadSignal.js +0 -183
  34. package/src/signals/timing/index.js +0 -6
@@ -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,281 +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
- // singletons. If two detectors share the same signal instances, calling
162
- // detector1.reset() would corrupt detector2's cached results.
163
- const signalClasses = includeInteractionSignals
164
- ? [
165
- WebDriverSignal, HeadlessSignal, NavigatorAnomalySignal, PermissionsSignal,
166
- PluginsSignal, WebGLSignal, CanvasSignal, AudioContextSignal, ScreenSignal,
167
- PageLoadSignal, DOMContentTimingSignal,
168
- PuppeteerSignal, PlaywrightSignal, SeleniumSignal, PhantomJSSignal,
169
- MouseMovementSignal, KeyboardPatternSignal, InteractionTimingSignal, ScrollBehaviorSignal,
170
- ]
171
- : [
172
- WebDriverSignal, HeadlessSignal, NavigatorAnomalySignal, PermissionsSignal,
173
- PluginsSignal, WebGLSignal, CanvasSignal, AudioContextSignal, ScreenSignal,
174
- PageLoadSignal, DOMContentTimingSignal,
175
- PuppeteerSignal, PlaywrightSignal, SeleniumSignal, PhantomJSSignal,
176
- ];
177
-
178
- const signals = signalClasses.map(Cls => new Cls());
179
-
180
- return new BotDetector({
181
- signals,
182
- instantBotSignals,
183
- ...detectorOptions,
184
- });
185
- }
186
-
187
- /**
188
- * Quick detection function for simple use cases.
189
- * Creates a detector, runs detection, and returns result.
190
- *
191
- * @param {Object} [options={}] - Detection options
192
- * @param {boolean} [options.skipInteractionSignals=false] - Skip signals requiring interaction
193
- * @returns {Promise<DetectionResult>}
194
- *
195
- * @example
196
- * const result = await detect();
197
- * if (result.verdict === 'bot') {
198
- * console.log('Bot detected!', result.score);
199
- * }
200
- */
201
- async function detect(options = {}) {
202
- const detector = createDetector({
203
- includeInteractionSignals: !options.skipInteractionSignals,
204
- });
205
- return detector.detect(options);
206
- }
207
-
208
- /**
209
- * Quick detection that only runs instant signals (no interaction required).
210
- * Suitable for immediate detection on page load.
211
- *
212
- * @returns {Promise<DetectionResult>}
213
- *
214
- * @example
215
- * document.addEventListener('DOMContentLoaded', async () => {
216
- * const result = await detectInstant();
217
- * console.log('Verdict:', result.verdict);
218
- * });
219
- */
220
- async function detectInstant() {
221
- return detect({ skipInteractionSignals: true });
222
- }
223
-
224
- // Export everything
225
- export {
226
- // Main class
227
- BotDetector,
228
-
229
- // Factory functions
230
- createDetector,
231
- detect,
232
- detectInstant,
233
-
234
- // Base classes
235
- Signal,
236
- ScoringEngine,
237
- VerdictEngine,
238
-
239
- // Enums
240
- Verdict,
241
-
242
- // Signal classes (for custom registration)
243
- Signals,
244
-
245
- // Individual signals
246
- WebDriverSignal,
247
- HeadlessSignal,
248
- NavigatorAnomalySignal,
249
- PermissionsSignal,
250
- MouseMovementSignal,
251
- KeyboardPatternSignal,
252
- InteractionTimingSignal,
253
- ScrollBehaviorSignal,
254
- PluginsSignal,
255
- WebGLSignal,
256
- CanvasSignal,
257
- AudioContextSignal,
258
- ScreenSignal,
259
- PageLoadSignal,
260
- DOMContentTimingSignal,
261
- PuppeteerSignal,
262
- PlaywrightSignal,
263
- SeleniumSignal,
264
- PhantomJSSignal,
265
-
266
- // Default signal instances
267
- defaultSignals,
268
- defaultInstantSignals,
269
- defaultInteractionSignals,
270
- };
271
-
272
- // Default export for convenience
273
- export default {
274
- BotDetector,
275
- createDetector,
276
- detect,
277
- detectInstant,
278
- Signal,
279
- Signals,
280
- Verdict,
281
- };
@@ -1,135 +0,0 @@
1
- /**
2
- * @fileoverview Detects PhantomJS-specific artifacts.
3
- */
4
-
5
- import { Signal } from '../../core/Signal.js';
6
-
7
- /**
8
- * Detects artifacts left by PhantomJS.
9
- * PhantomJS is an older headless browser that leaves specific traces.
10
- */
11
- class PhantomJSSignal extends Signal {
12
- static id = 'phantomjs';
13
- static category = 'automation';
14
- static weight = 1.0;
15
- static description = 'Detects PhantomJS automation artifacts';
16
-
17
- async detect() {
18
- const indicators = [];
19
- let confidence = 0;
20
-
21
- // Check for PhantomJS globals
22
- if (window.callPhantom) {
23
- indicators.push('callPhantom');
24
- confidence = Math.max(confidence, 1.0);
25
- }
26
-
27
- if (window._phantom) {
28
- indicators.push('_phantom');
29
- confidence = Math.max(confidence, 1.0);
30
- }
31
-
32
- if (window.phantom) {
33
- indicators.push('phantom');
34
- confidence = Math.max(confidence, 1.0);
35
- }
36
-
37
- // Check for PhantomJS in user agent
38
- const ua = navigator.userAgent || '';
39
- if (ua.includes('PhantomJS')) {
40
- indicators.push('phantomjs-ua');
41
- confidence = Math.max(confidence, 1.0);
42
- }
43
-
44
- // Check for PhantomJS specific properties
45
- if (window.__phantomas) {
46
- indicators.push('phantomas');
47
- confidence = Math.max(confidence, 1.0);
48
- }
49
-
50
- // Check for CasperJS (built on PhantomJS)
51
- if (window.__casper) {
52
- indicators.push('casperjs');
53
- confidence = Math.max(confidence, 1.0);
54
- }
55
-
56
- if (window.casper) {
57
- indicators.push('casper-global');
58
- confidence = Math.max(confidence, 1.0);
59
- }
60
-
61
- // Check for SlimerJS (PhantomJS alternative)
62
- if (window.slimer) {
63
- indicators.push('slimerjs');
64
- confidence = Math.max(confidence, 1.0);
65
- }
66
-
67
- // Check for NightmareJS (Electron-based, similar patterns)
68
- if (window.__nightmare) {
69
- indicators.push('nightmare');
70
- confidence = Math.max(confidence, 1.0);
71
- }
72
-
73
- if (window.nightmare) {
74
- indicators.push('nightmare-global');
75
- confidence = Math.max(confidence, 1.0);
76
- }
77
-
78
- // Check for function sources containing PhantomJS
79
- try {
80
- const funcString = Function.prototype.toString.call(Function);
81
- if (funcString.includes('phantom') || funcString.includes('Phantom')) {
82
- indicators.push('function-prototype-phantom');
83
- confidence = Math.max(confidence, 0.8);
84
- }
85
- } catch (e) {
86
- // Ignore errors
87
- }
88
-
89
- // Check for PhantomJS-specific behaviors
90
- // PhantomJS has a specific way of handling errors
91
- try {
92
- throw new Error('test');
93
- } catch (e) {
94
- const stack = e.stack || '';
95
- if (stack.includes('phantom')) {
96
- indicators.push('stack-trace-phantom');
97
- confidence = Math.max(confidence, 0.9);
98
- }
99
- }
100
-
101
- // Check for PhantomJS-specific plugin handling
102
- if (navigator.plugins && navigator.plugins.length === 0) {
103
- // Combined with other PhantomJS indicators
104
- if (indicators.length > 0) {
105
- indicators.push('no-plugins-phantom');
106
- confidence = Math.max(confidence, 0.5);
107
- }
108
- }
109
-
110
- // Check for specific PhantomJS window properties.
111
- const phantomProps = [
112
- '__PHANTOM__',
113
- 'PHANTOM',
114
- ];
115
-
116
- for (const prop of phantomProps) {
117
- if (prop in window) {
118
- indicators.push(`phantom-prop-${prop.toLowerCase()}`);
119
- confidence = Math.max(confidence, 0.9);
120
- }
121
- }
122
-
123
- // Check for QtWebKit (PhantomJS engine)
124
- if (ua.includes('QtWebKit')) {
125
- indicators.push('qtwebkit');
126
- confidence = Math.max(confidence, 0.7);
127
- }
128
-
129
- const triggered = indicators.length > 0;
130
-
131
- return this.createResult(triggered, { indicators }, confidence);
132
- }
133
- }
134
-
135
- export { PhantomJSSignal };
@@ -1,123 +0,0 @@
1
- /**
2
- * @fileoverview Detects Playwright-specific artifacts.
3
- */
4
-
5
- import { Signal } from '../../core/Signal.js';
6
-
7
- /**
8
- * Detects artifacts left by Playwright automation.
9
- * Playwright injects specific objects and leaves traces.
10
- */
11
- class PlaywrightSignal extends Signal {
12
- static id = 'playwright';
13
- static category = 'automation';
14
- static weight = 1.0;
15
- static description = 'Detects Playwright automation artifacts';
16
-
17
- async detect() {
18
- const indicators = [];
19
- let confidence = 0;
20
-
21
- // Check for Playwright namespace
22
- if (window.__playwright) {
23
- indicators.push('playwright-namespace');
24
- confidence = Math.max(confidence, 1.0);
25
- }
26
-
27
- // Check for Playwright-injected objects
28
- const playwrightGlobals = [
29
- '__playwright',
30
- '__pw_manual',
31
- '__pwInitScripts',
32
- 'playwright',
33
- ];
34
-
35
- for (const global of playwrightGlobals) {
36
- if (global in window) {
37
- indicators.push(`global-${global}`);
38
- confidence = Math.max(confidence, 1.0);
39
- }
40
- }
41
-
42
- // Check for Playwright's binding pattern
43
- if (window.__playwright__binding__) {
44
- indicators.push('playwright-binding');
45
- confidence = Math.max(confidence, 1.0);
46
- }
47
-
48
- // Check for Playwright-specific user agent markers
49
- const ua = navigator.userAgent || '';
50
- if (ua.includes('Playwright') || ua.includes('HeadlessChrome')) {
51
- indicators.push('playwright-ua-marker');
52
- confidence = Math.max(confidence, ua.includes('Playwright') ? 1.0 : 0.7);
53
- }
54
-
55
- // Check for Playwright's evaluate scope pattern
56
- try {
57
- // Playwright injects __pwBinding__ functions
58
- const windowKeys = Object.keys(window);
59
- const pwBindings = windowKeys.filter(k => k.startsWith('__pw'));
60
-
61
- if (pwBindings.length > 0) {
62
- indicators.push('pw-bindings');
63
- confidence = Math.max(confidence, 1.0);
64
- }
65
- } catch (e) {
66
- // Ignore errors
67
- }
68
-
69
- // Check for Playwright's typical initialization patterns
70
- if (typeof window.__pw_date_intercepted !== 'undefined') {
71
- indicators.push('date-interception');
72
- confidence = Math.max(confidence, 0.9);
73
- }
74
-
75
- // Check for Playwright's geolocation mock
76
- if (window.__pw_geolocation__) {
77
- indicators.push('geolocation-mock');
78
- confidence = Math.max(confidence, 0.9);
79
- }
80
-
81
- // Check for Playwright's permission override
82
- if (window.__pw_permissions__) {
83
- indicators.push('permissions-override');
84
- confidence = Math.max(confidence, 0.9);
85
- }
86
-
87
- // Check CDP session artifacts
88
- if (window.__cdpSession__) {
89
- indicators.push('cdp-session');
90
- confidence = Math.max(confidence, 0.8);
91
- }
92
-
93
- // Check for error stack traces containing Playwright
94
- try {
95
- throw new Error('stack trace test');
96
- } catch (e) {
97
- const stack = e.stack || '';
98
- if (stack.includes('playwright') || stack.includes('__pw')) {
99
- indicators.push('stack-trace-playwright');
100
- confidence = Math.max(confidence, 0.8);
101
- }
102
- }
103
-
104
- // Check for Playwright's locale/timezone mocking
105
- try {
106
- const date = new Date();
107
- const localeString = date.toLocaleString();
108
- // Playwright often mocks timezone
109
- if (window.__pwTimezone__) {
110
- indicators.push('timezone-mock');
111
- confidence = Math.max(confidence, 0.8);
112
- }
113
- } catch (e) {
114
- // Ignore errors
115
- }
116
-
117
- const triggered = indicators.length > 0;
118
-
119
- return this.createResult(triggered, { indicators }, confidence);
120
- }
121
- }
122
-
123
- export { PlaywrightSignal };