@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
+ * ScriptManager — main controller for consent-based script activation
3
+ * @module ScriptManager
4
+ */
5
+
6
+ import { ScriptScanner } from './ScriptScanner.js'
7
+ import { ScriptRegistry } from './ScriptRegistry.js'
8
+ import { ScriptLoader } from './ScriptLoader.js'
9
+
10
+ export class ScriptManager {
11
+ constructor(consentManager, config = {}) {
12
+ this.consentManager = consentManager
13
+ this.config = {
14
+ autoScan: true,
15
+ observeDOM: true,
16
+ removeBlockedScripts: true,
17
+ debug: false,
18
+ ...config
19
+ }
20
+
21
+ this.scanner = new ScriptScanner({
22
+ observeDOM: this.config.observeDOM
23
+ })
24
+
25
+ this.registry = new ScriptRegistry()
26
+ this.loader = new ScriptLoader()
27
+
28
+ this.isInitialized = false
29
+ this._debug('ScriptManager initialized')
30
+ }
31
+
32
+ /**
33
+ * Initialize script management system
34
+ */
35
+ async initialize() {
36
+ if (this.isInitialized) {
37
+ this._debug('Already initialized')
38
+ return
39
+ }
40
+
41
+ // Wait for DOM ready
42
+ if (document.readyState === 'loading') {
43
+ await new Promise(resolve => {
44
+ document.addEventListener('DOMContentLoaded', resolve, { once: true })
45
+ })
46
+ }
47
+
48
+ // Initial scan
49
+ if (this.config.autoScan) {
50
+ await this.scanAndRegister()
51
+ }
52
+
53
+ // Apply current consent
54
+ const consent = this.consentManager.getConsent()
55
+ if (consent) {
56
+ await this.applyConsent(consent.categories)
57
+ }
58
+
59
+ // Listen for consent changes
60
+ this.consentManager.on('consentChanged', async ({ categories }) => {
61
+ await this.applyConsent(categories)
62
+ })
63
+
64
+ // Observe DOM for new scripts
65
+ if (this.config.observeDOM) {
66
+ this.scanner.observe((newScripts) => {
67
+ this._debug('New scripts detected:', newScripts.length)
68
+ this.registry.addMany(newScripts)
69
+
70
+ // Auto-activate if consent already given
71
+ const consent = this.consentManager.getConsent()
72
+ if (consent) {
73
+ this.activateScriptsByConsent(newScripts, consent.categories)
74
+ }
75
+ })
76
+ }
77
+
78
+ this.isInitialized = true
79
+ this._debug('ScriptManager ready')
80
+ this._emitEvent('scripts:ready', { stats: this.registry.getStats() })
81
+ }
82
+
83
+ /**
84
+ * Scan DOM and register all scripts
85
+ */
86
+ async scanAndRegister() {
87
+ this._debug('Scanning DOM for scripts...')
88
+ const scripts = this.scanner.scan()
89
+ this._debug(`Found ${scripts.length} scripts`)
90
+
91
+ this.registry.addMany(scripts)
92
+
93
+ this._emitEvent('scripts:scanned', {
94
+ total: scripts.length,
95
+ stats: this.registry.getStats()
96
+ })
97
+
98
+ return scripts
99
+ }
100
+
101
+ /**
102
+ * Apply consent to scripts
103
+ * @param {Object} categories - Consent categories
104
+ */
105
+ async applyConsent(categories) {
106
+ this._debug('Applying consent:', categories)
107
+
108
+ const results = {
109
+ activated: [],
110
+ blocked: [],
111
+ failed: []
112
+ }
113
+
114
+ // Get all blocked scripts
115
+ const blockedScripts = this.registry.getBlocked()
116
+
117
+ for (const script of blockedScripts) {
118
+ const hasConsent = categories[script.category] === true
119
+
120
+ if (hasConsent) {
121
+ // Activate script
122
+ const success = await this.activateScript(script)
123
+
124
+ if (success) {
125
+ results.activated.push(script)
126
+ } else {
127
+ results.failed.push(script)
128
+ }
129
+ } else {
130
+ results.blocked.push(script)
131
+ }
132
+ }
133
+
134
+ this._debug('Consent applied:', {
135
+ activated: results.activated.length,
136
+ blocked: results.blocked.length,
137
+ failed: results.failed.length
138
+ })
139
+
140
+ this._emitEvent('scripts:consent-applied', results)
141
+
142
+ return results
143
+ }
144
+
145
+ /**
146
+ * Activate single script
147
+ * @param {Object} script - Script descriptor
148
+ * @returns {Promise<boolean>}
149
+ */
150
+ async activateScript(script) {
151
+ if (script.isActivated) {
152
+ this._debug('Script already activated:', script.id)
153
+ return true
154
+ }
155
+
156
+ this._debug('Activating script:', script.id, script.category)
157
+
158
+ try {
159
+ // Load script
160
+ const success = await this.loader.load(script)
161
+
162
+ if (success) {
163
+ // Mark as activated
164
+ this.registry.markActivated(script.id)
165
+
166
+ // Remove original blocked element
167
+ if (this.config.removeBlockedScripts && script.element) {
168
+ this.loader.removeBlockedScript(script.element)
169
+ }
170
+
171
+ this._emitEvent('script:activated', {
172
+ id: script.id,
173
+ category: script.category,
174
+ src: script.src || 'inline'
175
+ })
176
+
177
+ return true
178
+ }
179
+
180
+ return false
181
+ } catch (error) {
182
+ return false
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Activate scripts by consent (helper for new scripts)
188
+ * @param {Array} scripts - Scripts to check
189
+ * @param {Object} categories - Consent categories
190
+ */
191
+ async activateScriptsByConsent(scripts, categories) {
192
+ const results = []
193
+
194
+ for (const script of scripts) {
195
+ if (script.isBlocked && !script.isActivated) {
196
+ const hasConsent = categories[script.category] === true
197
+
198
+ if (hasConsent) {
199
+ const success = await this.activateScript(script)
200
+ results.push({ script, success })
201
+ }
202
+ }
203
+ }
204
+
205
+ return results
206
+ }
207
+
208
+ /**
209
+ * Get scripts by category
210
+ * @param {string} category - Category name
211
+ * @returns {Array}
212
+ */
213
+ getScriptsByCategory(category) {
214
+ return this.registry.getByCategory(category)
215
+ }
216
+
217
+ /**
218
+ * Get all registered scripts
219
+ * @returns {Array}
220
+ */
221
+ getAllScripts() {
222
+ return this.registry.getAll()
223
+ }
224
+
225
+ /**
226
+ * Get registry stats
227
+ * @returns {Object}
228
+ */
229
+ getStats() {
230
+ return this.registry.getStats()
231
+ }
232
+
233
+ /**
234
+ * Get blocked scripts count
235
+ * @returns {number}
236
+ */
237
+ getBlockedCount() {
238
+ return this.registry.getBlocked().length
239
+ }
240
+
241
+ /**
242
+ * Get activated scripts count
243
+ * @returns {number}
244
+ */
245
+ getActivatedCount() {
246
+ return this.registry.getActivated().length
247
+ }
248
+
249
+ /**
250
+ * Manually trigger rescan
251
+ */
252
+ async rescan() {
253
+ return this.scanAndRegister()
254
+ }
255
+
256
+ /**
257
+ * Destroy script manager
258
+ */
259
+ destroy() {
260
+ this.scanner.disconnect()
261
+ this.registry.clear()
262
+ this.loader.clear()
263
+ this.isInitialized = false
264
+ this._debug('ScriptManager destroyed')
265
+ }
266
+
267
+ // --- Internal helpers ---
268
+
269
+ _emitEvent(eventName, detail) {
270
+ if (typeof window !== 'undefined') {
271
+ window.dispatchEvent(new CustomEvent(`cookie-consent:${eventName}`, { detail }))
272
+ }
273
+ }
274
+
275
+ _debug(...args) {
276
+ // Debug disabled
277
+ }
278
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * ScriptRegistry — stores and manages discovered scripts
3
+ * @module ScriptRegistry
4
+ */
5
+
6
+ export class ScriptRegistry {
7
+ constructor() {
8
+ this.scripts = new Map() // id -> script descriptor
9
+ this.categorizedScripts = new Map() // category -> [scripts]
10
+ }
11
+
12
+ /**
13
+ * Add script to registry
14
+ * @param {Object} script - Script descriptor
15
+ */
16
+ add(script) {
17
+ if (!script.id) return
18
+
19
+ // Store by id
20
+ this.scripts.set(script.id, script)
21
+
22
+ // Store by category
23
+ if (script.category) {
24
+ if (!this.categorizedScripts.has(script.category)) {
25
+ this.categorizedScripts.set(script.category, [])
26
+ }
27
+
28
+ const categoryScripts = this.categorizedScripts.get(script.category)
29
+ if (!categoryScripts.find(s => s.id === script.id)) {
30
+ categoryScripts.push(script)
31
+ }
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Add multiple scripts
37
+ * @param {Array} scripts - Array of script descriptors
38
+ */
39
+ addMany(scripts) {
40
+ scripts.forEach(script => this.add(script))
41
+ }
42
+
43
+ /**
44
+ * Get script by id
45
+ * @param {string} id - Script id
46
+ * @returns {Object|null}
47
+ */
48
+ get(id) {
49
+ return this.scripts.get(id) || null
50
+ }
51
+
52
+ /**
53
+ * Get all scripts
54
+ * @returns {Array}
55
+ */
56
+ getAll() {
57
+ return Array.from(this.scripts.values())
58
+ }
59
+
60
+ /**
61
+ * Get scripts by category
62
+ * @param {string} category - Category name
63
+ * @returns {Array}
64
+ */
65
+ getByCategory(category) {
66
+ return this.categorizedScripts.get(category) || []
67
+ }
68
+
69
+ /**
70
+ * Get all categories
71
+ * @returns {Array}
72
+ */
73
+ getCategories() {
74
+ return Array.from(this.categorizedScripts.keys())
75
+ }
76
+
77
+ /**
78
+ * Get blocked scripts (not yet activated)
79
+ * @returns {Array}
80
+ */
81
+ getBlocked() {
82
+ return this.getAll().filter(script => script.isBlocked && !script.isActivated)
83
+ }
84
+
85
+ /**
86
+ * Get activated scripts
87
+ * @returns {Array}
88
+ */
89
+ getActivated() {
90
+ return this.getAll().filter(script => script.isActivated)
91
+ }
92
+
93
+ /**
94
+ * Mark script as activated
95
+ * @param {string} id - Script id
96
+ */
97
+ markActivated(id) {
98
+ const script = this.scripts.get(id)
99
+ if (script) {
100
+ script.isActivated = true
101
+ script.activatedAt = new Date().toISOString()
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Remove script from registry
107
+ * @param {string} id - Script id
108
+ */
109
+ remove(id) {
110
+ const script = this.scripts.get(id)
111
+ if (!script) return
112
+
113
+ // Remove from main map
114
+ this.scripts.delete(id)
115
+
116
+ // Remove from category map
117
+ if (script.category) {
118
+ const categoryScripts = this.categorizedScripts.get(script.category)
119
+ if (categoryScripts) {
120
+ const index = categoryScripts.findIndex(s => s.id === id)
121
+ if (index !== -1) {
122
+ categoryScripts.splice(index, 1)
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Clear all scripts
130
+ */
131
+ clear() {
132
+ this.scripts.clear()
133
+ this.categorizedScripts.clear()
134
+ }
135
+
136
+ /**
137
+ * Get registry stats
138
+ * @returns {Object}
139
+ */
140
+ getStats() {
141
+ const all = this.getAll()
142
+ return {
143
+ total: all.length,
144
+ blocked: all.filter(s => s.isBlocked && !s.isActivated).length,
145
+ activated: all.filter(s => s.isActivated).length,
146
+ byCategory: Object.fromEntries(
147
+ Array.from(this.categorizedScripts.entries()).map(([cat, scripts]) => [
148
+ cat,
149
+ {
150
+ total: scripts.length,
151
+ blocked: scripts.filter(s => s.isBlocked && !s.isActivated).length,
152
+ activated: scripts.filter(s => s.isActivated).length
153
+ }
154
+ ])
155
+ )
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Check if script exists
161
+ * @param {string} id - Script id
162
+ * @returns {boolean}
163
+ */
164
+ has(id) {
165
+ return this.scripts.has(id)
166
+ }
167
+
168
+ /**
169
+ * Get count of scripts
170
+ * @returns {number}
171
+ */
172
+ count() {
173
+ return this.scripts.size
174
+ }
175
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * ScriptScanner — scans DOM for blocked scripts with data-cookie-category
3
+ * @module ScriptScanner
4
+ */
5
+
6
+ export class ScriptScanner {
7
+ constructor(config = {}) {
8
+ this.config = {
9
+ attributeName: 'data-cookie-category',
10
+ srcAttributeName: 'data-cookie-src',
11
+ blockedTypes: ['text/plain', 'text/blocked'],
12
+ scanOnInit: true,
13
+ observeDOM: true,
14
+ scanIframes: true,
15
+ ...config
16
+ }
17
+ this.observer = null
18
+ }
19
+
20
+ /**
21
+ * Scan DOM for blocked scripts
22
+ * @returns {Array} Array of script descriptors
23
+ */
24
+ scan() {
25
+ const items = []
26
+
27
+ // Find all script tags with data-cookie-category
28
+ const scriptElements = document.querySelectorAll(`script[${this.config.attributeName}]`)
29
+
30
+ scriptElements.forEach((element, index) => {
31
+ const category = element.getAttribute(this.config.attributeName)
32
+ const type = element.getAttribute('type')
33
+ const isBlocked = this.config.blockedTypes.includes(type)
34
+
35
+ // Pre-blocking: use data-cookie-src instead of src
36
+ const cookieSrc = element.getAttribute(this.config.srcAttributeName)
37
+ const regularSrc = element.getAttribute('src')
38
+ const actualSrc = cookieSrc || regularSrc
39
+
40
+ const descriptor = {
41
+ id: `script-${index}-${Date.now()}`,
42
+ elementType: 'script',
43
+ element,
44
+ category,
45
+ type: type || 'text/javascript',
46
+ src: actualSrc || null,
47
+ cookieSrc: cookieSrc || null,
48
+ inline: !actualSrc,
49
+ content: !actualSrc ? element.textContent : null,
50
+ isBlocked,
51
+ isActivated: false,
52
+ attributes: this._getAttributes(element)
53
+ }
54
+
55
+ items.push(descriptor)
56
+ })
57
+
58
+ // Scan iframes if enabled
59
+ if (this.config.scanIframes) {
60
+ const iframeElements = document.querySelectorAll(`iframe[${this.config.attributeName}]`)
61
+
62
+ iframeElements.forEach((element, index) => {
63
+ const category = element.getAttribute(this.config.attributeName)
64
+ const cookieSrc = element.getAttribute(this.config.srcAttributeName)
65
+ const regularSrc = element.getAttribute('src')
66
+ const actualSrc = cookieSrc || regularSrc
67
+
68
+ const descriptor = {
69
+ id: `iframe-${index}-${Date.now()}`,
70
+ elementType: 'iframe',
71
+ element,
72
+ category,
73
+ src: actualSrc || null,
74
+ cookieSrc: cookieSrc || null,
75
+ isBlocked: true, // iframes always blocked until consent
76
+ isActivated: false,
77
+ attributes: this._getAttributes(element)
78
+ }
79
+
80
+ items.push(descriptor)
81
+ })
82
+ }
83
+
84
+ return items
85
+ }
86
+
87
+ /**
88
+ * Start observing DOM for new scripts
89
+ * @param {Function} callback - Called when new scripts detected
90
+ */
91
+ observe(callback) {
92
+ if (!this.config.observeDOM || typeof MutationObserver === 'undefined') {
93
+ return
94
+ }
95
+
96
+ this.observer = new MutationObserver((mutations) => {
97
+ let hasNewScripts = false
98
+
99
+ mutations.forEach((mutation) => {
100
+ mutation.addedNodes.forEach((node) => {
101
+ if (node.nodeType === 1) { // Element node
102
+ // Check if added node is a script
103
+ if (node.tagName === 'SCRIPT' && node.hasAttribute(this.config.attributeName)) {
104
+ hasNewScripts = true
105
+ }
106
+
107
+ // Check if added node contains scripts
108
+ const scripts = node.querySelectorAll?.(`script[${this.config.attributeName}]`)
109
+ if (scripts?.length > 0) {
110
+ hasNewScripts = true
111
+ }
112
+ }
113
+ })
114
+ })
115
+
116
+ if (hasNewScripts) {
117
+ const newScripts = this.scan()
118
+ callback(newScripts)
119
+ }
120
+ })
121
+
122
+ this.observer.observe(document.documentElement, {
123
+ childList: true,
124
+ subtree: true
125
+ })
126
+ }
127
+
128
+ /**
129
+ * Stop observing DOM
130
+ */
131
+ disconnect() {
132
+ if (this.observer) {
133
+ this.observer.disconnect()
134
+ this.observer = null
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Get all attributes from element (except data-cookie-category)
140
+ * @private
141
+ */
142
+ _getAttributes(element) {
143
+ const attributes = {}
144
+ const excludeAttrs = [
145
+ this.config.attributeName,
146
+ this.config.srcAttributeName,
147
+ 'type',
148
+ 'src' // Exclude src to prevent early execution
149
+ ]
150
+
151
+ Array.from(element.attributes).forEach((attr) => {
152
+ if (!excludeAttrs.includes(attr.name)) {
153
+ attributes[attr.name] = attr.value
154
+ }
155
+ })
156
+
157
+ return attributes
158
+ }
159
+
160
+ /**
161
+ * Find scripts by category
162
+ * @param {string} category - Category name
163
+ * @returns {Array} Filtered scripts
164
+ */
165
+ findByCategory(category) {
166
+ return this.scan().filter(script => script.category === category)
167
+ }
168
+
169
+ /**
170
+ * Check if script should be blocked
171
+ * @param {HTMLScriptElement} element - Script element
172
+ * @returns {boolean}
173
+ */
174
+ isBlocked(element) {
175
+ const type = element.getAttribute('type')
176
+ return this.config.blockedTypes.includes(type)
177
+ }
178
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Script Management System — Auto-blocking scripts (Cookiebot-style)
3
+ * @module scripts
4
+ */
5
+
6
+ export { ScriptScanner } from './ScriptScanner.js'
7
+ export { ScriptRegistry } from './ScriptRegistry.js'
8
+ export { ScriptLoader } from './ScriptLoader.js'
9
+ export { ScriptManager } from './ScriptManager.js'