@el7ven/cookie-kit 0.3.1 → 0.3.3
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/index.js +4 -4
- 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 +55 -17
- 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 +4 -12
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Consent Manager — handles consent state, persistence, and events
|
|
3
|
+
* @module ConsentManager
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { StorageAdapter } from './StorageAdapter.js'
|
|
7
|
+
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
consentLevel: 'standard', // 'lite' | 'standard' | 'enterprise'
|
|
10
|
+
configVersion: '1.0.0', // Config version for consent invalidation
|
|
11
|
+
schemaVersion: 1, // Consent data schema version (will be overridden by COOKIE_CONSENT_CONFIG)
|
|
12
|
+
policyVersion: null, // Privacy policy version (e.g., '2026-03')
|
|
13
|
+
storageKey: 'cookie_consent',
|
|
14
|
+
storageType: 'localStorage', // 'localStorage' | 'cookie' | 'both'
|
|
15
|
+
consentExpireDays: 365, // GDPR: consent expiration in days
|
|
16
|
+
mode: 'gdpr', // 'gdpr' | 'ccpa' | 'lgpd' | 'essential'
|
|
17
|
+
debug: false,
|
|
18
|
+
categories: {},
|
|
19
|
+
enableHash: true, // Enable consent hash for integrity verification (enterprise only)
|
|
20
|
+
enableHistory: false, // Enable consent history tracking (enterprise only)
|
|
21
|
+
enableIpHash: false, // Enable IP hash for proof of consent (enterprise only)
|
|
22
|
+
auditLog: false, // Enable audit logging for GDPR compliance (enterprise only)
|
|
23
|
+
domain: null, // Domain for meta tracking
|
|
24
|
+
language: null, // Language for meta tracking
|
|
25
|
+
tenant: null // Multi-tenant support (domain identifier)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULT_CONSENT_STATE = {
|
|
29
|
+
consentId: null, // UUID for proof of consent (GDPR)
|
|
30
|
+
timestamp: null, // ISO 8601 format
|
|
31
|
+
expiresAt: null, // ISO 8601 format
|
|
32
|
+
status: null, // 'accepted' | 'rejected' | 'custom'
|
|
33
|
+
source: 'banner', // 'banner' | 'settings' | 'api' | 'migration'
|
|
34
|
+
configVersion: null, // Config version at time of consent
|
|
35
|
+
policyVersion: null, // Privacy policy version at time of consent
|
|
36
|
+
schemaVersion: 1, // Schema version for migration support
|
|
37
|
+
region: 'ROW', // EU, EEA, UK, US, CA, ROW (Rest of World)
|
|
38
|
+
categories: {},
|
|
39
|
+
hash: null, // Integrity hash (SHA-256)
|
|
40
|
+
meta: {
|
|
41
|
+
userAgent: null,
|
|
42
|
+
language: null,
|
|
43
|
+
domain: null,
|
|
44
|
+
ipHash: null // SHA-256 hash of IP (never store raw IP)
|
|
45
|
+
},
|
|
46
|
+
history: [] // Array of previous consent states with actions
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class ConsentManager {
|
|
50
|
+
constructor(config = {}) {
|
|
51
|
+
this.config = { ...DEFAULT_CONFIG, ...config }
|
|
52
|
+
this.listeners = new Map()
|
|
53
|
+
this.storage = new StorageAdapter(this.config)
|
|
54
|
+
|
|
55
|
+
// Auto-fix null/undefined data in storage
|
|
56
|
+
this._fixCorruptedStorage()
|
|
57
|
+
|
|
58
|
+
this._normalizeCategories()
|
|
59
|
+
this._initializePublicAPI()
|
|
60
|
+
this._debug('ConsentManager initialized', this.config)
|
|
61
|
+
this._debug('Storage type:', this.storage.getActiveStorageType())
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// --- Category Normalization ---
|
|
65
|
+
|
|
66
|
+
_normalizeCategories() {
|
|
67
|
+
const raw = this.config.categories
|
|
68
|
+
if (Array.isArray(raw)) {
|
|
69
|
+
this.config.categories = raw.reduce((acc, cat) => {
|
|
70
|
+
acc[cat.id] = cat
|
|
71
|
+
return acc
|
|
72
|
+
}, {})
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getCategories() {
|
|
77
|
+
return { ...this.config.categories }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getRequiredCategories() {
|
|
81
|
+
return Object.keys(this.config.categories).filter(
|
|
82
|
+
id => this.config.categories[id].required
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getOptionalCategories() {
|
|
87
|
+
return Object.keys(this.config.categories).filter(
|
|
88
|
+
id => !this.config.categories[id].required
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- Consent State ---
|
|
93
|
+
|
|
94
|
+
getConsent() {
|
|
95
|
+
if (typeof window === 'undefined') return null
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const stored = this._read()
|
|
99
|
+
if (!stored || stored === 'null' || stored === 'undefined') return null
|
|
100
|
+
|
|
101
|
+
const consent = JSON.parse(stored)
|
|
102
|
+
if (!consent || typeof consent !== 'object') return null
|
|
103
|
+
|
|
104
|
+
// Config version mismatch — re-consent needed
|
|
105
|
+
if (consent.configVersion !== this.config.configVersion) {
|
|
106
|
+
this._debug('Config version mismatch, clearing consent')
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Hash verification (if enabled) - CRITICAL for tamper detection
|
|
111
|
+
if (this.config.enableHash && consent.hash) {
|
|
112
|
+
if (!this._verifyHashSync(consent)) {
|
|
113
|
+
this._debug('Hash verification failed, consent may be tampered')
|
|
114
|
+
this._logAudit('consent_hash_mismatch', { consentId: consent.consentId })
|
|
115
|
+
this._emitEvent('consent:integrity-failed', { consentId: consent.consentId })
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Expiry check (GDPR compliance) - using expiresAt
|
|
121
|
+
if (consent.expiresAt) {
|
|
122
|
+
const expiresDate = new Date(consent.expiresAt)
|
|
123
|
+
if (Date.now() > expiresDate.getTime()) {
|
|
124
|
+
this._debug('Consent expired at', consent.expiresAt)
|
|
125
|
+
this._logAudit('consent_expired', { consentId: consent.consentId })
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Fallback: check timestamp if expiresAt is missing (backward compatibility)
|
|
131
|
+
if (!consent.expiresAt && consent.timestamp) {
|
|
132
|
+
const timestampDate = new Date(consent.timestamp)
|
|
133
|
+
const maxAge = this.config.consentExpireDays * 24 * 60 * 60 * 1000
|
|
134
|
+
if (Date.now() - timestampDate.getTime() > maxAge) {
|
|
135
|
+
this._debug('Consent expired after', this.config.consentExpireDays, 'days')
|
|
136
|
+
this._logAudit('consent_expired', { consentId: consent.consentId })
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Return consent with computed fields
|
|
142
|
+
return {
|
|
143
|
+
...consent,
|
|
144
|
+
hasConsented: consent.status === 'accepted' || consent.status === 'rejected' || consent.status === 'custom'
|
|
145
|
+
}
|
|
146
|
+
} catch (e) {
|
|
147
|
+
this._debug('Error reading consent', e)
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async saveConsent(categories, options = {}) {
|
|
153
|
+
const { source = 'banner', ipAddress = null } = options
|
|
154
|
+
if (typeof window === 'undefined') return null
|
|
155
|
+
|
|
156
|
+
// Ensure required categories are always true
|
|
157
|
+
const finalCategories = { ...categories }
|
|
158
|
+
this.getRequiredCategories().forEach(id => {
|
|
159
|
+
finalCategories[id] = true
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Get previous consent for history
|
|
163
|
+
const previousConsent = this.getConsent()
|
|
164
|
+
|
|
165
|
+
// Calculate expiration date
|
|
166
|
+
const now = new Date()
|
|
167
|
+
const expiresAt = new Date(now.getTime() + this.config.consentExpireDays * 24 * 60 * 60 * 1000)
|
|
168
|
+
|
|
169
|
+
// Determine consent status based on enabled categories only
|
|
170
|
+
let status = 'custom'
|
|
171
|
+
const enabledCategories = Object.keys(this.config.categories).filter(id => this.config.categories[id].enabled)
|
|
172
|
+
const requiredCategories = this.getRequiredCategories().filter(id => this.config.categories[id].enabled)
|
|
173
|
+
const optionalCategories = enabledCategories.filter(id => !requiredCategories.includes(id))
|
|
174
|
+
|
|
175
|
+
// For essential mode with only required categories, status should be 'accepted'
|
|
176
|
+
if (optionalCategories.length === 0) {
|
|
177
|
+
// No optional categories - user accepted required categories only
|
|
178
|
+
status = 'accepted'
|
|
179
|
+
} else {
|
|
180
|
+
// Has optional categories - check user's choice
|
|
181
|
+
const allOptionalAccepted = optionalCategories.every(id => finalCategories[id] === true)
|
|
182
|
+
const allOptionalRejected = optionalCategories.every(id => finalCategories[id] === false)
|
|
183
|
+
status = allOptionalAccepted ? 'accepted' : (allOptionalRejected ? 'rejected' : 'custom')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Build consent object based on tier level
|
|
187
|
+
const consentLevel = this.config.consentLevel || 'standard'
|
|
188
|
+
|
|
189
|
+
// Tier 1: Lite - minimal consent (120 bytes)
|
|
190
|
+
let consent = {
|
|
191
|
+
status,
|
|
192
|
+
categories: finalCategories,
|
|
193
|
+
configVersion: this.config.configVersion,
|
|
194
|
+
timestamp: now.toISOString(),
|
|
195
|
+
expiresAt: expiresAt.toISOString(),
|
|
196
|
+
consentId: this._generateConsentId()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Tier 2: Standard - GDPR compliance (300 bytes)
|
|
200
|
+
if (consentLevel !== 'lite') {
|
|
201
|
+
consent.source = source
|
|
202
|
+
consent.policyVersion = this.config.policyVersion
|
|
203
|
+
consent.region = this._detectRegion()
|
|
204
|
+
consent.hasConsented = true
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Tier 3: Enterprise - full compliance (700 bytes)
|
|
208
|
+
if (consentLevel === 'enterprise') {
|
|
209
|
+
consent.schemaVersion = this.config.schemaVersion
|
|
210
|
+
consent.meta = {
|
|
211
|
+
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : null,
|
|
212
|
+
language: this.config.language || (typeof navigator !== 'undefined' ? navigator.language : null),
|
|
213
|
+
domain: this.config.domain || (typeof window !== 'undefined' ? window.location.hostname : null),
|
|
214
|
+
ipHash: null // Will be set if enableIpHash is true
|
|
215
|
+
}
|
|
216
|
+
consent.history = []
|
|
217
|
+
consent.hash = null // Will be set below
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Add to history if enabled (enterprise only)
|
|
221
|
+
if (consentLevel === 'enterprise' && this.config.enableHistory && previousConsent) {
|
|
222
|
+
consent.history = [
|
|
223
|
+
...(previousConsent.history || []),
|
|
224
|
+
{
|
|
225
|
+
timestamp: previousConsent.timestamp,
|
|
226
|
+
action: previousConsent.status || 'unknown',
|
|
227
|
+
categories: previousConsent.categories
|
|
228
|
+
}
|
|
229
|
+
].slice(-10) // Keep last 10 changes
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Generate IP hash if enabled (enterprise only)
|
|
233
|
+
if (consentLevel === 'enterprise' && this.config.enableIpHash && ipAddress && consent.meta) {
|
|
234
|
+
consent.meta.ipHash = await this._generateIpHash(ipAddress)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Generate hash for integrity (enterprise only)
|
|
238
|
+
if (consentLevel === 'enterprise' && this.config.enableHash) {
|
|
239
|
+
consent.hash = await this._generateHash(consent)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
// Validate consent before saving
|
|
244
|
+
if (!consent || typeof consent !== 'object') {
|
|
245
|
+
this._debug('Invalid consent object, not saving', consent)
|
|
246
|
+
return null
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const jsonString = JSON.stringify(consent)
|
|
250
|
+
|
|
251
|
+
// Double-check we're not saving null or undefined
|
|
252
|
+
if (jsonString === 'null' || jsonString === 'undefined') {
|
|
253
|
+
this._debug('Invalid JSON string detected, not saving', jsonString)
|
|
254
|
+
return null
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this._write(jsonString)
|
|
258
|
+
this._debug('Consent saved', consent)
|
|
259
|
+
this._logAudit('consent_saved', { consentId: consent.consentId, categories: finalCategories })
|
|
260
|
+
this._emit('consentChanged', { consent, categories: finalCategories })
|
|
261
|
+
|
|
262
|
+
// Emit per-category events
|
|
263
|
+
Object.keys(finalCategories).forEach(catId => {
|
|
264
|
+
this._emit('categoryChanged', {
|
|
265
|
+
category: catId,
|
|
266
|
+
granted: finalCategories[catId]
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
return consent
|
|
271
|
+
} catch (e) {
|
|
272
|
+
this._debug('Error saving consent', e)
|
|
273
|
+
return null
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
hasConsent() {
|
|
278
|
+
const consent = this.getConsent()
|
|
279
|
+
// For lite level, check if consent exists and has status
|
|
280
|
+
// For standard/enterprise, check hasConsented flag
|
|
281
|
+
return consent && (consent.hasConsented === true || consent.status)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
hasCategoryConsent(categoryId) {
|
|
285
|
+
const consent = this.getConsent()
|
|
286
|
+
return consent?.categories?.[categoryId] === true
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async acceptAll(source = 'banner', ipAddress = null) {
|
|
290
|
+
const categories = {}
|
|
291
|
+
Object.keys(this.config.categories).forEach(id => {
|
|
292
|
+
// Only save enabled categories
|
|
293
|
+
// For essential mode: only necessary will be enabled
|
|
294
|
+
// For GDPR mode: all enabled categories will be saved
|
|
295
|
+
if (this.config.categories[id].enabled) {
|
|
296
|
+
categories[id] = true
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
this._debug('Accept all')
|
|
300
|
+
const consent = await this.saveConsent(categories, { source, ipAddress })
|
|
301
|
+
return consent
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async rejectAll(source = 'banner', ipAddress = null) {
|
|
305
|
+
const categories = {}
|
|
306
|
+
Object.keys(this.config.categories).forEach(id => {
|
|
307
|
+
// Only save enabled categories
|
|
308
|
+
// Required categories get true, optional enabled categories get false
|
|
309
|
+
if (this.config.categories[id].enabled) {
|
|
310
|
+
categories[id] = this.config.categories[id].required || false
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
this._debug('Reject all')
|
|
314
|
+
const consent = await this.saveConsent(categories, { source, ipAddress })
|
|
315
|
+
return consent
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async acceptSelected(selectedIds, source = 'settings', ipAddress = null) {
|
|
319
|
+
const categories = {}
|
|
320
|
+
Object.keys(this.config.categories).forEach(id => {
|
|
321
|
+
// Only save enabled categories
|
|
322
|
+
// Selected or required categories get true, others get false
|
|
323
|
+
if (this.config.categories[id].enabled) {
|
|
324
|
+
categories[id] = selectedIds.includes(id) || this.config.categories[id].required
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
this._debug('Accept selected', selectedIds)
|
|
328
|
+
const consent = await this.saveConsent(categories, { source, ipAddress })
|
|
329
|
+
return consent
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
clearConsent() {
|
|
333
|
+
try {
|
|
334
|
+
this._remove()
|
|
335
|
+
this._debug('Consent cleared')
|
|
336
|
+
this._emit('consentCleared', {})
|
|
337
|
+
return true
|
|
338
|
+
} catch (e) {
|
|
339
|
+
return false
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
getDefaultCategoriesState() {
|
|
344
|
+
const categories = {}
|
|
345
|
+
Object.keys(this.config.categories).forEach(id => {
|
|
346
|
+
categories[id] = this.config.categories[id].required || false
|
|
347
|
+
})
|
|
348
|
+
return categories
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// --- Storage Abstraction (Safe Storage Adapter) ---
|
|
352
|
+
|
|
353
|
+
_read() {
|
|
354
|
+
return this.storage.get()
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
_write(value) {
|
|
358
|
+
return this.storage.set(this.config.storageKey || 'cookie_consent', value)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
_remove() {
|
|
362
|
+
return this.storage.remove(this.config.storageKey || 'cookie_consent')
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// --- GDPR Compliance Helpers ---
|
|
366
|
+
|
|
367
|
+
_generateConsentId() {
|
|
368
|
+
// Use crypto.randomUUID if available (modern browsers)
|
|
369
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
370
|
+
return crypto.randomUUID()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Fallback: cryptographically secure UUID v4 generator
|
|
374
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
375
|
+
const r = crypto.getRandomValues(new Uint8Array(1))[0] % 16
|
|
376
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
|
377
|
+
return v.toString(16)
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async _generateHash(consent) {
|
|
382
|
+
// SHA-256 hash for integrity verification
|
|
383
|
+
const data = JSON.stringify({
|
|
384
|
+
consentId: consent.consentId,
|
|
385
|
+
timestamp: consent.timestamp,
|
|
386
|
+
categories: consent.categories,
|
|
387
|
+
configVersion: consent.configVersion,
|
|
388
|
+
policyVersion: consent.policyVersion,
|
|
389
|
+
region: consent.region,
|
|
390
|
+
schemaVersion: consent.schemaVersion
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
// Use Web Crypto API for SHA-256 (modern browsers)
|
|
394
|
+
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
395
|
+
try {
|
|
396
|
+
const encoder = new TextEncoder()
|
|
397
|
+
const dataBuffer = encoder.encode(data)
|
|
398
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer)
|
|
399
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
|
400
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
|
401
|
+
} catch (e) {
|
|
402
|
+
this._debug('Crypto API not available, using fallback hash')
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Fallback: simple hash function for older browsers
|
|
407
|
+
return this._generateHashFallback(data)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
_verifyHashSync(consent) {
|
|
411
|
+
// Synchronous hash verification for getConsent()
|
|
412
|
+
const data = JSON.stringify({
|
|
413
|
+
consentId: consent.consentId,
|
|
414
|
+
timestamp: consent.timestamp,
|
|
415
|
+
categories: consent.categories,
|
|
416
|
+
configVersion: consent.configVersion,
|
|
417
|
+
policyVersion: consent.policyVersion,
|
|
418
|
+
region: consent.region,
|
|
419
|
+
schemaVersion: consent.schemaVersion
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
const expectedHash = this._generateHashFallback(data)
|
|
423
|
+
return expectedHash === consent.hash
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
_generateHashFallback(data) {
|
|
427
|
+
// Simple hash function for sync verification
|
|
428
|
+
let hash = 0
|
|
429
|
+
for (let i = 0; i < data.length; i++) {
|
|
430
|
+
const char = data.charCodeAt(i)
|
|
431
|
+
hash = ((hash << 5) - hash) + char
|
|
432
|
+
hash = hash & hash // Convert to 32bit integer
|
|
433
|
+
}
|
|
434
|
+
return Math.abs(hash).toString(16)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async _generateIpHash(ip) {
|
|
438
|
+
// SHA-256 hash of IP address (NEVER store raw IP)
|
|
439
|
+
if (!ip) return null
|
|
440
|
+
|
|
441
|
+
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
442
|
+
try {
|
|
443
|
+
const encoder = new TextEncoder()
|
|
444
|
+
const dataBuffer = encoder.encode(ip)
|
|
445
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer)
|
|
446
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
|
447
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
|
448
|
+
} catch (e) {
|
|
449
|
+
this._debug('Crypto API not available for IP hash')
|
|
450
|
+
return null
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return null
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
_fixCorruptedStorage() {
|
|
458
|
+
try {
|
|
459
|
+
const stored = this._read()
|
|
460
|
+
|
|
461
|
+
// Check for null/undefined/literal null string
|
|
462
|
+
if (!stored || stored === 'null' || stored === 'undefined') {
|
|
463
|
+
this._debug('Detected corrupted storage data, removing...')
|
|
464
|
+
this._remove()
|
|
465
|
+
return
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Try to parse and validate
|
|
469
|
+
try {
|
|
470
|
+
const consent = JSON.parse(stored)
|
|
471
|
+
|
|
472
|
+
if (!consent || typeof consent !== 'object') {
|
|
473
|
+
this._debug('Invalid consent object detected, removing...')
|
|
474
|
+
this._remove()
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Check for required fields
|
|
479
|
+
const requiredFields = ['consentId', 'timestamp', 'categories']
|
|
480
|
+
const missingFields = requiredFields.filter(field => !consent[field])
|
|
481
|
+
|
|
482
|
+
if (missingFields.length > 0) {
|
|
483
|
+
this._debug('Missing required fields, removing corrupted data:', missingFields)
|
|
484
|
+
this._remove()
|
|
485
|
+
return
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Check categories object integrity
|
|
489
|
+
if (!consent.categories || typeof consent.categories !== 'object') {
|
|
490
|
+
this._debug('Invalid categories object, removing corrupted data')
|
|
491
|
+
this._remove()
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
this._debug('Storage data is valid')
|
|
496
|
+
|
|
497
|
+
} catch (parseError) {
|
|
498
|
+
this._debug('JSON parse error, removing corrupted data:', parseError.message)
|
|
499
|
+
this._remove()
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
} catch (e) {
|
|
503
|
+
this._debug('Error checking storage integrity:', e)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
_detectRegion() {
|
|
508
|
+
// Try to detect region from config or timezone
|
|
509
|
+
if (this.config.region) return this.config.region
|
|
510
|
+
|
|
511
|
+
if (typeof Intl === 'undefined') return 'ROW'
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
515
|
+
|
|
516
|
+
// EU timezones
|
|
517
|
+
const euTimezones = [
|
|
518
|
+
'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Europe/Madrid',
|
|
519
|
+
'Europe/Rome', 'Europe/Amsterdam', 'Europe/Brussels', 'Europe/Vienna',
|
|
520
|
+
'Europe/Stockholm', 'Europe/Copenhagen', 'Europe/Helsinki', 'Europe/Athens',
|
|
521
|
+
'Europe/Prague', 'Europe/Warsaw', 'Europe/Budapest', 'Europe/Bucharest'
|
|
522
|
+
]
|
|
523
|
+
|
|
524
|
+
if (euTimezones.some(tz => timezone.includes(tz.split('/')[1]))) {
|
|
525
|
+
return 'EU'
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (timezone.startsWith('America/New_York') || timezone.startsWith('America/Los_Angeles')) {
|
|
529
|
+
return 'US'
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (timezone.startsWith('America/Toronto')) {
|
|
533
|
+
return 'CA'
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return 'ROW'
|
|
537
|
+
} catch (e) {
|
|
538
|
+
return 'ROW'
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
_logAudit(action, data) {
|
|
543
|
+
if (!this.config.auditLog) return
|
|
544
|
+
|
|
545
|
+
const auditEntry = {
|
|
546
|
+
timestamp: Date.now(),
|
|
547
|
+
action,
|
|
548
|
+
...data
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
this._debug('Audit log:', auditEntry)
|
|
552
|
+
this._emit('auditLog', auditEntry)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
resetConsent() {
|
|
556
|
+
this._debug('Resetting consent')
|
|
557
|
+
this._logAudit('consent_reset', {})
|
|
558
|
+
return this.clearConsent()
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
getConsentProof() {
|
|
562
|
+
const consent = this.getConsent()
|
|
563
|
+
if (!consent) return null
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
consentId: consent.consentId,
|
|
567
|
+
timestamp: consent.timestamp,
|
|
568
|
+
expiresAt: consent.expiresAt,
|
|
569
|
+
configVersion: consent.configVersion,
|
|
570
|
+
region: consent.region,
|
|
571
|
+
categories: consent.categories,
|
|
572
|
+
hash: consent.hash,
|
|
573
|
+
meta: consent.meta
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Get consent history
|
|
578
|
+
getConsentHistory() {
|
|
579
|
+
const consent = this.getConsent()
|
|
580
|
+
return consent?.history || []
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// --- Event System ---
|
|
584
|
+
|
|
585
|
+
on(event, callback) {
|
|
586
|
+
if (!this.listeners.has(event)) {
|
|
587
|
+
this.listeners.set(event, [])
|
|
588
|
+
}
|
|
589
|
+
this.listeners.get(event).push(callback)
|
|
590
|
+
return () => this.off(event, callback)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
off(event, callback) {
|
|
594
|
+
if (!this.listeners.has(event)) return
|
|
595
|
+
const list = this.listeners.get(event).filter(cb => cb !== callback)
|
|
596
|
+
this.listeners.set(event, list)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
_emit(event, data) {
|
|
600
|
+
this._debug(`Event: ${event}`, data)
|
|
601
|
+
|
|
602
|
+
// Internal listeners
|
|
603
|
+
if (this.listeners.has(event)) {
|
|
604
|
+
this.listeners.get(event).forEach(cb => cb(data))
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Emit DOM CustomEvent (old format for backward compatibility)
|
|
608
|
+
this._emitEvent(event, data)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
_emitEvent(eventName, detail) {
|
|
612
|
+
// Enterprise event system - dispatches CustomEvents to window
|
|
613
|
+
if (typeof window === 'undefined') return
|
|
614
|
+
|
|
615
|
+
// New format: cookie-consent:event-name
|
|
616
|
+
window.dispatchEvent(new CustomEvent(`cookie-consent:${eventName}`, { detail }))
|
|
617
|
+
|
|
618
|
+
// Legacy format: cookieConsent:eventName (backward compatibility)
|
|
619
|
+
window.dispatchEvent(new CustomEvent(`cookieConsent:${eventName}`, { detail }))
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// --- Public API (window.CookieConsent) ---
|
|
623
|
+
|
|
624
|
+
_initializePublicAPI() {
|
|
625
|
+
if (typeof window === 'undefined') return
|
|
626
|
+
|
|
627
|
+
// Public API for external scripts
|
|
628
|
+
window.CookieConsent = {
|
|
629
|
+
has: (category) => this.hasCategoryConsent(category),
|
|
630
|
+
get: () => this.getConsent(),
|
|
631
|
+
getProof: () => this.getConsentProof(),
|
|
632
|
+
region: () => this._detectRegion(),
|
|
633
|
+
categories: () => this.config.categories,
|
|
634
|
+
acceptAll: (source) => this.acceptAll(source),
|
|
635
|
+
rejectAll: (source) => this.rejectAll(source),
|
|
636
|
+
acceptSelected: (categories, source) => this.acceptSelected(categories, source),
|
|
637
|
+
reset: () => this.resetConsent(),
|
|
638
|
+
on: (event, callback) => this.on(event, callback),
|
|
639
|
+
off: (event, callback) => this.off(event, callback),
|
|
640
|
+
// Internal access for CMP Platform
|
|
641
|
+
consentManager: this
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Debug mode API
|
|
645
|
+
if (this.config.debug) {
|
|
646
|
+
window.__COOKIE_CONSENT__ = {
|
|
647
|
+
tier: () => {
|
|
648
|
+
const tierInfo = `
|
|
649
|
+
ConsentKit v${this.config.configVersion}
|
|
650
|
+
Tier: ${this.config.consentLevel || 'standard'}
|
|
651
|
+
Region: ${this._detectRegion()}
|
|
652
|
+
Storage: ${this.storage.getActiveStorageType()}
|
|
653
|
+
`.trim()
|
|
654
|
+
console.log(tierInfo)
|
|
655
|
+
return {
|
|
656
|
+
version: this.config.configVersion,
|
|
657
|
+
tier: this.config.consentLevel || 'standard',
|
|
658
|
+
region: this._detectRegion(),
|
|
659
|
+
storage: this.storage.getActiveStorageType()
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
show: () => {
|
|
663
|
+
this._emitEvent('show-banner', {})
|
|
664
|
+
return 'Banner shown'
|
|
665
|
+
},
|
|
666
|
+
reset: () => {
|
|
667
|
+
this.resetConsent()
|
|
668
|
+
return 'Consent reset'
|
|
669
|
+
},
|
|
670
|
+
state: () => {
|
|
671
|
+
const consent = this.getConsent()
|
|
672
|
+
console.table(consent?.categories || {})
|
|
673
|
+
return consent
|
|
674
|
+
},
|
|
675
|
+
proof: () => {
|
|
676
|
+
const proof = this.getConsentProof()
|
|
677
|
+
console.log('Consent Proof:', proof)
|
|
678
|
+
return proof
|
|
679
|
+
},
|
|
680
|
+
history: () => {
|
|
681
|
+
const history = this.getConsentHistory()
|
|
682
|
+
console.table(history)
|
|
683
|
+
return history
|
|
684
|
+
},
|
|
685
|
+
storage: () => {
|
|
686
|
+
return {
|
|
687
|
+
type: this.storage.getActiveStorageType(),
|
|
688
|
+
available: this.storage.isAvailable()
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
config: () => {
|
|
692
|
+
console.log('Config:', this.config)
|
|
693
|
+
return this.config
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
this._debug('Debug mode enabled. Use window.__COOKIE_CONSENT__ in console')
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// --- Debug ---
|
|
702
|
+
|
|
703
|
+
_debug(...args) {
|
|
704
|
+
if (this.config.debug) {
|
|
705
|
+
console.log('[CookieConsent]', ...args)
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export { DEFAULT_CONFIG, DEFAULT_CONSENT_STATE }
|