@el7ven/cookie-kit 0.3.1 → 0.3.2

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,488 @@
1
+ /**
2
+ * TrackerDetector — automatically detects trackers on the page
3
+ * @module TrackerDetector
4
+ */
5
+
6
+ import { getAllTrackerPatterns } from './TrackerPatterns.js'
7
+
8
+ export class TrackerDetector {
9
+ constructor(config = {}) {
10
+ this.config = {
11
+ detectOnInit: true,
12
+ detectFromScripts: true,
13
+ detectFromGlobals: true,
14
+ detectFromDataLayer: true,
15
+ detectFromCookies: false, // Optional: can be privacy-invasive
16
+ detectFromNetwork: false, // Advanced: intercept fetch/XHR
17
+ observeDOM: true, // MutationObserver for dynamic trackers
18
+ confidenceScoring: true, // Calculate confidence scores
19
+ debug: false,
20
+ ...config
21
+ }
22
+
23
+ this.detectedTrackers = new Map() // id -> tracker info
24
+ this.observer = null
25
+ this.networkInterceptors = []
26
+ this.detectTimer = null // Debounce timer
27
+ }
28
+
29
+ /**
30
+ * Run full detection
31
+ * @returns {Array} Detected trackers
32
+ */
33
+ /**
34
+ * Initialize detector with observers and interceptors
35
+ */
36
+ initialize() {
37
+ // Start DOM observation
38
+ if (this.config.observeDOM) {
39
+ this._startDOMObserver()
40
+ }
41
+
42
+ // Setup network interception
43
+ if (this.config.detectFromNetwork) {
44
+ this._setupNetworkInterception()
45
+ }
46
+
47
+ // Initial detection
48
+ this.detect()
49
+ }
50
+
51
+ /**
52
+ * Run full detection
53
+ * @returns {Array} Detected trackers
54
+ */
55
+ detect() {
56
+ this._debug('Starting tracker detection...')
57
+
58
+ const detectionResults = new Map() // id -> { sources: [], confidence: 0 }
59
+
60
+ // 1. Detect from script sources
61
+ if (this.config.detectFromScripts) {
62
+ const scriptTrackers = this._detectFromScripts()
63
+ scriptTrackers.forEach(({ id, confidence }) => {
64
+ this._addDetectionResult(detectionResults, id, 'script', confidence)
65
+ })
66
+ }
67
+
68
+ // 2. Detect from global variables
69
+ if (this.config.detectFromGlobals) {
70
+ const globalTrackers = this._detectFromGlobals()
71
+ globalTrackers.forEach(({ id, confidence }) => {
72
+ this._addDetectionResult(detectionResults, id, 'global', confidence)
73
+ })
74
+ }
75
+
76
+ // 3. Detect from dataLayer
77
+ if (this.config.detectFromDataLayer) {
78
+ const dataLayerTrackers = this._detectFromDataLayer()
79
+ dataLayerTrackers.forEach(({ id, confidence }) => {
80
+ this._addDetectionResult(detectionResults, id, 'dataLayer', confidence)
81
+ })
82
+ }
83
+
84
+ // 4. Detect from cookies (optional)
85
+ if (this.config.detectFromCookies) {
86
+ const cookieTrackers = this._detectFromCookies()
87
+ cookieTrackers.forEach(({ id, confidence }) => {
88
+ this._addDetectionResult(detectionResults, id, 'cookie', confidence)
89
+ })
90
+ }
91
+
92
+ // Store detected trackers with confidence scores
93
+ const patterns = getAllTrackerPatterns()
94
+ detectionResults.forEach((result, id) => {
95
+ const pattern = patterns.find(p => p.id === id)
96
+ if (pattern) {
97
+ const confidence = this.config.confidenceScoring
98
+ ? this._calculateConfidence(result.sources)
99
+ : 1.0
100
+
101
+ // Update or add tracker (prevent duplicates, update confidence)
102
+ if (this.detectedTrackers.has(id)) {
103
+ const existing = this.detectedTrackers.get(id)
104
+ // Update confidence to maximum
105
+ existing.confidence = Math.max(existing.confidence, confidence)
106
+ // Merge detection sources
107
+ existing.detectedBy = [...new Set([...existing.detectedBy, ...result.sources])]
108
+ } else {
109
+ this.detectedTrackers.set(id, {
110
+ ...pattern,
111
+ detectedAt: new Date().toISOString(),
112
+ detectedBy: result.sources,
113
+ confidence
114
+ })
115
+ }
116
+ }
117
+ })
118
+
119
+ const trackers = Array.from(this.detectedTrackers.values())
120
+ this._debug(`Detected ${trackers.length} trackers:`, trackers.map(t => `${t.name} (${(t.confidence * 100).toFixed(0)}%)`))
121
+
122
+ return trackers
123
+ }
124
+
125
+ /**
126
+ * Detect trackers from script sources
127
+ * @private
128
+ */
129
+ _detectFromScripts() {
130
+ const detected = []
131
+ const scripts = document.querySelectorAll('script[src]')
132
+ const patterns = getAllTrackerPatterns()
133
+
134
+ scripts.forEach(script => {
135
+ const src = script.getAttribute('src') || ''
136
+
137
+ patterns.forEach(pattern => {
138
+ pattern.scripts.forEach(scriptPattern => {
139
+ if (src.includes(scriptPattern)) {
140
+ detected.push({ id: pattern.id, confidence: 0.9 })
141
+ this._debug(`Detected ${pattern.name} from script:`, src)
142
+ }
143
+ })
144
+ })
145
+ })
146
+
147
+ return detected
148
+ }
149
+
150
+ /**
151
+ * Detect trackers from global variables
152
+ * @private
153
+ */
154
+ _detectFromGlobals() {
155
+ const detected = []
156
+ const patterns = getAllTrackerPatterns()
157
+
158
+ patterns.forEach(pattern => {
159
+ pattern.globals.forEach(globalVar => {
160
+ if (typeof window !== 'undefined' && window[globalVar] !== undefined) {
161
+ detected.push({ id: pattern.id, confidence: 1.0 })
162
+ this._debug(`Detected ${pattern.name} from global:`, globalVar)
163
+ }
164
+ })
165
+ })
166
+
167
+ return detected
168
+ }
169
+
170
+ /**
171
+ * Detect trackers from dataLayer
172
+ * @private
173
+ */
174
+ _detectFromDataLayer() {
175
+ const detected = []
176
+
177
+ if (typeof window === 'undefined' || !window.dataLayer) {
178
+ return detected
179
+ }
180
+
181
+ const patterns = getAllTrackerPatterns()
182
+ patterns.forEach(pattern => {
183
+ if (pattern.dataLayer.length === 0) return
184
+
185
+ pattern.dataLayer.forEach(key => {
186
+ const hasKey = window.dataLayer.some(item => {
187
+ if (typeof item === 'object') {
188
+ return key in item || JSON.stringify(item).includes(key)
189
+ }
190
+ return false
191
+ })
192
+
193
+ if (hasKey) {
194
+ detected.push({ id: pattern.id, confidence: 0.8 })
195
+ this._debug(`Detected ${pattern.name} from dataLayer:`, key)
196
+ }
197
+ })
198
+ })
199
+
200
+ return detected
201
+ }
202
+
203
+ /**
204
+ * Detect trackers from cookies
205
+ * @private
206
+ */
207
+ _detectFromCookies() {
208
+ const detected = []
209
+
210
+ if (typeof document === 'undefined') {
211
+ return detected
212
+ }
213
+
214
+ const cookies = document.cookie
215
+ const patterns = getAllTrackerPatterns()
216
+
217
+ patterns.forEach(pattern => {
218
+ pattern.cookies.forEach(cookiePattern => {
219
+ if (cookies.includes(cookiePattern)) {
220
+ detected.push({ id: pattern.id, confidence: 0.7 })
221
+ this._debug(`Detected ${pattern.name} from cookie:`, cookiePattern)
222
+ }
223
+ })
224
+ })
225
+
226
+ return detected
227
+ }
228
+
229
+ /**
230
+ * Get detected trackers
231
+ * @returns {Array}
232
+ */
233
+ getDetected() {
234
+ return Array.from(this.detectedTrackers.values())
235
+ }
236
+
237
+ /**
238
+ * Get detected trackers by category
239
+ * @param {string} category - Category name
240
+ * @returns {Array}
241
+ */
242
+ getDetectedByCategory(category) {
243
+ return this.getDetected().filter(tracker => tracker.category === category)
244
+ }
245
+
246
+ /**
247
+ * Check if specific tracker is detected
248
+ * @param {string} id - Tracker ID
249
+ * @returns {boolean}
250
+ */
251
+ isDetected(id) {
252
+ return this.detectedTrackers.has(id)
253
+ }
254
+
255
+ /**
256
+ * Get detection stats
257
+ * @returns {Object}
258
+ */
259
+ getStats() {
260
+ const trackers = this.getDetected()
261
+ const byCategory = {}
262
+
263
+ trackers.forEach(tracker => {
264
+ if (!byCategory[tracker.category]) {
265
+ byCategory[tracker.category] = []
266
+ }
267
+ byCategory[tracker.category].push(tracker.name)
268
+ })
269
+
270
+ return {
271
+ total: trackers.length,
272
+ byCategory,
273
+ trackers: trackers.map(t => ({
274
+ id: t.id,
275
+ name: t.name,
276
+ category: t.category
277
+ }))
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Clear detected trackers
283
+ */
284
+ clear() {
285
+ this.detectedTrackers.clear()
286
+ }
287
+
288
+ /**
289
+ * Re-run detection
290
+ * @returns {Array}
291
+ */
292
+ refresh() {
293
+ this.clear()
294
+ return this.detect()
295
+ }
296
+
297
+ /**
298
+ * Destroy detector and cleanup
299
+ */
300
+ destroy() {
301
+ if (this.observer) {
302
+ this.observer.disconnect()
303
+ this.observer = null
304
+ }
305
+
306
+ this._removeNetworkInterception()
307
+ this.clear()
308
+ }
309
+
310
+ // --- Advanced Detection Methods ---
311
+
312
+ /**
313
+ * Start MutationObserver for dynamic trackers
314
+ * @private
315
+ */
316
+ _startDOMObserver() {
317
+ if (typeof MutationObserver === 'undefined') {
318
+ return
319
+ }
320
+
321
+ this.observer = new MutationObserver((mutations) => {
322
+ let hasNewScripts = false
323
+
324
+ mutations.forEach((mutation) => {
325
+ mutation.addedNodes.forEach((node) => {
326
+ if (node.nodeType === 1) {
327
+ if (node.tagName === 'SCRIPT' || node.querySelector?.('script')) {
328
+ hasNewScripts = true
329
+ }
330
+ }
331
+ })
332
+ })
333
+
334
+ if (hasNewScripts) {
335
+ this._debug('New scripts detected, scheduling re-scan...')
336
+ this._scheduleDetect() // Debounced
337
+ }
338
+ })
339
+
340
+ this.observer.observe(document.documentElement, {
341
+ childList: true,
342
+ subtree: true
343
+ })
344
+
345
+ this._debug('DOM observer started')
346
+ }
347
+
348
+ /**
349
+ * Setup network interception (fetch/XHR)
350
+ * SAFE: preserves original context and bindings
351
+ * @private
352
+ */
353
+ _setupNetworkInterception() {
354
+ if (typeof window === 'undefined') return
355
+
356
+ const patterns = getAllTrackerPatterns()
357
+
358
+ // Intercept fetch (SAFE: bind original context)
359
+ if (window.fetch) {
360
+ const originalFetch = window.fetch.bind(window)
361
+ window.fetch = async (...args) => {
362
+ const url = args[0]?.toString() || ''
363
+ this._checkNetworkRequest(url, patterns)
364
+ return originalFetch(...args)
365
+ }
366
+ this.networkInterceptors.push({ type: 'fetch', original: originalFetch })
367
+ }
368
+
369
+ // Intercept XMLHttpRequest (SAFE: preserve prototype)
370
+ if (window.XMLHttpRequest) {
371
+ const originalOpen = XMLHttpRequest.prototype.open
372
+ const self = this
373
+ XMLHttpRequest.prototype.open = function(...args) {
374
+ const url = args[1]?.toString() || ''
375
+ self._checkNetworkRequest(url, patterns)
376
+ return originalOpen.apply(this, args)
377
+ }
378
+ this.networkInterceptors.push({ type: 'xhr', original: originalOpen })
379
+ }
380
+
381
+ this._debug('Network interception enabled (safe mode)')
382
+ }
383
+
384
+ /**
385
+ * Check network request against patterns
386
+ * @private
387
+ */
388
+ _checkNetworkRequest(url, patterns) {
389
+ patterns.forEach(pattern => {
390
+ pattern.network?.forEach(networkPattern => {
391
+ if (url.includes(networkPattern)) {
392
+ this._debug(`Detected ${pattern.name} from network:`, url)
393
+
394
+ // Update or add tracker
395
+ if (this.detectedTrackers.has(pattern.id)) {
396
+ const existing = this.detectedTrackers.get(pattern.id)
397
+ existing.confidence = Math.max(existing.confidence, 0.85)
398
+ if (!existing.detectedBy.includes('network')) {
399
+ existing.detectedBy.push('network')
400
+ }
401
+ } else {
402
+ this.detectedTrackers.set(pattern.id, {
403
+ ...pattern,
404
+ detectedAt: new Date().toISOString(),
405
+ detectedBy: ['network'],
406
+ confidence: 0.85
407
+ })
408
+ }
409
+ }
410
+ })
411
+ })
412
+ }
413
+
414
+ /**
415
+ * Remove network interception
416
+ * @private
417
+ */
418
+ _removeNetworkInterception() {
419
+ this.networkInterceptors.forEach(({ type, original }) => {
420
+ if (type === 'fetch' && window.fetch) {
421
+ window.fetch = original
422
+ } else if (type === 'xhr' && XMLHttpRequest.prototype.open) {
423
+ XMLHttpRequest.prototype.open = original
424
+ }
425
+ })
426
+ this.networkInterceptors = []
427
+ }
428
+
429
+ /**
430
+ * Add detection result
431
+ * @private
432
+ */
433
+ _addDetectionResult(results, id, source, confidence) {
434
+ if (!results.has(id)) {
435
+ results.set(id, { sources: [], confidences: [] })
436
+ }
437
+ const result = results.get(id)
438
+ result.sources.push(source)
439
+ result.confidences.push(confidence)
440
+ }
441
+
442
+ /**
443
+ * Calculate overall confidence score
444
+ * @private
445
+ */
446
+ _calculateConfidence(sources) {
447
+ if (sources.length === 0) return 0
448
+
449
+ // Weight different sources
450
+ const weights = {
451
+ script: 0.9,
452
+ global: 1.0,
453
+ dataLayer: 0.8,
454
+ cookie: 0.7,
455
+ network: 0.85
456
+ }
457
+
458
+ let totalWeight = 0
459
+ let weightedSum = 0
460
+
461
+ sources.forEach(source => {
462
+ const weight = weights[source] || 0.5
463
+ weightedSum += weight
464
+ totalWeight += 1
465
+ })
466
+
467
+ return totalWeight > 0 ? weightedSum / totalWeight : 0
468
+ }
469
+
470
+ /**
471
+ * Schedule detection with debounce
472
+ * Prevents excessive detect() calls from MutationObserver
473
+ * @private
474
+ */
475
+ _scheduleDetect() {
476
+ clearTimeout(this.detectTimer)
477
+
478
+ this.detectTimer = setTimeout(() => {
479
+ this.detect()
480
+ }, 200) // 200ms debounce
481
+ }
482
+
483
+ _debug(...args) {
484
+ if (this.config.debug) {
485
+ console.log('[TrackerDetector]', ...args)
486
+ }
487
+ }
488
+ }