@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.
- package/dist/index.cjs +827 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +101 -2
- package/dist/index.d.ts +101 -2
- package/dist/index.mjs +825 -1
- package/dist/index.mjs.map +1 -1
- package/dist/loamly.iife.global.js +605 -1
- package/dist/loamly.iife.global.js.map +1 -1
- package/dist/loamly.iife.min.global.js +8 -1
- package/dist/loamly.iife.min.global.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +4 -1
- package/src/core.ts +201 -1
- package/src/detection/agentic-browser.ts +328 -0
- package/src/detection/behavioral-classifier.ts +489 -0
- package/src/detection/focus-blur.ts +251 -0
- package/src/detection/index.ts +21 -2
- package/src/index.ts +7 -0
- package/src/types.ts +38 -0
|
@@ -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
|
+
|