@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,278 @@
1
+ /**
2
+ * ScriptRewriter — automatically rewrites scripts to be CMP-compliant
3
+ * @module ScriptRewriter
4
+ *
5
+ * Converts regular scripts:
6
+ * <script src="https://www.google-analytics.com/gtag.js"></script>
7
+ *
8
+ * Into CMP-compliant scripts:
9
+ * <script
10
+ * type="text/plain"
11
+ * data-cookie-category="analytics"
12
+ * data-cookie-src="https://www.google-analytics.com/gtag.js"
13
+ * ></script>
14
+ */
15
+
16
+ import { getAllTrackerPatterns } from '../trackers/TrackerPatterns.js'
17
+
18
+ export class ScriptRewriter {
19
+ constructor(config = {}) {
20
+ this.config = {
21
+ autoRewrite: true,
22
+ rewriteInline: false, // Rewrite inline scripts (dangerous!)
23
+ preserveAttributes: true, // Preserve original attributes
24
+ observeDOM: true, // Watch for new scripts
25
+ whitelist: [], // URLs to never rewrite
26
+ blacklist: [], // URLs to always rewrite
27
+ debug: false,
28
+ ...config
29
+ }
30
+
31
+ this.rewrittenScripts = new Map() // original script -> rewritten script
32
+ this.observer = null
33
+ }
34
+
35
+ /**
36
+ * Initialize rewriter
37
+ */
38
+ initialize() {
39
+ if (this.config.autoRewrite) {
40
+ this.rewriteAll()
41
+ }
42
+
43
+ if (this.config.observeDOM) {
44
+ this._startObserver()
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Rewrite all scripts on the page
50
+ * @returns {number} Number of scripts rewritten
51
+ */
52
+ rewriteAll() {
53
+ this._debug('Starting script rewriting...')
54
+
55
+ const scripts = document.querySelectorAll('script[src]')
56
+ let rewrittenCount = 0
57
+
58
+ scripts.forEach(script => {
59
+ if (this._shouldRewrite(script)) {
60
+ const rewritten = this._rewriteScript(script)
61
+ if (rewritten) {
62
+ rewrittenCount++
63
+ }
64
+ }
65
+ })
66
+
67
+ this._debug(`Rewrote ${rewrittenCount} scripts`)
68
+ return rewrittenCount
69
+ }
70
+
71
+ /**
72
+ * Rewrite a single script element
73
+ * @param {HTMLScriptElement} script - Script to rewrite
74
+ * @returns {HTMLScriptElement|null} Rewritten script or null
75
+ */
76
+ _rewriteScript(script) {
77
+ const src = script.getAttribute('src')
78
+ if (!src) return null
79
+
80
+ // Check if already rewritten
81
+ if (script.type === 'text/plain' && script.dataset.cookieSrc) {
82
+ this._debug('Script already rewritten:', src)
83
+ return null
84
+ }
85
+
86
+ // Detect tracker category
87
+ const category = this._detectCategory(src)
88
+ if (!category) {
89
+ this._debug('No category detected for:', src)
90
+ return null
91
+ }
92
+
93
+ // Create new script element
94
+ const newScript = document.createElement('script')
95
+
96
+ // Set CMP attributes
97
+ newScript.type = 'text/plain'
98
+ newScript.dataset.cookieCategory = category
99
+ newScript.dataset.cookieSrc = src
100
+
101
+ // Preserve original attributes
102
+ if (this.config.preserveAttributes) {
103
+ Array.from(script.attributes).forEach(attr => {
104
+ if (attr.name !== 'src' && attr.name !== 'type') {
105
+ newScript.setAttribute(attr.name, attr.value)
106
+ }
107
+ })
108
+ }
109
+
110
+ // Replace script
111
+ script.parentNode?.replaceChild(newScript, script)
112
+
113
+ // Store mapping
114
+ this.rewrittenScripts.set(script, newScript)
115
+
116
+ this._debug(`Rewrote script: ${src} → category: ${category}`)
117
+ return newScript
118
+ }
119
+
120
+ /**
121
+ * Check if script should be rewritten
122
+ * @param {HTMLScriptElement} script
123
+ * @returns {boolean}
124
+ */
125
+ _shouldRewrite(script) {
126
+ const src = script.getAttribute('src')
127
+ if (!src) return false
128
+
129
+ // Already rewritten
130
+ if (script.type === 'text/plain' && script.dataset.cookieSrc) {
131
+ return false
132
+ }
133
+
134
+ // Check whitelist (never rewrite)
135
+ if (this.config.whitelist.length > 0) {
136
+ const isWhitelisted = this.config.whitelist.some(pattern =>
137
+ src.includes(pattern)
138
+ )
139
+ if (isWhitelisted) {
140
+ this._debug('Whitelisted, skipping:', src)
141
+ return false
142
+ }
143
+ }
144
+
145
+ // Check blacklist (always rewrite)
146
+ if (this.config.blacklist.length > 0) {
147
+ const isBlacklisted = this.config.blacklist.some(pattern =>
148
+ src.includes(pattern)
149
+ )
150
+ if (isBlacklisted) {
151
+ return true
152
+ }
153
+ }
154
+
155
+ // Check if tracker is detected
156
+ const category = this._detectCategory(src)
157
+ return category !== null
158
+ }
159
+
160
+ /**
161
+ * Detect category from script src
162
+ * @param {string} src - Script source URL
163
+ * @returns {string|null} Category name or null
164
+ */
165
+ _detectCategory(src) {
166
+ const patterns = getAllTrackerPatterns()
167
+
168
+ for (const pattern of patterns) {
169
+ for (const scriptPattern of pattern.scripts) {
170
+ if (src.includes(scriptPattern)) {
171
+ return pattern.category
172
+ }
173
+ }
174
+ }
175
+
176
+ return null
177
+ }
178
+
179
+ /**
180
+ * Start MutationObserver to watch for new scripts
181
+ * @private
182
+ */
183
+ _startObserver() {
184
+ if (typeof MutationObserver === 'undefined') {
185
+ return
186
+ }
187
+
188
+ this.observer = new MutationObserver((mutations) => {
189
+ mutations.forEach((mutation) => {
190
+ mutation.addedNodes.forEach((node) => {
191
+ if (node.nodeType === 1) {
192
+ // Check if it's a script
193
+ if (node.tagName === 'SCRIPT' && node.getAttribute('src')) {
194
+ if (this._shouldRewrite(node)) {
195
+ this._debug('New script detected, rewriting...')
196
+ this._rewriteScript(node)
197
+ }
198
+ }
199
+
200
+ // Check for scripts inside added node
201
+ const scripts = node.querySelectorAll?.('script[src]')
202
+ scripts?.forEach(script => {
203
+ if (this._shouldRewrite(script)) {
204
+ this._rewriteScript(script)
205
+ }
206
+ })
207
+ }
208
+ })
209
+ })
210
+ })
211
+
212
+ this.observer.observe(document.documentElement, {
213
+ childList: true,
214
+ subtree: true
215
+ })
216
+
217
+ this._debug('Script rewriter observer started')
218
+ }
219
+
220
+ /**
221
+ * Get rewritten scripts
222
+ * @returns {Array}
223
+ */
224
+ getRewrittenScripts() {
225
+ return Array.from(this.rewrittenScripts.entries()).map(([original, rewritten]) => ({
226
+ original: original.src,
227
+ category: rewritten.dataset.cookieCategory,
228
+ rewritten: rewritten.dataset.cookieSrc
229
+ }))
230
+ }
231
+
232
+ /**
233
+ * Get stats
234
+ * @returns {Object}
235
+ */
236
+ getStats() {
237
+ const scripts = this.getRewrittenScripts()
238
+ const byCategory = {}
239
+
240
+ scripts.forEach(script => {
241
+ if (!byCategory[script.category]) {
242
+ byCategory[script.category] = []
243
+ }
244
+ byCategory[script.category].push(script.original)
245
+ })
246
+
247
+ return {
248
+ total: scripts.length,
249
+ byCategory
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Destroy rewriter
255
+ */
256
+ destroy() {
257
+ if (this.observer) {
258
+ this.observer.disconnect()
259
+ this.observer = null
260
+ }
261
+ this.rewrittenScripts.clear()
262
+ }
263
+
264
+ _debug(...args) {
265
+ // Debug disabled
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Helper function to rewrite scripts automatically
271
+ * @param {Object} config - Configuration
272
+ * @returns {ScriptRewriter}
273
+ */
274
+ export function initScriptRewriter(config = {}) {
275
+ const rewriter = new ScriptRewriter(config)
276
+ rewriter.initialize()
277
+ return rewriter
278
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Script Rewriting System — automatically rewrite scripts to be CMP-compliant
3
+ * @module rewriting
4
+ */
5
+
6
+ export { ScriptRewriter, initScriptRewriter } from './ScriptRewriter.js'
@@ -0,0 +1,310 @@
1
+ /**
2
+ * ScriptLoader — safely injects scripts into DOM
3
+ * @module ScriptLoader
4
+ */
5
+
6
+ export class ScriptLoader {
7
+ constructor(config = {}) {
8
+ this.config = {
9
+ timeout: 10000, // 10 seconds
10
+ retryAttempts: 3,
11
+ retryDelay: 1000,
12
+ ...config
13
+ }
14
+ this.loadedScripts = new Set() // Track by src
15
+ this.loadedHashes = new Set() // Track by content hash
16
+ this.loadedIds = new Set() // Track by id
17
+ }
18
+
19
+ /**
20
+ * Load external script
21
+ * @param {Object} scriptDescriptor - Script descriptor from registry
22
+ * @returns {Promise<boolean>}
23
+ */
24
+ async loadExternal(scriptDescriptor) {
25
+ const { src, cookieSrc, attributes, id } = scriptDescriptor
26
+ const actualSrc = cookieSrc || src
27
+
28
+ if (!actualSrc) {
29
+ console.warn('[ScriptLoader] No src provided for external script')
30
+ return false
31
+ }
32
+
33
+ // Deduplication: check if already loaded
34
+ if (this.loadedScripts.has(actualSrc) || this.loadedIds.has(id)) {
35
+ console.log('[ScriptLoader] Script already loaded:', actualSrc)
36
+ return true
37
+ }
38
+
39
+ return new Promise((resolve) => {
40
+ const script = document.createElement('script')
41
+
42
+ // Set src from data-cookie-src (pre-blocking)
43
+ script.src = actualSrc
44
+
45
+ // Apply attributes
46
+ Object.entries(attributes || {}).forEach(([key, value]) => {
47
+ if (key !== 'src') {
48
+ script.setAttribute(key, value)
49
+ }
50
+ })
51
+
52
+ // Set proper type
53
+ script.type = 'text/javascript'
54
+
55
+ // Add data attribute for tracking
56
+ script.setAttribute('data-cookie-loaded', 'true')
57
+ script.setAttribute('data-cookie-id', id)
58
+
59
+ let timeoutId = null
60
+
61
+ const cleanup = () => {
62
+ if (timeoutId) clearTimeout(timeoutId)
63
+ script.removeEventListener('load', onLoad)
64
+ script.removeEventListener('error', onError)
65
+ }
66
+
67
+ const onLoad = () => {
68
+ cleanup()
69
+ this.loadedScripts.add(actualSrc)
70
+ this.loadedIds.add(id)
71
+ resolve(true)
72
+ }
73
+
74
+ const onError = (error) => {
75
+ cleanup()
76
+ console.error('[ScriptLoader] Failed to load script:', actualSrc, error)
77
+ resolve(false)
78
+ }
79
+
80
+ // Timeout
81
+ timeoutId = setTimeout(() => {
82
+ cleanup()
83
+ console.warn('[ScriptLoader] Script load timeout:', actualSrc)
84
+ resolve(false)
85
+ }, this.config.timeout)
86
+
87
+ script.addEventListener('load', onLoad)
88
+ script.addEventListener('error', onError)
89
+
90
+ // Inject into DOM
91
+ const target = document.head || document.documentElement
92
+ target.appendChild(script)
93
+ })
94
+ }
95
+
96
+ /**
97
+ * Load inline script
98
+ * @param {Object} scriptDescriptor - Script descriptor from registry
99
+ * @returns {Promise<boolean>}
100
+ */
101
+ async loadInline(scriptDescriptor) {
102
+ const { content, attributes, id } = scriptDescriptor
103
+
104
+ if (!content) {
105
+ console.warn('[ScriptLoader] No content provided for inline script')
106
+ return false
107
+ }
108
+
109
+ // Deduplication: check by content hash
110
+ const contentHash = this._hashContent(content)
111
+ if (this.loadedHashes.has(contentHash) || this.loadedIds.has(id)) {
112
+ console.log('[ScriptLoader] Inline script already loaded')
113
+ return true
114
+ }
115
+
116
+ try {
117
+ const script = document.createElement('script')
118
+
119
+ // Set content
120
+ script.textContent = content
121
+
122
+ // Apply attributes
123
+ Object.entries(attributes || {}).forEach(([key, value]) => {
124
+ script.setAttribute(key, value)
125
+ })
126
+
127
+ // Set proper type
128
+ script.type = 'text/javascript'
129
+
130
+ // Add data attribute for tracking
131
+ script.setAttribute('data-cookie-loaded', 'true')
132
+ script.setAttribute('data-cookie-id', id)
133
+
134
+ // Inject into DOM
135
+ const target = document.head || document.documentElement
136
+ target.appendChild(script)
137
+
138
+ // Mark as loaded
139
+ this.loadedHashes.add(contentHash)
140
+ this.loadedIds.add(id)
141
+
142
+ return true
143
+ } catch (error) {
144
+ console.error('[ScriptLoader] Failed to load inline script:', error)
145
+ return false
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Load script (auto-detect inline vs external)
151
+ * @param {Object} scriptDescriptor - Script descriptor
152
+ * @returns {Promise<boolean>}
153
+ */
154
+ async load(scriptDescriptor) {
155
+ const { elementType } = scriptDescriptor
156
+
157
+ // Handle iframes
158
+ if (elementType === 'iframe') {
159
+ return this.loadIframe(scriptDescriptor)
160
+ }
161
+
162
+ // Handle scripts
163
+ if (scriptDescriptor.inline) {
164
+ return this.loadInline(scriptDescriptor)
165
+ } else {
166
+ return this.loadExternal(scriptDescriptor)
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Load iframe (YouTube, Maps, etc)
172
+ * @param {Object} iframeDescriptor - Iframe descriptor
173
+ * @returns {Promise<boolean>}
174
+ */
175
+ async loadIframe(iframeDescriptor) {
176
+ const { src, cookieSrc, attributes, id, element } = iframeDescriptor
177
+ const actualSrc = cookieSrc || src
178
+
179
+ if (!actualSrc) {
180
+ console.warn('[ScriptLoader] No src provided for iframe')
181
+ return false
182
+ }
183
+
184
+ // Deduplication
185
+ if (this.loadedScripts.has(actualSrc) || this.loadedIds.has(id)) {
186
+ console.log('[ScriptLoader] Iframe already loaded:', actualSrc)
187
+ return true
188
+ }
189
+
190
+ try {
191
+ // Update existing iframe or create new one
192
+ if (element && element.parentNode) {
193
+ // Update src on existing element
194
+ element.src = actualSrc
195
+
196
+ // Remove data-cookie-src attribute
197
+ element.removeAttribute('data-cookie-src')
198
+
199
+ this.loadedScripts.add(actualSrc)
200
+ this.loadedIds.add(id)
201
+ return true
202
+ } else {
203
+ // Create new iframe
204
+ const iframe = document.createElement('iframe')
205
+ iframe.src = actualSrc
206
+
207
+ // Apply attributes
208
+ Object.entries(attributes || {}).forEach(([key, value]) => {
209
+ iframe.setAttribute(key, value)
210
+ })
211
+
212
+ iframe.setAttribute('data-cookie-loaded', 'true')
213
+ iframe.setAttribute('data-cookie-id', id)
214
+
215
+ // Find parent or append to body
216
+ const target = element?.parentNode || document.body
217
+ target.appendChild(iframe)
218
+
219
+ this.loadedScripts.add(actualSrc)
220
+ this.loadedIds.add(id)
221
+ return true
222
+ }
223
+ } catch (error) {
224
+ console.error('[ScriptLoader] Failed to load iframe:', error)
225
+ return false
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Load multiple scripts sequentially
231
+ * @param {Array} scriptDescriptors - Array of script descriptors
232
+ * @returns {Promise<Object>} Results object
233
+ */
234
+ async loadMany(scriptDescriptors) {
235
+ const results = {
236
+ total: scriptDescriptors.length,
237
+ loaded: 0,
238
+ failed: 0,
239
+ scripts: []
240
+ }
241
+
242
+ for (const script of scriptDescriptors) {
243
+ const success = await this.load(script)
244
+
245
+ results.scripts.push({
246
+ id: script.id,
247
+ category: script.category,
248
+ success
249
+ })
250
+
251
+ if (success) {
252
+ results.loaded++
253
+ } else {
254
+ results.failed++
255
+ }
256
+ }
257
+
258
+ return results
259
+ }
260
+
261
+ /**
262
+ * Remove original blocked script element
263
+ * @param {HTMLScriptElement} element - Original script element
264
+ */
265
+ removeBlockedScript(element) {
266
+ if (element && element.parentNode) {
267
+ element.parentNode.removeChild(element)
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Check if script is already loaded
273
+ * @param {string} src - Script src
274
+ * @returns {boolean}
275
+ */
276
+ isLoaded(src) {
277
+ return this.loadedScripts.has(src)
278
+ }
279
+
280
+ /**
281
+ * Clear loaded scripts cache
282
+ */
283
+ clear() {
284
+ this.loadedScripts.clear()
285
+ this.loadedHashes.clear()
286
+ this.loadedIds.clear()
287
+ }
288
+
289
+ /**
290
+ * Get loaded scripts count
291
+ * @returns {number}
292
+ */
293
+ getLoadedCount() {
294
+ return this.loadedScripts.size
295
+ }
296
+
297
+ /**
298
+ * Simple hash function for content deduplication
299
+ * @private
300
+ */
301
+ _hashContent(content) {
302
+ let hash = 0
303
+ for (let i = 0; i < content.length; i++) {
304
+ const char = content.charCodeAt(i)
305
+ hash = ((hash << 5) - hash) + char
306
+ hash = hash & hash
307
+ }
308
+ return hash.toString(36)
309
+ }
310
+ }