@ecomconsult/consentkit 0.3.4 → 0.4.0

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,862 @@
1
+ /*!
2
+ * ConsentKit debug panel (opt-in) — shows what the banner actually did on this
3
+ * page. Off by default: without activation this file creates no DOM, installs
4
+ * no observers and touches nothing.
5
+ *
6
+ * Activation (this browser only, nothing is sent anywhere):
7
+ * ?ck_debug=1 or #ck_debug — turns it on and remembers it (localStorage)
8
+ * ?ck_debug=0 — turns it off and forgets it
9
+ * localStorage.ck_debug = '1' — same as the query parameter
10
+ *
11
+ * Load order: ck-core.js -> ck-locales.js -> ck-ui.js -> [ck-saas.js] -> ck-debug.js
12
+ *
13
+ * Copyright (c) 2026 E-COM CONSULT PLUS. MIT License — see LICENSE.
14
+ */
15
+ (function (global) {
16
+ 'use strict';
17
+
18
+ if (!global || typeof global !== 'object') { return; }
19
+
20
+ var LS_KEY = 'ck_debug';
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Activation (pure, testable: no DOM, no storage)
24
+ // ---------------------------------------------------------------------------
25
+ // Returns { active, persist } where persist is 'on' | 'off' | null:
26
+ // the query/hash form is sticky so the panel survives navigation, the bare
27
+ // localStorage form changes nothing.
28
+ function isOffValue(v) {
29
+ return v === '0' || v === 'false' || v === 'no' || v === '';
30
+ }
31
+
32
+ function parseActivation(search, hash, stored) {
33
+ var on = null;
34
+
35
+ var q = String(search || '');
36
+ if (q.charAt(0) === '?') { q = q.slice(1); }
37
+ var pairs = q ? q.split('&') : [];
38
+ for (var i = 0; i < pairs.length; i++) {
39
+ var eq = pairs[i].indexOf('=');
40
+ var k = eq === -1 ? pairs[i] : pairs[i].slice(0, eq);
41
+ if (k !== LS_KEY) { continue; }
42
+ // ?ck_debug (no "="), ?ck_debug=1, =true, =yes -> on
43
+ // ?ck_debug=0, =false, =no, =(empty) -> off. The empty value is treated
44
+ // as off deliberately: a form or a link builder that drops the value is
45
+ // far more likely to mean "not set" than "switch the panel on".
46
+ on = eq === -1 ? true : !isOffValue(pairs[i].slice(eq + 1));
47
+ }
48
+
49
+ if (on === null) {
50
+ var h = String(hash || '');
51
+ if (h.charAt(0) === '#') { h = h.slice(1); }
52
+ // #ck_debug or #ck_debug=1 (a plain fragment id, not a query)
53
+ if (h === LS_KEY) { on = true; }
54
+ else if (h.indexOf(LS_KEY + '=') === 0) { on = !isOffValue(h.slice(LS_KEY.length + 1)); }
55
+ }
56
+
57
+ if (on === null) {
58
+ return { active: String(stored || '') === '1', persist: null };
59
+ }
60
+ return { active: on, persist: on ? 'on' : 'off' };
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Report (pure, testable: takes plain data, returns plain data)
65
+ // ---------------------------------------------------------------------------
66
+ // No PII by construction: cookie names only (never values), host + path only
67
+ // (never query strings — tracker URLs carry ids there).
68
+ function stripUrl(url) {
69
+ var s = String(url || '');
70
+ var host = '';
71
+ var path = '';
72
+ var m = /^(?:[a-z]+:)?\/\/([^/?#]+)([^?#]*)/i.exec(s);
73
+ if (m) {
74
+ host = m[1].toLowerCase().replace(/:\d+$/, '');
75
+ path = m[2] || '';
76
+ } else {
77
+ path = s.split('?')[0].split('#')[0];
78
+ }
79
+ return { host: host, path: path };
80
+ }
81
+
82
+ // entries: PerformanceResourceTiming-like [{ name, startTime, initiatorType }]
83
+ // consentAtMs: performance-clock ms of the decision, or null when undecided
84
+ // classify: url -> category | null (ConsentKit._categoryForUrl)
85
+ function buildRequests(entries, consentAtMs, classify) {
86
+ var out = [];
87
+ var list = entries || [];
88
+ var seen = {};
89
+ for (var i = 0; i < list.length; i++) {
90
+ var e = list[i];
91
+ if (!e || !e.name) { continue; }
92
+ var cat = null;
93
+ try { cat = classify ? classify(e.name) : null; } catch (err) { cat = null; }
94
+ if (!cat) { continue; } // only known trackers
95
+ var p = stripUrl(e.name);
96
+ var t = Number(e.startTime) || 0;
97
+ var when = consentAtMs === null || consentAtMs === undefined
98
+ ? 'before' : (t >= consentAtMs ? 'after' : 'before');
99
+ var key = p.host + '|' + p.path + '|' + when;
100
+ if (seen[key]) { seen[key].count++; continue; }
101
+ var rec = {
102
+ host: p.host, path: p.path, category: cat, when: when,
103
+ at: Math.round(t), kind: String((e && e.initiatorType) || ''), count: 1
104
+ };
105
+ seen[key] = rec;
106
+ out.push(rec);
107
+ }
108
+ return out;
109
+ }
110
+
111
+ function buildReport(input) {
112
+ var d = input || {};
113
+ var st = d.state || {};
114
+ var cats = st.categories || {};
115
+ return {
116
+ generatedAt: d.now || null,
117
+ client: {
118
+ version: d.version || null,
119
+ source: d.siteId ? 'saas' : 'inline',
120
+ siteId: d.siteId || null,
121
+ policyVersion: st.policyVersion || (d.config && d.config.policyVersion) || null,
122
+ etag: d.etag || null
123
+ },
124
+ consent: {
125
+ status: !st.decided ? 'none'
126
+ : (cats.functional && cats.analytics && cats.marketing) ? 'accepted'
127
+ : (!cats.functional && !cats.analytics && !cats.marketing) ? 'rejected'
128
+ : 'partial',
129
+ categories: {
130
+ necessary: true,
131
+ functional: !!cats.functional,
132
+ analytics: !!cats.analytics,
133
+ marketing: !!cats.marketing
134
+ },
135
+ decidedAt: st.ts || null,
136
+ method: st.method || null,
137
+ ttlDays: d.ttlDays == null ? null : Number(d.ttlDays)
138
+ },
139
+ blocked: (d.blocked || []).map(function (b) {
140
+ // `strict` marks an entry the engine held back only because strict mode
141
+ // is on and the host is an unknown third party — not because the
142
+ // tracker database recognised it.
143
+ return {
144
+ host: b.host, path: b.path, kind: b.kind,
145
+ category: b.category, origin: b.origin, strict: b.strict === true,
146
+ // false when the visitor granted the category but the element never
147
+ // loaded — usually a script that was created and never appended.
148
+ revived: b.revived !== false
149
+ };
150
+ }),
151
+ requests: buildRequests(d.entries, d.consentAtMs, d.classify),
152
+ consentMode: (d.consentMode || []).slice(),
153
+ // Names only — a consent debug panel must never leak cookie contents.
154
+ cookieNames: (d.cookieNames || []).slice(),
155
+ note: NOTE_EN
156
+ };
157
+ }
158
+
159
+ var NOTE_RU = 'Запросы, ушедшие до загрузки ConsentKit (обычный <script src> ' +
160
+ 'в разметке), видны в этом списке, но заблокировать их клиент не может — ' +
161
+ 'такие теги размечают вручную.';
162
+ var NOTE_EN = 'Requests that left before ConsentKit loaded (a plain <script src> ' +
163
+ 'written into the HTML) show up here but cannot be blocked — mark such tags up manually.';
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Panel language (pure; the JSON report stays language-neutral either way)
167
+ // ---------------------------------------------------------------------------
168
+ // The panel is read by whoever is debugging the site, so it follows the same
169
+ // language the banner resolved for this visitor rather than a build flag.
170
+ // Only ru and en exist: this is an internal diagnostic surface, and a
171
+ // half-translated one is worse than an English one.
172
+ var STRINGS = {
173
+ ru: {
174
+ regionLabel: 'ConsentKit — режим отладки',
175
+ collapse: 'Свернуть',
176
+ expand: 'Развернуть',
177
+ closeLabel: 'Закрыть и выключить режим отладки',
178
+ secClient: 'Клиент',
179
+ version: 'версия',
180
+ source: 'источник',
181
+ srcSaas: 'SaaS',
182
+ srcInline: 'инлайн',
183
+ secConsent: 'Согласие',
184
+ decidedAt: 'решение',
185
+ method: 'способ',
186
+ ttl: 'срок cookie',
187
+ days: ' дн.',
188
+ status: { none: 'нет решения', accepted: 'принято', rejected: 'отклонено', partial: 'частично' },
189
+ secBlocked: 'Заблокировано до согласия',
190
+ noBlocked: 'ничего не перехвачено',
191
+ markup: ' (разметка)',
192
+ strict: 'strict',
193
+ notRevived: ' — не ожил после согласия',
194
+ secRequests: 'Запросы к трекерам',
195
+ noRequests: 'запросов к известным трекерам не было',
196
+ after: 'после согласия',
197
+ before: 'до согласия',
198
+ ms: ' мс',
199
+ note: NOTE_RU,
200
+ secConsentMode: 'Consent Mode / dataLayer',
201
+ noEvents: 'событий не было',
202
+ secActions: 'Действия',
203
+ reset: 'Сбросить согласие',
204
+ showPrefs: 'Показать настройки',
205
+ copy: 'Скопировать отчёт',
206
+ copied: 'Скопировано',
207
+ copyFailed: 'Не вышло',
208
+ footer: 'Панель видна только в этом браузере. Выключить: добавьте ?ck_debug=0 к адресу.'
209
+ },
210
+ en: {
211
+ regionLabel: 'ConsentKit — debug mode',
212
+ collapse: 'Collapse',
213
+ expand: 'Expand',
214
+ closeLabel: 'Close and turn debug mode off',
215
+ secClient: 'Client',
216
+ version: 'version',
217
+ source: 'source',
218
+ srcSaas: 'SaaS',
219
+ srcInline: 'inline',
220
+ secConsent: 'Consent',
221
+ decidedAt: 'decided',
222
+ method: 'method',
223
+ ttl: 'cookie lifetime',
224
+ days: ' days',
225
+ status: { none: 'no decision', accepted: 'accepted', rejected: 'rejected', partial: 'partial' },
226
+ secBlocked: 'Blocked until consent',
227
+ noBlocked: 'nothing intercepted',
228
+ markup: ' (markup)',
229
+ strict: 'strict',
230
+ notRevived: ' — did not come back after consent',
231
+ secRequests: 'Tracker requests',
232
+ noRequests: 'no requests to known trackers',
233
+ after: 'after consent',
234
+ before: 'before consent',
235
+ ms: ' ms',
236
+ note: NOTE_EN,
237
+ secConsentMode: 'Consent Mode / dataLayer',
238
+ noEvents: 'no events',
239
+ secActions: 'Actions',
240
+ reset: 'Reset consent',
241
+ showPrefs: 'Show preferences',
242
+ copy: 'Copy report',
243
+ copied: 'Copied',
244
+ copyFailed: 'Failed',
245
+ footer: 'This panel is visible in this browser only. To turn it off, add ?ck_debug=0 to the URL.'
246
+ }
247
+ };
248
+
249
+ // banner language (ConsentKit config) -> navigator.language -> en.
250
+ // `cfgLang` is ConsentKit.config.language, which may be 'auto'.
251
+ function pickLang(cfgLang, navLang) {
252
+ var raw = String(cfgLang || '').toLowerCase();
253
+ if (!raw || raw === 'auto') { raw = String(navLang || '').toLowerCase(); }
254
+ return raw.slice(0, 2) === 'ru' ? 'ru' : 'en';
255
+ }
256
+
257
+ // Testable surface. Published before the activation check so the inactive
258
+ // path is testable too; it is not a public API.
259
+ var API = {
260
+ parseActivation: parseActivation,
261
+ buildReport: buildReport,
262
+ buildRequests: buildRequests,
263
+ stripUrl: stripUrl,
264
+ pickLang: pickLang,
265
+ strings: STRINGS,
266
+ active: false
267
+ };
268
+ try {
269
+ if (global.ConsentKit) { global.ConsentKit._debug = API; }
270
+ global.__ckDebug = API;
271
+ } catch (e) { /* noop */ }
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // Everything below runs only when activated.
275
+ // ---------------------------------------------------------------------------
276
+ var doc = global.document;
277
+ var loc = global.location;
278
+
279
+ function lsGet(k) {
280
+ try { return global.localStorage ? global.localStorage.getItem(k) : null; } catch (e) { return null; }
281
+ }
282
+ function lsSet(k, v) {
283
+ try { if (global.localStorage) { global.localStorage.setItem(k, v); } } catch (e) { /* noop */ }
284
+ }
285
+ function lsDel(k) {
286
+ try { if (global.localStorage) { global.localStorage.removeItem(k); } } catch (e) { /* noop */ }
287
+ }
288
+
289
+ var act = parseActivation(loc && loc.search, loc && loc.hash, lsGet(LS_KEY));
290
+ if (act.persist === 'on') { lsSet(LS_KEY, '1'); }
291
+ if (act.persist === 'off') { lsDel(LS_KEY); }
292
+ if (!act.active) { return; }
293
+ if (!doc || typeof doc.createElement !== 'function') { return; }
294
+ API.active = true;
295
+
296
+ var CK = global.ConsentKit;
297
+
298
+ // ---------------------------------------------------------------------------
299
+ // Observation (installed only when active)
300
+ // ---------------------------------------------------------------------------
301
+ var resources = [];
302
+ var consentAtMs = null; // performance clock, set when consent is given
303
+ var consentMode = []; // last ck_* dataLayer events and gtag consent calls
304
+ var CM_MAX = 20;
305
+
306
+ function nowMs() {
307
+ try {
308
+ if (global.performance && typeof global.performance.now === 'function') {
309
+ return global.performance.now();
310
+ }
311
+ } catch (e) { /* noop */ }
312
+ return 0;
313
+ }
314
+
315
+ // A decision restored from a previous page load happened before this page
316
+ // started, so every request on this load counts as "after".
317
+ function seedConsentTime() {
318
+ try {
319
+ var st = CK && CK.getState ? CK.getState() : null;
320
+ if (st && st.decided && consentAtMs === null) { consentAtMs = 0; }
321
+ } catch (e) { /* noop */ }
322
+ }
323
+
324
+ // Stamp the moment the visitor decides, BEFORE the core acts on it.
325
+ //
326
+ // This cannot wait for ck:change: accept() revives the blocked scripts
327
+ // synchronously and dispatches ck:change afterwards, so by the time the event
328
+ // fires those scripts have already issued their requests. Stamping on the
329
+ // event would then place them *before* the cutoff and the panel would report
330
+ // "до согласия" for the very requests the consent just released — exactly
331
+ // backwards, and on the one line an owner reads most carefully.
332
+ //
333
+ // Wrapping is observe-only: the original method is called with the original
334
+ // arguments and its return value passed straight back.
335
+ function stampOnDecision() {
336
+ try {
337
+ if (!CK) { return; }
338
+ ['accept', 'rejectAll'].forEach(function (name) {
339
+ var orig = CK[name];
340
+ if (typeof orig !== 'function' || orig.__ckDebugWrapped) { return; }
341
+ var wrapped = function () {
342
+ try { consentAtMs = nowMs(); } catch (e) { /* noop */ }
343
+ return orig.apply(CK, arguments);
344
+ };
345
+ wrapped.__ckDebugWrapped = true;
346
+ CK[name] = wrapped;
347
+ });
348
+ // withdraw() puts the page back to "no decision yet".
349
+ var w = CK.withdraw;
350
+ if (typeof w === 'function' && !w.__ckDebugWrapped) {
351
+ var wrappedW = function () {
352
+ try { consentAtMs = null; } catch (e) { /* noop */ }
353
+ return w.apply(CK, arguments);
354
+ };
355
+ wrappedW.__ckDebugWrapped = true;
356
+ CK.withdraw = wrappedW;
357
+ }
358
+ } catch (e) { /* noop */ }
359
+ }
360
+
361
+ function collectResources() {
362
+ try {
363
+ var perf = global.performance;
364
+ if (!perf) { return; }
365
+ if (typeof perf.getEntriesByType === 'function') {
366
+ var buffered = perf.getEntriesByType('resource') || [];
367
+ for (var i = 0; i < buffered.length; i++) { resources.push(buffered[i]); }
368
+ }
369
+ if (typeof global.PerformanceObserver === 'function') {
370
+ var po = new global.PerformanceObserver(function (list) {
371
+ try {
372
+ var got = list.getEntries() || [];
373
+ for (var j = 0; j < got.length; j++) { resources.push(got[j]); }
374
+ schedule();
375
+ } catch (e2) { /* noop */ }
376
+ });
377
+ // buffered:true re-delivers what happened before we attached; harmless
378
+ // duplicates are deduped when the list is rendered.
379
+ try { po.observe({ type: 'resource', buffered: true }); }
380
+ catch (e3) { try { po.observe({ entryTypes: ['resource'] }); } catch (e4) { /* noop */ } }
381
+ }
382
+ } catch (e) { /* noop */ }
383
+ }
384
+
385
+ // Observe only: the existing dataLayer.push is called first and its return
386
+ // value passed through, so a GTM/other wrapper keeps working.
387
+ function watchDataLayer() {
388
+ try {
389
+ var dl = global.dataLayer;
390
+ if (!dl) { global.dataLayer = dl = []; }
391
+ if (typeof dl.push !== 'function' || dl.__ckDebugWatched) { return; }
392
+ // Whatever is already in the queue counts too.
393
+ for (var i = 0; i < dl.length; i++) { noteDataLayer(dl[i]); }
394
+ var prev = dl.push;
395
+ dl.push = function () {
396
+ try {
397
+ for (var j = 0; j < arguments.length; j++) { noteDataLayer(arguments[j]); }
398
+ } catch (e) { /* never break the host page */ }
399
+ return prev.apply(this, arguments);
400
+ };
401
+ dl.__ckDebugWatched = true;
402
+ } catch (e) { /* noop */ }
403
+ }
404
+
405
+ function noteDataLayer(arg) {
406
+ try {
407
+ var rec = null;
408
+ // gtag() pushes an arguments object: ['consent', 'default'|'update', {...}]
409
+ if (arg && typeof arg === 'object' && typeof arg.length === 'number' && arg[0] === 'consent') {
410
+ var signals = {};
411
+ var payload = arg[2];
412
+ if (payload && typeof payload === 'object') {
413
+ for (var k in payload) {
414
+ if (Object.prototype.hasOwnProperty.call(payload, k)) { signals[k] = payload[k]; }
415
+ }
416
+ }
417
+ rec = { type: 'gtag consent ' + String(arg[1] || ''), signals: signals, at: Math.round(nowMs()) };
418
+ } else if (arg && typeof arg === 'object' && typeof arg.event === 'string' &&
419
+ arg.event.indexOf('ck_') === 0) {
420
+ rec = { type: arg.event, signals: null, at: Math.round(nowMs()) };
421
+ }
422
+ if (!rec) { return; }
423
+ consentMode.push(rec);
424
+ if (consentMode.length > CM_MAX) { consentMode.shift(); }
425
+ schedule();
426
+ } catch (e) { /* noop */ }
427
+ }
428
+
429
+ function cookieNames() {
430
+ var names = [];
431
+ try {
432
+ var raw = typeof doc.cookie === 'string' ? doc.cookie : '';
433
+ var parts = raw.split(';');
434
+ for (var i = 0; i < parts.length; i++) {
435
+ var t = parts[i].trim();
436
+ if (!t) { continue; }
437
+ var eq = t.indexOf('=');
438
+ names.push(eq === -1 ? t : t.slice(0, eq)); // name only, never the value
439
+ }
440
+ } catch (e) { /* noop */ }
441
+ return names;
442
+ }
443
+
444
+ function saasInfo() {
445
+ var out = { siteId: null, etag: null };
446
+ try {
447
+ if (CK && CK._saas) {
448
+ out.siteId = CK._saas.siteId || null;
449
+ var cached = null;
450
+ try {
451
+ var raw = lsGet('ck_cfg_' + out.siteId);
452
+ cached = raw ? JSON.parse(raw) : null;
453
+ } catch (e2) { cached = null; }
454
+ out.etag = cached && cached.etag ? cached.etag : null;
455
+ }
456
+ } catch (e) { /* noop */ }
457
+ return out;
458
+ }
459
+
460
+ function reportInput() {
461
+ var s = saasInfo();
462
+ var cfg = (CK && CK.config) || {};
463
+ return {
464
+ now: new Date().toISOString(),
465
+ version: CK ? CK.version : null,
466
+ state: CK && CK.getState ? CK.getState() : {},
467
+ config: cfg,
468
+ siteId: s.siteId,
469
+ etag: s.etag,
470
+ ttlDays: cfg.consentTtlDays == null ? null : cfg.consentTtlDays,
471
+ blocked: (CK && typeof CK._blocked === 'function') ? CK._blocked() : [],
472
+ entries: resources,
473
+ consentAtMs: consentAtMs,
474
+ classify: CK ? CK._categoryForUrl : null,
475
+ consentMode: consentMode,
476
+ cookieNames: cookieNames()
477
+ };
478
+ }
479
+
480
+ // ---------------------------------------------------------------------------
481
+ // Panel (own Shadow DOM host, own styles)
482
+ // ---------------------------------------------------------------------------
483
+ var CSS = [
484
+ ':host{all:initial;position:fixed;right:12px;bottom:12px;z-index:2147483646;',
485
+ 'font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace}',
486
+ '*{box-sizing:border-box}',
487
+ '.w{width:min(360px,calc(100vw - 24px));background:#12151c;color:#dfe3ea;',
488
+ 'border:1px solid #2b3040;border-radius:8px;box-shadow:0 6px 24px rgba(0,0,0,.4);overflow:hidden}',
489
+ '.hd{display:flex;align-items:center;gap:8px;padding:7px 9px;background:#1a1f2a;',
490
+ 'border-bottom:1px solid #2b3040}',
491
+ '.hd b{font-weight:600;color:#fff;font-size:12px}',
492
+ '.hd .sp{flex:1}',
493
+ 'button{font:inherit;color:#dfe3ea;background:#232936;border:1px solid #39415400;',
494
+ 'border-color:#394154;border-radius:5px;padding:3px 8px;cursor:pointer}',
495
+ 'button:hover{background:#2c3444}',
496
+ 'button:focus-visible{outline:2px solid #7aa2ff;outline-offset:1px}',
497
+ '.bd{max-height:min(60vh,460px);overflow:auto;padding:2px 9px 9px}',
498
+ '.bd[hidden]{display:none}',
499
+ 'section{border-top:1px solid #232936;padding:7px 0}',
500
+ 'section:first-child{border-top:0}',
501
+ 'h2{margin:0 0 4px;font-size:11px;font-weight:600;color:#8d96ab;text-transform:uppercase;',
502
+ 'letter-spacing:.04em}',
503
+ 'dl{margin:0;display:grid;grid-template-columns:auto 1fr;gap:1px 8px}',
504
+ 'dt{color:#8d96ab}',
505
+ 'dd{margin:0;overflow-wrap:anywhere}',
506
+ 'ul{margin:0;padding:0;list-style:none}',
507
+ 'li{padding:2px 0;border-top:1px dotted #262c3a;overflow-wrap:anywhere}',
508
+ 'li:first-child{border-top:0}',
509
+ '.t{display:inline-block;padding:0 5px;border-radius:3px;font-size:10px;',
510
+ 'background:#2a3142;color:#a9b3c9;margin-right:4px}',
511
+ '.t.on{background:#173a24;color:#7ee2a0}',
512
+ '.t.off{background:#3a1a1d;color:#ff9aa2}',
513
+ '.mut{color:#8d96ab}',
514
+ '.note{margin:6px 0 0;color:#8d96ab;font-size:11px;line-height:1.4}',
515
+ '.row{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px}',
516
+ '@media (prefers-reduced-motion:no-preference){button{transition:background-color .12s ease}}'
517
+ ].join('');
518
+
519
+ var host = null, root = null, body = null, toggleBtn = null, collapsed = false;
520
+ var frame = 0;
521
+
522
+ function el(tag, attrs, text) {
523
+ var n = doc.createElement(tag);
524
+ if (attrs) {
525
+ for (var k in attrs) {
526
+ if (Object.prototype.hasOwnProperty.call(attrs, k)) { n.setAttribute(k, attrs[k]); }
527
+ }
528
+ }
529
+ if (text != null) { n.textContent = String(text); }
530
+ return n;
531
+ }
532
+
533
+ function section(title) {
534
+ var s = doc.createElement('section');
535
+ s.appendChild(el('h2', null, title));
536
+ return s;
537
+ }
538
+
539
+ function defs(pairs) {
540
+ var dl = doc.createElement('dl');
541
+ for (var i = 0; i < pairs.length; i++) {
542
+ dl.appendChild(el('dt', null, pairs[i][0]));
543
+ dl.appendChild(el('dd', null, pairs[i][1] == null || pairs[i][1] === '' ? '—' : pairs[i][1]));
544
+ }
545
+ return dl;
546
+ }
547
+
548
+ function tag(text, cls) { return el('span', { class: 't' + (cls ? ' ' + cls : '') }, text); }
549
+
550
+ // Resolved lazily, not at parse time: this file runs before ConsentKit.init()
551
+ // has merged the site's config, so asking for the language now would always
552
+ // read the built-in default. Re-resolved on every render so a page that
553
+ // switches language at runtime switches the panel too.
554
+ var T = STRINGS.en;
555
+ function refreshLang() {
556
+ var nav = global.navigator;
557
+ T = STRINGS[pickLang(
558
+ (CK && CK.config && CK.config.language) || '',
559
+ (nav && (nav.language || nav.userLanguage)) || ''
560
+ )] || STRINGS.en;
561
+ return T;
562
+ }
563
+
564
+ function render() {
565
+ if (!body) { return; }
566
+ refreshLang();
567
+ var r = buildReport(reportInput());
568
+ body.textContent = '';
569
+
570
+ // 1. Client
571
+ var s1 = section(T.secClient);
572
+ s1.appendChild(defs([
573
+ [T.version, r.client.version],
574
+ [T.source, r.client.source === 'saas' ? T.srcSaas : T.srcInline],
575
+ ['siteId', r.client.siteId],
576
+ ['policyVersion', r.client.policyVersion],
577
+ ['ETag', r.client.etag]
578
+ ]));
579
+ body.appendChild(s1);
580
+
581
+ // 2. Consent
582
+ var s2 = section(T.secConsent);
583
+ var line = el('div');
584
+ line.appendChild(tag(T.status[r.consent.status] || r.consent.status,
585
+ r.consent.status === 'accepted' ? 'on' : r.consent.status === 'rejected' ? 'off' : ''));
586
+ s2.appendChild(line);
587
+ var cl = doc.createElement('div');
588
+ ['necessary', 'functional', 'analytics', 'marketing'].forEach(function (c) {
589
+ cl.appendChild(tag((r.consent.categories[c] ? '✓ ' : '✗ ') + c,
590
+ r.consent.categories[c] ? 'on' : 'off'));
591
+ });
592
+ s2.appendChild(cl);
593
+ s2.appendChild(defs([
594
+ [T.decidedAt, r.consent.decidedAt],
595
+ [T.method, r.consent.method],
596
+ [T.ttl, r.consent.ttlDays == null ? null : r.consent.ttlDays + T.days]
597
+ ]));
598
+ body.appendChild(s2);
599
+
600
+ // 3. Blocked until consent
601
+ var s3 = section(T.secBlocked + ' (' + r.blocked.length + ')');
602
+ if (!r.blocked.length) {
603
+ s3.appendChild(el('div', { class: 'mut' }, T.noBlocked));
604
+ } else {
605
+ var u3 = doc.createElement('ul');
606
+ r.blocked.forEach(function (b) {
607
+ var li = doc.createElement('li');
608
+ li.appendChild(tag(b.kind));
609
+ li.appendChild(tag(b.category || '?'));
610
+ if (b.strict) { li.appendChild(tag(T.strict, 'off')); }
611
+ li.appendChild(doc.createTextNode(b.host + b.path +
612
+ (b.origin === 'markup' ? T.markup : '') +
613
+ (b.revived === false ? T.notRevived : '')));
614
+ u3.appendChild(li);
615
+ });
616
+ s3.appendChild(u3);
617
+ }
618
+ body.appendChild(s3);
619
+
620
+ // 4. Tracker requests
621
+ var s4 = section(T.secRequests + ' (' + r.requests.length + ')');
622
+ if (!r.requests.length) {
623
+ s4.appendChild(el('div', { class: 'mut' }, T.noRequests));
624
+ } else {
625
+ var u4 = doc.createElement('ul');
626
+ r.requests.forEach(function (q) {
627
+ var li = doc.createElement('li');
628
+ li.appendChild(tag(q.when === 'after' ? T.after : T.before,
629
+ q.when === 'after' ? 'on' : 'off'));
630
+ li.appendChild(tag(q.category));
631
+ li.appendChild(doc.createTextNode(q.host + q.path + ' · ' + q.at + T.ms +
632
+ (q.count > 1 ? ' ×' + q.count : '')));
633
+ u4.appendChild(li);
634
+ });
635
+ s4.appendChild(u4);
636
+ }
637
+ s4.appendChild(el('p', { class: 'note' }, T.note));
638
+ body.appendChild(s4);
639
+
640
+ // 5. Consent Mode
641
+ var s5 = section(T.secConsentMode + ' (' + r.consentMode.length + ')');
642
+ if (!r.consentMode.length) {
643
+ s5.appendChild(el('div', { class: 'mut' }, T.noEvents));
644
+ } else {
645
+ var u5 = doc.createElement('ul');
646
+ r.consentMode.slice().reverse().forEach(function (c) {
647
+ var li = doc.createElement('li');
648
+ li.appendChild(tag(c.at + T.ms));
649
+ var txt = c.type;
650
+ if (c.signals) {
651
+ var bits = [];
652
+ for (var k in c.signals) {
653
+ if (Object.prototype.hasOwnProperty.call(c.signals, k)) {
654
+ bits.push(k + '=' + c.signals[k]);
655
+ }
656
+ }
657
+ if (bits.length) { txt += ': ' + bits.join(', '); }
658
+ }
659
+ li.appendChild(doc.createTextNode(txt));
660
+ u5.appendChild(li);
661
+ });
662
+ s5.appendChild(u5);
663
+ }
664
+ body.appendChild(s5);
665
+
666
+ // 6. Buttons
667
+ var s6 = section(T.secActions);
668
+ var row = el('div', { class: 'row' });
669
+ var bReset = el('button', { type: 'button' }, T.reset);
670
+ bReset.addEventListener('click', resetConsent);
671
+ var bShow = el('button', { type: 'button' }, T.showPrefs);
672
+ bShow.addEventListener('click', showBanner);
673
+ var bCopy = el('button', { type: 'button' }, T.copy);
674
+ bCopy.addEventListener('click', function () { copyReport(bCopy); });
675
+ row.appendChild(bReset); row.appendChild(bShow); row.appendChild(bCopy);
676
+ s6.appendChild(row);
677
+ s6.appendChild(el('p', { class: 'note' }, T.footer));
678
+ body.appendChild(s6);
679
+ }
680
+
681
+ function schedule() {
682
+ if (frame) { return; }
683
+ frame = 1;
684
+ var run = function () { frame = 0; try { render(); } catch (e) { /* noop */ } };
685
+ try {
686
+ if (typeof global.requestAnimationFrame === 'function') { global.requestAnimationFrame(run); }
687
+ else { global.setTimeout(run, 50); }
688
+ } catch (e) { run(); }
689
+ }
690
+
691
+ // ---------------------------------------------------------------------------
692
+ // Buttons
693
+ // ---------------------------------------------------------------------------
694
+ function resetConsent() {
695
+ try { if (CK && CK.withdraw) { CK.withdraw(); } } catch (e) { /* noop */ }
696
+ // withdraw() already clears the record; belt and braces for a page whose
697
+ // cookie was written on a parent domain.
698
+ try {
699
+ var h = global.location && global.location.hostname ? global.location.hostname : '';
700
+ var variants = ['', h ? '; domain=' + h : '', h ? '; domain=.' + h : ''];
701
+ var labels = h ? h.split('.') : [];
702
+ if (labels.length > 2) { variants.push('; domain=.' + labels.slice(-2).join('.')); }
703
+ for (var i = 0; i < variants.length; i++) {
704
+ doc.cookie = 'ck_consent=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/' + variants[i];
705
+ }
706
+ } catch (e) { /* noop */ }
707
+ lsDel('ck_consent');
708
+ try { global.location.reload(); } catch (e) { /* noop */ }
709
+ }
710
+
711
+ function showBanner() {
712
+ // Non-destructive on purpose: this opens the preferences panel and leaves
713
+ // the stored decision alone. Erasing consent is the other button's job, and
714
+ // on a live site a "show banner" click must never wipe the owner's record.
715
+ try { if (CK && CK.show) { CK.show(); } } catch (e) { /* noop */ }
716
+ schedule();
717
+ }
718
+
719
+ function copyReport(btn) {
720
+ var text = '';
721
+ try { text = JSON.stringify(buildReport(reportInput()), null, 2); } catch (e) { text = '{}'; }
722
+ var done = function (ok) {
723
+ try {
724
+ btn.textContent = ok ? T.copied : T.copyFailed;
725
+ global.setTimeout(function () { btn.textContent = T.copy; }, 1500);
726
+ } catch (e2) { /* noop */ }
727
+ };
728
+ try {
729
+ if (global.navigator && global.navigator.clipboard && global.navigator.clipboard.writeText) {
730
+ global.navigator.clipboard.writeText(text).then(function () { done(true); },
731
+ function () { done(fallbackCopy(text)); });
732
+ return;
733
+ }
734
+ } catch (e) { /* noop */ }
735
+ done(fallbackCopy(text));
736
+ }
737
+
738
+ function fallbackCopy(text) {
739
+ try {
740
+ var ta = doc.createElement('textarea');
741
+ ta.value = text;
742
+ ta.setAttribute('style', 'position:fixed;top:-9999px;left:-9999px');
743
+ doc.body.appendChild(ta);
744
+ ta.select();
745
+ var ok = doc.execCommand ? doc.execCommand('copy') : false;
746
+ doc.body.removeChild(ta);
747
+ return !!ok;
748
+ } catch (e) { return false; }
749
+ }
750
+
751
+ // ---------------------------------------------------------------------------
752
+ // Mount
753
+ // ---------------------------------------------------------------------------
754
+ // The banner's bar layout sits at bottom:16 across the full width and the UI's
755
+ // floating button at bottom-left; lift the panel above whatever is at the
756
+ // bottom right now so its buttons stay clickable.
757
+ function avoidBanner() {
758
+ if (!host) { return; }
759
+ var bottom = 12;
760
+ try {
761
+ var ckRoot = doc.getElementById('ck-root');
762
+ var sr = ckRoot && ckRoot.shadowRoot;
763
+ // The bar spans the width at bottom:16; the box-right card sits exactly
764
+ // where this panel does. Both must be cleared (the UI's floating button
765
+ // is bottom-left and never collides).
766
+ var banner = sr && sr.querySelector(
767
+ '.ck-banner--bar.ck-pos-bottom, .ck-banner--box.ck-pos-bottom-right');
768
+ if (banner && banner.getBoundingClientRect) {
769
+ var r = banner.getBoundingClientRect();
770
+ if (r.height > 0 && r.bottom > (global.innerHeight || 0) - r.height - 40) {
771
+ bottom = Math.round(r.height) + 24;
772
+ }
773
+ }
774
+ } catch (e) { /* noop */ }
775
+ try { host.style.bottom = bottom + 'px'; } catch (e2) { /* noop */ }
776
+ }
777
+
778
+ function mount() {
779
+ if (host || !doc.body) { return; }
780
+ // A page can carry the loader (inline block) AND a manual <script> tag for
781
+ // the panel: the second copy must not mount a second panel.
782
+ if (doc.querySelector('ck-debug')) { return; }
783
+ refreshLang();
784
+ host = doc.createElement('ck-debug');
785
+ host.setAttribute('aria-live', 'off');
786
+ root = host.attachShadow ? host.attachShadow({ mode: 'open' }) : null;
787
+ if (!root) { host = null; return; }
788
+
789
+ var style = doc.createElement('style');
790
+ style.textContent = CSS;
791
+ root.appendChild(style);
792
+
793
+ var wrap = el('div', { class: 'w', role: 'region', 'aria-label': T.regionLabel });
794
+ var head = el('div', { class: 'hd' });
795
+ head.appendChild(el('b', null, 'ConsentKit debug'));
796
+ head.appendChild(el('span', { class: 'sp' }));
797
+
798
+ toggleBtn = el('button', { type: 'button', 'aria-expanded': 'true' }, T.collapse);
799
+ toggleBtn.addEventListener('click', function () {
800
+ collapsed = !collapsed;
801
+ body.hidden = collapsed;
802
+ toggleBtn.textContent = collapsed ? T.expand : T.collapse;
803
+ toggleBtn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
804
+ if (!collapsed) { schedule(); }
805
+ });
806
+ head.appendChild(toggleBtn);
807
+
808
+ var closeBtn = el('button', { type: 'button', 'aria-label': T.closeLabel }, '×');
809
+ closeBtn.addEventListener('click', function () {
810
+ lsDel(LS_KEY);
811
+ try { host.parentNode.removeChild(host); } catch (e) { /* noop */ }
812
+ host = null;
813
+ API.active = false;
814
+ });
815
+ head.appendChild(closeBtn);
816
+
817
+ wrap.appendChild(head);
818
+ body = el('div', { class: 'bd' });
819
+ wrap.appendChild(body);
820
+ root.appendChild(wrap);
821
+ doc.body.appendChild(host);
822
+
823
+ render();
824
+ avoidBanner();
825
+ }
826
+
827
+ function boot() {
828
+ seedConsentTime();
829
+ stampOnDecision();
830
+ collectResources();
831
+ watchDataLayer();
832
+ mount();
833
+
834
+ try {
835
+ doc.addEventListener('ck:change', function () {
836
+ // Fallback for a decision made through some other path than
837
+ // accept()/rejectAll() (a host page calling into the core directly).
838
+ // stampOnDecision() has usually set this already, and its stamp is the
839
+ // accurate one — do not overwrite it here: entry.startTime is on the
840
+ // performance clock, and by now the revived scripts have already run.
841
+ try {
842
+ var st = CK && CK.getState ? CK.getState() : null;
843
+ if (st && st.decided) { if (consentAtMs === null) { consentAtMs = nowMs(); } }
844
+ else { consentAtMs = null; }
845
+ } catch (e2) { /* noop */ }
846
+ schedule();
847
+ global.setTimeout(avoidBanner, 60);
848
+ }, false);
849
+ doc.addEventListener('ck:init', function () { schedule(); global.setTimeout(avoidBanner, 60); }, false);
850
+ } catch (e) { /* noop */ }
851
+
852
+ // Late trackers and revived scripts keep arriving after the first render.
853
+ try { global.setInterval(schedule, 3000); } catch (e) { /* noop */ }
854
+ try { global.addEventListener('resize', avoidBanner, false); } catch (e) { /* noop */ }
855
+ }
856
+
857
+ try {
858
+ if (doc.readyState === 'loading') {
859
+ doc.addEventListener('DOMContentLoaded', function () { try { boot(); } catch (e) { /* noop */ } }, false);
860
+ } else { boot(); }
861
+ } catch (e) { /* noop */ }
862
+ })(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this));