@loamly/tracker 1.6.0 → 1.8.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,328 @@
1
+ /**
2
+ * Agentic Browser Detection
3
+ *
4
+ * LOA-187: Detects AI agentic browsers like Perplexity Comet, ChatGPT Atlas,
5
+ * and other automated browsing agents.
6
+ *
7
+ * Detection methods:
8
+ * - DOM fingerprinting (Perplexity Comet overlay)
9
+ * - Mouse movement patterns (teleporting clicks)
10
+ * - CDP (Chrome DevTools Protocol) automation fingerprint
11
+ * - navigator.webdriver detection
12
+ *
13
+ * @module @loamly/tracker/detection/agentic-browser
14
+ * @license MIT
15
+ */
16
+
17
+ /**
18
+ * Agentic detection result
19
+ */
20
+ export interface AgenticDetectionResult {
21
+ /** Whether Perplexity Comet DOM element was detected */
22
+ cometDOMDetected: boolean
23
+ /** Whether CDP automation was detected */
24
+ cdpDetected: boolean
25
+ /** Mouse movement patterns */
26
+ mousePatterns: {
27
+ teleportingClicks: number
28
+ totalMovements: number
29
+ }
30
+ /** Overall agentic probability (0-1) */
31
+ agenticProbability: number
32
+ /** Detection signals */
33
+ signals: string[]
34
+ }
35
+
36
+ /**
37
+ * Perplexity Comet DOM Detector
38
+ *
39
+ * Detects the Comet browser overlay stop button which is injected
40
+ * into the DOM when Comet is actively browsing.
41
+ */
42
+ export class CometDetector {
43
+ private detected = false
44
+ private checkComplete = false
45
+ private observer: MutationObserver | null = null
46
+
47
+ /**
48
+ * Initialize detection
49
+ * @param timeout - Max time to observe for Comet DOM (default: 5s)
50
+ */
51
+ init(timeout = 5000): void {
52
+ if (typeof document === 'undefined') return
53
+
54
+ // Initial check
55
+ this.check()
56
+
57
+ if (!this.detected && document.body) {
58
+ // Observe for dynamic injection
59
+ this.observer = new MutationObserver(() => this.check())
60
+ this.observer.observe(document.body, { childList: true, subtree: true })
61
+
62
+ // Stop observing after timeout
63
+ setTimeout(() => {
64
+ if (this.observer && !this.detected) {
65
+ this.observer.disconnect()
66
+ this.observer = null
67
+ this.checkComplete = true
68
+ }
69
+ }, timeout)
70
+ }
71
+ }
72
+
73
+ private check(): void {
74
+ // Perplexity Comet injects an overlay with this class
75
+ if (document.querySelector('.pplx-agent-overlay-stop-button')) {
76
+ this.detected = true
77
+ this.checkComplete = true
78
+ if (this.observer) {
79
+ this.observer.disconnect()
80
+ this.observer = null
81
+ }
82
+ }
83
+ }
84
+
85
+ isDetected(): boolean {
86
+ return this.detected
87
+ }
88
+
89
+ isCheckComplete(): boolean {
90
+ return this.checkComplete
91
+ }
92
+
93
+ destroy(): void {
94
+ if (this.observer) {
95
+ this.observer.disconnect()
96
+ this.observer = null
97
+ }
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Mouse Movement Analyzer
103
+ *
104
+ * Detects unnatural mouse movements characteristic of automated browsers:
105
+ * - Teleporting clicks (instant large movements)
106
+ * - Perfect linear movements
107
+ * - No micro-adjustments
108
+ */
109
+ export class MouseAnalyzer {
110
+ private lastX = -1
111
+ private lastY = -1
112
+ private teleportingClicks = 0
113
+ private totalMovements = 0
114
+ private readonly teleportThreshold: number
115
+
116
+ /**
117
+ * @param teleportThreshold - Distance in pixels to consider a teleport (default: 500)
118
+ */
119
+ constructor(teleportThreshold = 500) {
120
+ this.teleportThreshold = teleportThreshold
121
+ }
122
+
123
+ /**
124
+ * Initialize mouse tracking
125
+ */
126
+ init(): void {
127
+ if (typeof document === 'undefined') return
128
+
129
+ document.addEventListener('mousemove', this.handleMove, { passive: true })
130
+ document.addEventListener('mousedown', this.handleClick, { passive: true })
131
+ }
132
+
133
+ private handleMove = (e: MouseEvent): void => {
134
+ this.totalMovements++
135
+ this.lastX = e.clientX
136
+ this.lastY = e.clientY
137
+ }
138
+
139
+ private handleClick = (e: MouseEvent): void => {
140
+ if (this.lastX !== -1 && this.lastY !== -1) {
141
+ const dx = Math.abs(e.clientX - this.lastX)
142
+ const dy = Math.abs(e.clientY - this.lastY)
143
+
144
+ // Large instant movement before click = teleporting
145
+ if (dx > this.teleportThreshold || dy > this.teleportThreshold) {
146
+ this.teleportingClicks++
147
+ }
148
+ }
149
+ this.lastX = e.clientX
150
+ this.lastY = e.clientY
151
+ }
152
+
153
+ getPatterns(): { teleportingClicks: number; totalMovements: number } {
154
+ return {
155
+ teleportingClicks: this.teleportingClicks,
156
+ totalMovements: this.totalMovements,
157
+ }
158
+ }
159
+
160
+ destroy(): void {
161
+ if (typeof document === 'undefined') return
162
+ document.removeEventListener('mousemove', this.handleMove)
163
+ document.removeEventListener('mousedown', this.handleClick)
164
+ }
165
+ }
166
+
167
+ /**
168
+ * CDP (Chrome DevTools Protocol) Automation Detector
169
+ *
170
+ * Detects headless browsers and automation tools:
171
+ * - navigator.webdriver (set by Selenium, Puppeteer, Playwright)
172
+ * - Chrome automation flags
173
+ * - Missing browser APIs
174
+ */
175
+ export class CDPDetector {
176
+ private detected = false
177
+
178
+ /**
179
+ * Run detection checks
180
+ */
181
+ detect(): boolean {
182
+ if (typeof navigator === 'undefined') return false
183
+
184
+ // Check 1: navigator.webdriver (Chrome 76+, Firefox 60+)
185
+ // Set to true by automation frameworks
186
+ if ((navigator as Navigator & { webdriver?: boolean }).webdriver) {
187
+ this.detected = true
188
+ return true
189
+ }
190
+
191
+ // Check 2: Chrome automation extension (legacy check)
192
+ if (typeof window !== 'undefined') {
193
+ const win = window as Window & {
194
+ chrome?: { runtime?: unknown }
195
+ __webdriver_evaluate?: unknown
196
+ __selenium_evaluate?: unknown
197
+ __webdriver_script_function?: unknown
198
+ __webdriver_script_func?: unknown
199
+ __webdriver_script_fn?: unknown
200
+ __fxdriver_evaluate?: unknown
201
+ __driver_unwrapped?: unknown
202
+ __webdriver_unwrapped?: unknown
203
+ __driver_evaluate?: unknown
204
+ __selenium_unwrapped?: unknown
205
+ __fxdriver_unwrapped?: unknown
206
+ }
207
+
208
+ // Selenium/WebDriver fingerprints
209
+ const automationProps = [
210
+ '__webdriver_evaluate',
211
+ '__selenium_evaluate',
212
+ '__webdriver_script_function',
213
+ '__webdriver_script_func',
214
+ '__webdriver_script_fn',
215
+ '__fxdriver_evaluate',
216
+ '__driver_unwrapped',
217
+ '__webdriver_unwrapped',
218
+ '__driver_evaluate',
219
+ '__selenium_unwrapped',
220
+ '__fxdriver_unwrapped',
221
+ ]
222
+
223
+ for (const prop of automationProps) {
224
+ if (prop in win) {
225
+ this.detected = true
226
+ return true
227
+ }
228
+ }
229
+ }
230
+
231
+ return false
232
+ }
233
+
234
+ isDetected(): boolean {
235
+ return this.detected
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Agentic Browser Analyzer
241
+ *
242
+ * Combines all detection methods into a unified result.
243
+ */
244
+ export class AgenticBrowserAnalyzer {
245
+ private cometDetector: CometDetector
246
+ private mouseAnalyzer: MouseAnalyzer
247
+ private cdpDetector: CDPDetector
248
+ private initialized = false
249
+
250
+ constructor() {
251
+ this.cometDetector = new CometDetector()
252
+ this.mouseAnalyzer = new MouseAnalyzer()
253
+ this.cdpDetector = new CDPDetector()
254
+ }
255
+
256
+ /**
257
+ * Initialize all detectors
258
+ */
259
+ init(): void {
260
+ if (this.initialized) return
261
+ this.initialized = true
262
+
263
+ this.cometDetector.init()
264
+ this.mouseAnalyzer.init()
265
+ this.cdpDetector.detect()
266
+ }
267
+
268
+ /**
269
+ * Get current detection result
270
+ */
271
+ getResult(): AgenticDetectionResult {
272
+ const signals: string[] = []
273
+ let probability = 0
274
+
275
+ // Comet detection (85% confidence)
276
+ if (this.cometDetector.isDetected()) {
277
+ signals.push('comet_dom_detected')
278
+ probability = Math.max(probability, 0.85)
279
+ }
280
+
281
+ // CDP detection (92% confidence)
282
+ if (this.cdpDetector.isDetected()) {
283
+ signals.push('cdp_detected')
284
+ probability = Math.max(probability, 0.92)
285
+ }
286
+
287
+ // Mouse patterns (78% confidence per teleport)
288
+ const mousePatterns = this.mouseAnalyzer.getPatterns()
289
+ if (mousePatterns.teleportingClicks > 0) {
290
+ signals.push(`teleporting_clicks:${mousePatterns.teleportingClicks}`)
291
+ probability = Math.max(probability, 0.78)
292
+ }
293
+
294
+ return {
295
+ cometDOMDetected: this.cometDetector.isDetected(),
296
+ cdpDetected: this.cdpDetector.isDetected(),
297
+ mousePatterns,
298
+ agenticProbability: probability,
299
+ signals,
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Cleanup resources
305
+ */
306
+ destroy(): void {
307
+ this.cometDetector.destroy()
308
+ this.mouseAnalyzer.destroy()
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Create and initialize an agentic browser analyzer
314
+ */
315
+ export function createAgenticAnalyzer(): AgenticBrowserAnalyzer {
316
+ const analyzer = new AgenticBrowserAnalyzer()
317
+
318
+ if (typeof document !== 'undefined') {
319
+ if (document.readyState === 'loading') {
320
+ document.addEventListener('DOMContentLoaded', () => analyzer.init())
321
+ } else {
322
+ analyzer.init()
323
+ }
324
+ }
325
+
326
+ return analyzer
327
+ }
328
+