@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.
@@ -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 }