@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.
- package/package.json +21 -1
- package/src/core/AnalyticsManager.js +400 -0
- package/src/core/ConsentManager.js +710 -0
- package/src/core/ConsentMode.js +109 -0
- package/src/core/FocusTrap.js +130 -0
- package/src/core/GeoDetector.js +144 -0
- package/src/core/ScriptLoader.js +229 -0
- package/src/core/StorageAdapter.js +179 -0
- package/src/core/analytics.js +101 -10
- package/src/core/index.js +7 -7
- package/src/geo/GeoDetector.js +536 -0
- package/src/geo/RegionRules.js +126 -0
- package/src/geo/index.js +16 -0
- package/src/index.js +56 -18
- package/src/js/CookieConsent.js +0 -1
- package/src/locales/en.js +54 -0
- package/src/locales/index.js +20 -0
- package/src/locales/ro.js +54 -0
- package/src/plugins/CMPPlugin.js +187 -0
- package/src/plugins/PluginManager.js +234 -0
- package/src/plugins/index.js +7 -0
- package/src/providers/GoogleConsentModeProvider.js +278 -0
- package/src/providers/index.js +6 -0
- package/src/rewriting/ScriptRewriter.js +278 -0
- package/src/rewriting/index.js +6 -0
- package/src/scripts/ScriptLoader.js +310 -0
- package/src/scripts/ScriptManager.js +278 -0
- package/src/scripts/ScriptRegistry.js +175 -0
- package/src/scripts/ScriptScanner.js +178 -0
- package/src/scripts/index.js +9 -0
- package/src/trackers/TrackerDetector.js +488 -0
- package/src/trackers/TrackerPatterns.js +307 -0
- package/src/trackers/TrackerRegistry.js +172 -0
- package/src/trackers/index.js +15 -0
- package/src/utils/cookies.js +37 -0
- package/src/utils/dom.js +58 -0
- package/src/utils/helpers.js +89 -0
- package/src/vue/CookieConsent.vue +1 -1
- 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
|
+
}
|
package/src/core/analytics.js
CHANGED
|
@@ -127,27 +127,115 @@ export function loadYandexMetrica(counterId) {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
|
-
* Block
|
|
131
|
-
*
|
|
132
|
-
*
|
|
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
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
enabled:
|
|
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
|
-
|
|
52
|
-
enabled:
|
|
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
|
-
|
|
60
|
-
enabled:
|
|
59
|
+
disabled: false,
|
|
60
|
+
enabled: false
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
|