@el7ven/cookie-kit 0.2.21 → 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.
Files changed (39) hide show
  1. package/package.json +21 -1
  2. package/src/core/AnalyticsManager.js +400 -0
  3. package/src/core/ConsentManager.js +710 -0
  4. package/src/core/ConsentMode.js +109 -0
  5. package/src/core/FocusTrap.js +130 -0
  6. package/src/core/GeoDetector.js +144 -0
  7. package/src/core/ScriptLoader.js +229 -0
  8. package/src/core/StorageAdapter.js +179 -0
  9. package/src/core/analytics.js +101 -10
  10. package/src/core/index.js +7 -7
  11. package/src/geo/GeoDetector.js +536 -0
  12. package/src/geo/RegionRules.js +126 -0
  13. package/src/geo/index.js +16 -0
  14. package/src/index.js +56 -18
  15. package/src/js/CookieConsent.js +0 -1
  16. package/src/locales/en.js +54 -0
  17. package/src/locales/index.js +20 -0
  18. package/src/locales/ro.js +54 -0
  19. package/src/plugins/CMPPlugin.js +187 -0
  20. package/src/plugins/PluginManager.js +234 -0
  21. package/src/plugins/index.js +7 -0
  22. package/src/providers/GoogleConsentModeProvider.js +278 -0
  23. package/src/providers/index.js +6 -0
  24. package/src/rewriting/ScriptRewriter.js +278 -0
  25. package/src/rewriting/index.js +6 -0
  26. package/src/scripts/ScriptLoader.js +310 -0
  27. package/src/scripts/ScriptManager.js +278 -0
  28. package/src/scripts/ScriptRegistry.js +175 -0
  29. package/src/scripts/ScriptScanner.js +178 -0
  30. package/src/scripts/index.js +9 -0
  31. package/src/trackers/TrackerDetector.js +488 -0
  32. package/src/trackers/TrackerPatterns.js +307 -0
  33. package/src/trackers/TrackerRegistry.js +172 -0
  34. package/src/trackers/index.js +15 -0
  35. package/src/utils/cookies.js +37 -0
  36. package/src/utils/dom.js +58 -0
  37. package/src/utils/helpers.js +89 -0
  38. package/src/vue/CookieConsent.vue +1 -1
  39. package/src/vue/CookieDrawer.vue +4 -4
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Safe Storage Adapter — handles localStorage failures (Safari private mode, quota exceeded)
3
+ * @module StorageAdapter
4
+ */
5
+
6
+ export class StorageAdapter {
7
+ constructor(config = {}) {
8
+ this.storageType = config.storageType || 'localStorage'
9
+ this.storageKey = config.storageKey || 'cookie_consent'
10
+ this.cookieExpireDays = config.consentExpireDays || 365
11
+ this._testStorage()
12
+ }
13
+
14
+ /**
15
+ * Test if storage is available (Safari private mode blocks localStorage)
16
+ */
17
+ _testStorage() {
18
+ this.isLocalStorageAvailable = false
19
+ this.isCookieAvailable = false
20
+
21
+ // Test localStorage
22
+ if (typeof window !== 'undefined' && window.localStorage) {
23
+ try {
24
+ const testKey = '__storage_test__'
25
+ localStorage.setItem(testKey, 'test')
26
+ localStorage.removeItem(testKey)
27
+ this.isLocalStorageAvailable = true
28
+ } catch (e) {
29
+ console.warn('[StorageAdapter] localStorage not available:', e.message)
30
+ }
31
+ }
32
+
33
+ // Test cookies
34
+ if (typeof document !== 'undefined') {
35
+ try {
36
+ document.cookie = '__cookie_test__=test; path=/; SameSite=Lax'
37
+ this.isCookieAvailable = document.cookie.includes('__cookie_test__')
38
+ document.cookie = '__cookie_test__=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'
39
+ } catch (e) {
40
+ console.warn('[StorageAdapter] Cookies not available:', e.message)
41
+ }
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Get data from storage
47
+ */
48
+ get(key = null) {
49
+ const storageKey = key || this.storageKey
50
+
51
+ try {
52
+ // Try localStorage first
53
+ if (this.storageType === 'localStorage' || this.storageType === 'both') {
54
+ if (this.isLocalStorageAvailable) {
55
+ const value = localStorage.getItem(storageKey)
56
+ if (value) return value
57
+ }
58
+ }
59
+
60
+ // Fallback to cookies
61
+ if (this.storageType === 'cookie' || this.storageType === 'both') {
62
+ if (this.isCookieAvailable) {
63
+ return this._readCookie(storageKey)
64
+ }
65
+ }
66
+
67
+ return null
68
+ } catch (e) {
69
+ console.error('[StorageAdapter] Error reading storage:', e)
70
+ return null
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Set data to storage
76
+ */
77
+ set(key, value) {
78
+ const storageKey = typeof key === 'string' ? key : this.storageKey
79
+ const data = typeof key === 'string' ? value : key
80
+
81
+ // Validate data before saving
82
+ if (data === null || data === undefined) {
83
+ console.warn('[StorageAdapter] Attempted to save null/undefined data, skipping')
84
+ return false
85
+ }
86
+
87
+ if (typeof data === 'string' && (data === 'null' || data === 'undefined')) {
88
+ console.warn('[StorageAdapter] Attempted to save literal "null"/"undefined" string, skipping')
89
+ return false
90
+ }
91
+
92
+ try {
93
+ // Try localStorage
94
+ if (this.storageType === 'localStorage' || this.storageType === 'both') {
95
+ if (this.isLocalStorageAvailable) {
96
+ try {
97
+ localStorage.setItem(storageKey, data)
98
+ } catch (e) {
99
+ // Quota exceeded — try to clear old data
100
+ if (e.name === 'QuotaExceededError') {
101
+ console.warn('[StorageAdapter] localStorage quota exceeded, falling back to cookies')
102
+ this.isLocalStorageAvailable = false
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ // Fallback to cookies
109
+ if (this.storageType === 'cookie' || this.storageType === 'both') {
110
+ if (this.isCookieAvailable) {
111
+ this._writeCookie(storageKey, data, this.cookieExpireDays)
112
+ }
113
+ }
114
+
115
+ return true
116
+ } catch (e) {
117
+ console.error('[StorageAdapter] Error writing storage:', e)
118
+ return false
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Remove data from storage
124
+ */
125
+ remove(key = null) {
126
+ const storageKey = key || this.storageKey
127
+
128
+ try {
129
+ // Remove from localStorage
130
+ if (this.storageType === 'localStorage' || this.storageType === 'both') {
131
+ if (this.isLocalStorageAvailable) {
132
+ localStorage.removeItem(storageKey)
133
+ }
134
+ }
135
+
136
+ // Remove from cookies
137
+ if (this.storageType === 'cookie' || this.storageType === 'both') {
138
+ if (this.isCookieAvailable) {
139
+ this._writeCookie(storageKey, '', -1)
140
+ }
141
+ }
142
+
143
+ return true
144
+ } catch (e) {
145
+ console.error('[StorageAdapter] Error removing storage:', e)
146
+ return false
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Check if storage is available
152
+ */
153
+ isAvailable() {
154
+ return this.isLocalStorageAvailable || this.isCookieAvailable
155
+ }
156
+
157
+ /**
158
+ * Get storage type being used
159
+ */
160
+ getActiveStorageType() {
161
+ if (this.isLocalStorageAvailable) return 'localStorage'
162
+ if (this.isCookieAvailable) return 'cookie'
163
+ return 'none'
164
+ }
165
+
166
+ // --- Cookie helpers ---
167
+
168
+ _readCookie(name) {
169
+ if (typeof document === 'undefined') return null
170
+ const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
171
+ return match ? decodeURIComponent(match[2]) : null
172
+ }
173
+
174
+ _writeCookie(name, value, days) {
175
+ if (typeof document === 'undefined') return
176
+ const expires = new Date(Date.now() + days * 864e5).toUTCString()
177
+ document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax`
178
+ }
179
+ }
@@ -127,27 +127,115 @@ export function loadYandexMetrica(counterId) {
127
127
  }
128
128
 
129
129
  /**
130
- * Block/unblock scripts based on consent categories
131
- * Scripts with data-cookie-category attribute will be managed
132
- * @param {Object} categories - { analytics: true, marketing: false }
130
+ * Block all scripts with data-cookie-category BEFORE consent.
131
+ * Call this as early as possible (before DOMContentLoaded).
132
+ * Rewrites script type to text/plain so they don't execute.
133
+ *
134
+ * Usage in HTML:
135
+ * <script data-cookie-category="analytics" src="..."></script>
136
+ * <script data-cookie-category="marketing">inline code</script>
133
137
  */
134
- export function manageScriptBlocking(categories = {}) {
138
+ export function blockScriptsBeforeConsent() {
135
139
  if (typeof document === 'undefined') return
136
140
 
141
+ // MutationObserver to catch dynamically added scripts
142
+ const observer = new MutationObserver((mutations) => {
143
+ for (const mutation of mutations) {
144
+ for (const node of mutation.addedNodes) {
145
+ if (node.tagName === 'SCRIPT' && node.getAttribute('data-cookie-category')) {
146
+ const category = node.getAttribute('data-cookie-category')
147
+ // Check if consent already given for this category
148
+ if (!_hasConsentFor(category)) {
149
+ node.type = 'text/plain'
150
+ if (node.src) {
151
+ node.dataset.originalSrc = node.src
152
+ node.removeAttribute('src')
153
+ }
154
+ }
155
+ }
156
+ }
157
+ }
158
+ })
159
+
160
+ observer.observe(document.documentElement, { childList: true, subtree: true })
161
+
162
+ // Also block any scripts already in the DOM
137
163
  document.querySelectorAll('script[data-cookie-category]').forEach(script => {
164
+ const category = script.getAttribute('data-cookie-category')
165
+ if (!_hasConsentFor(category)) {
166
+ if (!script.dataset.blocked) {
167
+ script.dataset.blocked = 'true'
168
+ // Already executed scripts can't be un-executed,
169
+ // but we mark them so future loads respect consent
170
+ }
171
+ }
172
+ })
173
+
174
+ // Store observer reference for cleanup
175
+ if (typeof window !== 'undefined') {
176
+ window.__cookieKitScriptObserver = observer
177
+ }
178
+
179
+ return observer
180
+ }
181
+
182
+ /**
183
+ * Unblock and execute scripts for consented categories.
184
+ * @param {Object} categories - { analytics: true, marketing: false }
185
+ */
186
+ export function unblockConsentedScripts(categories = {}) {
187
+ if (typeof document === 'undefined') return
188
+
189
+ document.querySelectorAll('script[data-cookie-category][type="text/plain"]').forEach(script => {
138
190
  const category = script.getAttribute('data-cookie-category')
139
191
  if (categories[category]) {
140
- // Re-enable script
141
- if (script.type === 'text/plain' && script.dataset.originalSrc) {
142
- const newScript = document.createElement('script')
192
+ const newScript = document.createElement('script')
193
+
194
+ // Copy attributes
195
+ for (const attr of script.attributes) {
196
+ if (attr.name !== 'type' && attr.name !== 'data-original-src') {
197
+ newScript.setAttribute(attr.name, attr.value)
198
+ }
199
+ }
200
+
201
+ // Restore src or inline content
202
+ if (script.dataset.originalSrc) {
143
203
  newScript.src = script.dataset.originalSrc
144
204
  newScript.async = true
145
- script.parentNode.replaceChild(newScript, script)
205
+ } else if (script.textContent) {
206
+ newScript.textContent = script.textContent
146
207
  }
208
+
209
+ newScript.type = 'text/javascript'
210
+ script.parentNode.replaceChild(newScript, script)
147
211
  }
148
212
  })
149
213
  }
150
214
 
215
+ /**
216
+ * Check localStorage for existing consent for a category
217
+ * @private
218
+ */
219
+ function _hasConsentFor(category) {
220
+ if (typeof localStorage === 'undefined') return false
221
+ try {
222
+ const stored = localStorage.getItem('cookie_consent')
223
+ if (!stored) return false
224
+ const consent = JSON.parse(stored)
225
+ return consent?.categories?.[category] === true
226
+ } catch {
227
+ return false
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Legacy alias
233
+ * @param {Object} categories - { analytics: true, marketing: false }
234
+ */
235
+ export function manageScriptBlocking(categories = {}) {
236
+ unblockConsentedScripts(categories)
237
+ }
238
+
151
239
  /**
152
240
  * Create analytics manager that auto-integrates with cookie consent
153
241
  * @param {Object} config - { ga4: 'G-XXX', gtm: 'GTM-XXX', metaPixel: 'XXX', yandexMetrica: 'XXX' }
@@ -160,6 +248,9 @@ export function createAnalyticsManager(config = {}) {
160
248
  if (initialized) return
161
249
  initialized = true
162
250
 
251
+ // Block scripts before consent
252
+ blockScriptsBeforeConsent()
253
+
163
254
  // Always init Google Consent Mode first (denied by default)
164
255
  initGoogleConsentMode()
165
256
 
@@ -181,8 +272,8 @@ export function createAnalyticsManager(config = {}) {
181
272
  if (config.metaPixel) loadMetaPixel(config.metaPixel)
182
273
  }
183
274
 
184
- // Manage script blocking
185
- manageScriptBlocking(categories)
275
+ // Unblock scripts for consented categories
276
+ unblockConsentedScripts(categories)
186
277
  }
187
278
 
188
279
  const connectToCookieConsent = () => {
package/src/core/index.js CHANGED
@@ -32,7 +32,7 @@ const DEFAULT_CONFIG = {
32
32
  label: 'Necesare',
33
33
  description: 'Cookie-uri esențiale pentru funcționarea site-ului',
34
34
  required: true,
35
- locked: true,
35
+ disabled: true,
36
36
  enabled: true
37
37
  },
38
38
  analytics: {
@@ -40,24 +40,24 @@ const DEFAULT_CONFIG = {
40
40
  label: 'Analiză',
41
41
  description: 'Cookie-uri pentru analiza traficului și comportamentului utilizatorilor',
42
42
  required: false,
43
- locked: false,
44
- enabled: true
43
+ disabled: false,
44
+ enabled: false
45
45
  },
46
46
  marketing: {
47
47
  id: 'marketing',
48
48
  label: 'Marketing',
49
49
  description: 'Cookie-uri pentru publicitate personalizată',
50
50
  required: false,
51
- locked: false,
52
- enabled: true
51
+ disabled: false,
52
+ enabled: false
53
53
  },
54
54
  preferences: {
55
55
  id: 'preferences',
56
56
  label: 'Preferințe',
57
57
  description: 'Cookie-uri pentru salvarea preferințelor utilizatorului',
58
58
  required: false,
59
- locked: false,
60
- enabled: true
59
+ disabled: false,
60
+ enabled: false
61
61
  }
62
62
  },
63
63