@ecomconsult/consentkit 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/LICENSE +33 -0
- package/README.md +433 -0
- package/npm/core.cjs +67 -0
- package/npm/core.mjs +33 -0
- package/npm/index.cjs +80 -0
- package/npm/index.d.ts +224 -0
- package/npm/index.mjs +47 -0
- package/npm/internal-stub.mjs +83 -0
- package/npm/react.mjs +153 -0
- package/package.json +65 -0
- package/src/ck-core.js +1044 -0
- package/src/ck-locales.js +936 -0
- package/src/ck-saas.js +363 -0
- package/src/ck-ui.js +1421 -0
package/src/ck-core.js
ADDED
|
@@ -0,0 +1,1044 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* ConsentKit Core — consent state + storage + blocking engine + Google Consent Mode v2.
|
|
3
|
+
* Vanilla ES2020, zero dependencies, no build step.
|
|
4
|
+
* Blocking activates at parse time, before ConsentKit.init().
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2026 E-COM CONSULT PLUS. MIT License — see LICENSE.
|
|
7
|
+
*/
|
|
8
|
+
(function (global) {
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
if (!global) { return; }
|
|
12
|
+
var doc = global.document;
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Natives captured before any patching, so unblocking never re-enters patches.
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
var nativeCreateElement = doc && doc.createElement ? doc.createElement.bind(doc) : null;
|
|
18
|
+
var nativeSetAttribute = (global.Element && global.Element.prototype && global.Element.prototype.setAttribute) || null;
|
|
19
|
+
var nativeScriptSrcDesc = null;
|
|
20
|
+
try {
|
|
21
|
+
if (global.HTMLScriptElement && global.HTMLScriptElement.prototype) {
|
|
22
|
+
nativeScriptSrcDesc = Object.getOwnPropertyDescriptor(global.HTMLScriptElement.prototype, 'src');
|
|
23
|
+
}
|
|
24
|
+
} catch (e) { /* noop */ }
|
|
25
|
+
|
|
26
|
+
// Internal flag: while true, patches let everything through (used when we
|
|
27
|
+
// re-create previously blocked scripts after consent).
|
|
28
|
+
var bypass = false;
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Constants
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
var STORAGE_KEY = 'ck_consent';
|
|
34
|
+
var CATEGORIES = ['necessary', 'functional', 'analytics', 'marketing'];
|
|
35
|
+
var OPT_IN = ['functional', 'analytics', 'marketing'];
|
|
36
|
+
|
|
37
|
+
// Domain database: host -> category. Matching is suffix-based (see
|
|
38
|
+
// categoryForUrl), so a bare registrable domain also covers its subdomains.
|
|
39
|
+
// Bare entries are used ONLY for domains dedicated entirely to tracking;
|
|
40
|
+
// where a parent domain also serves ordinary site assets (CDNs, fonts,
|
|
41
|
+
// images) the specific tracking subdomain is listed instead.
|
|
42
|
+
// Snapshot: 2026-08. Not exhaustive — extended as new trackers appear.
|
|
43
|
+
var HOST_DB = {
|
|
44
|
+
// --- analytics -----------------------------------------------------
|
|
45
|
+
'google-analytics.com': 'analytics',
|
|
46
|
+
'ssl.google-analytics.com': 'analytics',
|
|
47
|
+
'analytics.google.com': 'analytics', // subdomain only: google.com is not a tracker
|
|
48
|
+
'mc.yandex.ru': 'analytics',
|
|
49
|
+
'mc.yandex.com': 'analytics',
|
|
50
|
+
'hotjar.com': 'analytics', // static./script./vars. subdomains
|
|
51
|
+
'hotjar.io': 'analytics',
|
|
52
|
+
'clarity.ms': 'analytics', // Microsoft Clarity
|
|
53
|
+
'matomo.cloud': 'analytics', // Matomo Cloud tenants
|
|
54
|
+
'mixpanel.com': 'analytics', // cdn.mxpnl.com below
|
|
55
|
+
'mxpnl.com': 'analytics',
|
|
56
|
+
'amplitude.com': 'analytics',
|
|
57
|
+
'segment.com': 'analytics',
|
|
58
|
+
'segment.io': 'analytics',
|
|
59
|
+
'heap.io': 'analytics',
|
|
60
|
+
'heapanalytics.com': 'analytics',
|
|
61
|
+
'fullstory.com': 'analytics',
|
|
62
|
+
'mouseflow.com': 'analytics',
|
|
63
|
+
'smartlook.com': 'analytics',
|
|
64
|
+
'smartlook.cloud': 'analytics',
|
|
65
|
+
'plausible.io': 'analytics',
|
|
66
|
+
'crazyegg.com': 'analytics',
|
|
67
|
+
'logrocket.com': 'analytics',
|
|
68
|
+
'lr-ingest.io': 'analytics', // LogRocket ingest
|
|
69
|
+
'newrelic.com': 'analytics', // browser agent
|
|
70
|
+
'nr-data.net': 'analytics', // New Relic beacon
|
|
71
|
+
'datadoghq.com': 'analytics', // RUM
|
|
72
|
+
'datadoghq-browser-agent.com': 'analytics',
|
|
73
|
+
|
|
74
|
+
// --- marketing -----------------------------------------------------
|
|
75
|
+
'connect.facebook.net': 'marketing',
|
|
76
|
+
'facebook.net': 'marketing', // connect.facebook.net covered above
|
|
77
|
+
'analytics.tiktok.com': 'marketing',
|
|
78
|
+
'googleadservices.com': 'marketing',
|
|
79
|
+
'doubleclick.net': 'marketing',
|
|
80
|
+
'googlesyndication.com': 'marketing',
|
|
81
|
+
'criteo.com': 'marketing',
|
|
82
|
+
'criteo.net': 'marketing',
|
|
83
|
+
'taboola.com': 'marketing',
|
|
84
|
+
'outbrain.com': 'marketing',
|
|
85
|
+
'snap.licdn.com': 'marketing', // subdomain: licdn.com serves LinkedIn assets
|
|
86
|
+
'ads.linkedin.com': 'marketing',
|
|
87
|
+
'static.ads-twitter.com': 'marketing',
|
|
88
|
+
'ads-twitter.com': 'marketing',
|
|
89
|
+
'ct.pinterest.com': 'marketing', // subdomain: pinterest.com is a normal site
|
|
90
|
+
'sc-static.net': 'marketing', // Snapchat pixel CDN
|
|
91
|
+
'bat.bing.com': 'marketing', // subdomain: bing.com is a normal site
|
|
92
|
+
'adroll.com': 'marketing',
|
|
93
|
+
'hs-analytics.net': 'marketing', // HubSpot tracking
|
|
94
|
+
'hs-scripts.com': 'marketing',
|
|
95
|
+
'hsadspixel.net': 'marketing',
|
|
96
|
+
'chimpstatic.com': 'marketing', // Mailchimp connected sites
|
|
97
|
+
'list-manage.com': 'marketing',
|
|
98
|
+
'redditstatic.com': 'marketing',
|
|
99
|
+
'q.quora.com': 'marketing', // subdomain: quora.com is a normal site
|
|
100
|
+
'amazon-adsystem.com': 'marketing',
|
|
101
|
+
|
|
102
|
+
// --- functional ----------------------------------------------------
|
|
103
|
+
'intercom.io': 'functional',
|
|
104
|
+
'intercomcdn.com': 'functional',
|
|
105
|
+
'zopim.com': 'functional', // Zendesk Chat
|
|
106
|
+
'zdassets.com': 'functional', // Zendesk widget
|
|
107
|
+
'crisp.chat': 'functional',
|
|
108
|
+
'tawk.to': 'functional',
|
|
109
|
+
'livechatinc.com': 'functional',
|
|
110
|
+
'drift.com': 'functional',
|
|
111
|
+
'driftt.com': 'functional',
|
|
112
|
+
'jivosite.com': 'functional',
|
|
113
|
+
'jivo.chat': 'functional',
|
|
114
|
+
'tidio.co': 'functional',
|
|
115
|
+
'tidiochat.com': 'functional'
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Path-fragment database: URL substring -> category. Matched against the full
|
|
119
|
+
// resolved URL, so it can distinguish two very different scripts served from
|
|
120
|
+
// the SAME host (googletagmanager.com).
|
|
121
|
+
//
|
|
122
|
+
// WHY THE GTM CONTAINER IS NOT BLOCKED (do not "fix" this by putting
|
|
123
|
+
// googletagmanager.com back into HOST_DB):
|
|
124
|
+
// gtm.js is a CONTAINER, not a tracker. Google's consent model is that the
|
|
125
|
+
// container always loads and the tags INSIDE it wait — Google tags obey
|
|
126
|
+
// Consent Mode (our 'default: denied' is pushed at parse time, before any
|
|
127
|
+
// tag can fire), and everything else can hang off our ck_consent_* trigger
|
|
128
|
+
// events. Blocking the container breaks that model for every GTM site: the
|
|
129
|
+
// tags never get the chance to see consent at all.
|
|
130
|
+
// gtag.js is the opposite case — a direct GA4/Ads install with no container
|
|
131
|
+
// in front of it — so it stays blocked by path.
|
|
132
|
+
var PATH_DB = {
|
|
133
|
+
'/gtag/js': 'analytics', // googletagmanager.com/gtag/js?id=G-XXXX
|
|
134
|
+
'/trackers/ga.js': 'analytics',
|
|
135
|
+
'/trackers/pixel.js': 'marketing',
|
|
136
|
+
'/trackers/chat.js': 'functional'
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Cookie masks cleared on reject/withdraw, grouped by the category that owns
|
|
140
|
+
// them, so a partial accept only clears the categories that stay denied.
|
|
141
|
+
//
|
|
142
|
+
// Scope note: only FIRST-PARTY cookies (written on the host site's domain) can
|
|
143
|
+
// be removed via document.cookie. Cookies a vendor sets on its own domain
|
|
144
|
+
// (e.g. LinkedIn's bcookie on .linkedin.com) are out of reach and deliberately
|
|
145
|
+
// not listed. Exact names are preferred over prefixes; prefixes are used only
|
|
146
|
+
// where the suffix is dynamic (a property/site id) and the stem is distinctive
|
|
147
|
+
// enough not to collide with unrelated cookies. Snapshot: 2026-08.
|
|
148
|
+
var COOKIE_EXACT = {
|
|
149
|
+
analytics: [
|
|
150
|
+
'_ga', '_gid', '_gat',
|
|
151
|
+
'_clck', '_clsk', // Clarity (NOT a _cl prefix: too broad)
|
|
152
|
+
'_hjSessionUser', '_hjSession', '_hjIncludedInSessionSample',
|
|
153
|
+
'_fs_uid', // FullStory
|
|
154
|
+
'mf_user', // Mouseflow
|
|
155
|
+
'ajs_user_id', 'ajs_anonymous_id', // Segment
|
|
156
|
+
'is_returning', // Crazy Egg
|
|
157
|
+
'demo_ga'
|
|
158
|
+
],
|
|
159
|
+
marketing: [
|
|
160
|
+
'_fbp', '_fbc', '_ttp', '_gcl_au', '_gcl_aw',
|
|
161
|
+
'_scid', '_scid_r', // Snapchat
|
|
162
|
+
'_uetsid', '_uetvid', // Microsoft/Bing Ads
|
|
163
|
+
'_rdt_uuid', // Reddit
|
|
164
|
+
'_pin_unauth', // Pinterest
|
|
165
|
+
'li_fat_id', // LinkedIn Insight (first-party)
|
|
166
|
+
'__adroll', '__adroll_fpc',
|
|
167
|
+
'demo_fbp'
|
|
168
|
+
],
|
|
169
|
+
functional: [
|
|
170
|
+
'demo_chat'
|
|
171
|
+
]
|
|
172
|
+
};
|
|
173
|
+
var COOKIE_PREFIX = {
|
|
174
|
+
analytics: [
|
|
175
|
+
'_ga_', // GA4 per-property
|
|
176
|
+
'_ym_', // Yandex Metrica
|
|
177
|
+
'_hj', // Hotjar (vendor-owned stem)
|
|
178
|
+
'_pk_', // Matomo
|
|
179
|
+
'ajs_', // Segment
|
|
180
|
+
'_dd_' // Datadog RUM
|
|
181
|
+
],
|
|
182
|
+
marketing: [
|
|
183
|
+
'_gac_', // Google Ads per-campaign
|
|
184
|
+
'_ttp_',
|
|
185
|
+
'cto_', // Criteo
|
|
186
|
+
'hubspotutk', '__hstc', '__hssrc', '__hssc'
|
|
187
|
+
],
|
|
188
|
+
functional: [
|
|
189
|
+
'intercom-', // Intercom widget state
|
|
190
|
+
'drift_', 'driftt_',
|
|
191
|
+
'crisp-client',
|
|
192
|
+
'__tawkuuid'
|
|
193
|
+
]
|
|
194
|
+
};
|
|
195
|
+
// demo_* cookies not matched above are only cleared on a full purge.
|
|
196
|
+
var FULL_PURGE_PREFIX = ['demo_'];
|
|
197
|
+
|
|
198
|
+
var DEFAULT_CONFIG = {
|
|
199
|
+
policyVersion: '1',
|
|
200
|
+
language: 'auto',
|
|
201
|
+
layout: { type: 'bar', position: 'bottom' },
|
|
202
|
+
theme: { accent: '#2B50D8', radius: '10px' },
|
|
203
|
+
categories: {
|
|
204
|
+
functional: { enabled: true },
|
|
205
|
+
analytics: { enabled: true },
|
|
206
|
+
marketing: { enabled: true }
|
|
207
|
+
},
|
|
208
|
+
consentTtlDays: 365,
|
|
209
|
+
integrations: { gcm: true, gtmDataLayer: true },
|
|
210
|
+
cookieTable: []
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Small utilities (all defensive — core must never throw)
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
function noop() {}
|
|
217
|
+
|
|
218
|
+
function clone(o) {
|
|
219
|
+
try { return JSON.parse(JSON.stringify(o)); } catch (e) { return {}; }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isPlainObject(v) {
|
|
223
|
+
return !!v && typeof v === 'object' && !Array.isArray(v);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Shallow merge one level deep so partial nested config keeps sibling defaults.
|
|
227
|
+
function mergeConfig(base, patch) {
|
|
228
|
+
var out = clone(base);
|
|
229
|
+
if (!isPlainObject(patch)) { return out; }
|
|
230
|
+
Object.keys(patch).forEach(function (k) {
|
|
231
|
+
var v = patch[k];
|
|
232
|
+
if (isPlainObject(v) && isPlainObject(out[k])) {
|
|
233
|
+
Object.keys(v).forEach(function (k2) {
|
|
234
|
+
if (isPlainObject(v[k2]) && isPlainObject(out[k][k2])) {
|
|
235
|
+
var inner = out[k][k2];
|
|
236
|
+
Object.keys(v[k2]).forEach(function (k3) { inner[k3] = v[k2][k3]; });
|
|
237
|
+
} else {
|
|
238
|
+
out[k][k2] = v[k2];
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
} else {
|
|
242
|
+
out[k] = v;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function uuid() {
|
|
249
|
+
try {
|
|
250
|
+
if (global.crypto && typeof global.crypto.randomUUID === 'function') {
|
|
251
|
+
return global.crypto.randomUUID();
|
|
252
|
+
}
|
|
253
|
+
if (global.crypto && typeof global.crypto.getRandomValues === 'function') {
|
|
254
|
+
var b = new Uint8Array(16);
|
|
255
|
+
global.crypto.getRandomValues(b);
|
|
256
|
+
b[6] = (b[6] & 0x0f) | 0x40;
|
|
257
|
+
b[8] = (b[8] & 0x3f) | 0x80;
|
|
258
|
+
var hex = [];
|
|
259
|
+
for (var i = 0; i < 16; i++) { hex.push((b[i] + 0x100).toString(16).slice(1)); }
|
|
260
|
+
return hex.slice(0, 4).join('') + '-' + hex.slice(4, 6).join('') + '-' +
|
|
261
|
+
hex.slice(6, 8).join('') + '-' + hex.slice(8, 10).join('') + '-' +
|
|
262
|
+
hex.slice(10, 16).join('');
|
|
263
|
+
}
|
|
264
|
+
} catch (e) { /* fall through */ }
|
|
265
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
266
|
+
var r = (Math.random() * 16) | 0;
|
|
267
|
+
return (c === 'x' ? r : ((r & 0x3) | 0x8)).toString(16);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function b64encode(str) {
|
|
272
|
+
try {
|
|
273
|
+
if (typeof global.btoa === 'function') {
|
|
274
|
+
return global.btoa(unescape(encodeURIComponent(str)));
|
|
275
|
+
}
|
|
276
|
+
if (typeof global.Buffer === 'function') {
|
|
277
|
+
return global.Buffer.from(str, 'utf8').toString('base64');
|
|
278
|
+
}
|
|
279
|
+
} catch (e) { /* noop */ }
|
|
280
|
+
return '';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function b64decode(str) {
|
|
284
|
+
try {
|
|
285
|
+
if (typeof global.atob === 'function') {
|
|
286
|
+
return decodeURIComponent(escape(global.atob(str)));
|
|
287
|
+
}
|
|
288
|
+
if (typeof global.Buffer === 'function') {
|
|
289
|
+
return global.Buffer.from(str, 'base64').toString('utf8');
|
|
290
|
+
}
|
|
291
|
+
} catch (e) { /* noop */ }
|
|
292
|
+
return '';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function emptyCategories() {
|
|
296
|
+
return { necessary: true, functional: false, analytics: false, marketing: false };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// Cookie helpers
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
function readCookie(name) {
|
|
303
|
+
try {
|
|
304
|
+
var raw = doc && typeof doc.cookie === 'string' ? doc.cookie : '';
|
|
305
|
+
var parts = raw.split(';');
|
|
306
|
+
for (var i = 0; i < parts.length; i++) {
|
|
307
|
+
var p = parts[i].trim();
|
|
308
|
+
var eq = p.indexOf('=');
|
|
309
|
+
if (eq > -1 && p.slice(0, eq) === name) { return p.slice(eq + 1); }
|
|
310
|
+
}
|
|
311
|
+
} catch (e) { /* noop */ }
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function writeCookie(name, value, days) {
|
|
316
|
+
try {
|
|
317
|
+
var exp = new Date(Date.now() + days * 864e5).toUTCString();
|
|
318
|
+
doc.cookie = name + '=' + value + '; expires=' + exp + '; path=/; SameSite=Lax';
|
|
319
|
+
} catch (e) { /* noop */ }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function hostname() {
|
|
323
|
+
try { return (global.location && global.location.hostname) || ''; } catch (e) { return ''; }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Delete on no-domain, host and .host variants — localhost rejects domain=.
|
|
327
|
+
function deleteCookie(name) {
|
|
328
|
+
var h = hostname();
|
|
329
|
+
var variants = ['', h ? '; domain=' + h : '', h ? '; domain=.' + h : ''];
|
|
330
|
+
// Also try the registrable parent (a.b.example.com -> .example.com).
|
|
331
|
+
var labels = h ? h.split('.') : [];
|
|
332
|
+
if (labels.length > 2) { variants.push('; domain=.' + labels.slice(-2).join('.')); }
|
|
333
|
+
for (var i = 0; i < variants.length; i++) {
|
|
334
|
+
try {
|
|
335
|
+
doc.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/' + variants[i];
|
|
336
|
+
} catch (e) { /* noop */ }
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function listCookieNames() {
|
|
341
|
+
var names = [];
|
|
342
|
+
try {
|
|
343
|
+
var raw = doc && typeof doc.cookie === 'string' ? doc.cookie : '';
|
|
344
|
+
raw.split(';').forEach(function (p) {
|
|
345
|
+
var t = p.trim();
|
|
346
|
+
if (!t) { return; }
|
|
347
|
+
var eq = t.indexOf('=');
|
|
348
|
+
names.push(eq > -1 ? t.slice(0, eq) : t);
|
|
349
|
+
});
|
|
350
|
+
} catch (e) { /* noop */ }
|
|
351
|
+
return names;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Purges cookies owned by the given categories. `full` also sweeps the
|
|
355
|
+
// catch-all demo_* prefix (used by rejectAll/withdraw).
|
|
356
|
+
function purgeCookies(cats, full) {
|
|
357
|
+
var exact = [];
|
|
358
|
+
var prefixes = full ? FULL_PURGE_PREFIX.slice() : [];
|
|
359
|
+
cats.forEach(function (c) {
|
|
360
|
+
exact = exact.concat(COOKIE_EXACT[c] || []);
|
|
361
|
+
prefixes = prefixes.concat(COOKIE_PREFIX[c] || []);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
listCookieNames().forEach(function (n) {
|
|
365
|
+
if (n === STORAGE_KEY) { return; }
|
|
366
|
+
var hit = exact.indexOf(n) > -1;
|
|
367
|
+
if (!hit) {
|
|
368
|
+
for (var i = 0; i < prefixes.length; i++) {
|
|
369
|
+
if (n.indexOf(prefixes[i]) === 0) { hit = true; break; }
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (hit) { deleteCookie(n); }
|
|
373
|
+
});
|
|
374
|
+
// Masks may name cookies invisible to document.cookie: try them anyway.
|
|
375
|
+
exact.forEach(deleteCookie);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Clears every known tracker cookie (reject / withdraw).
|
|
379
|
+
function purgeKnownCookies() {
|
|
380
|
+
purgeCookies(OPT_IN, true);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// Storage
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
function lsGet(k) {
|
|
387
|
+
try { return global.localStorage ? global.localStorage.getItem(k) : null; } catch (e) { return null; }
|
|
388
|
+
}
|
|
389
|
+
function lsSet(k, v) {
|
|
390
|
+
try { if (global.localStorage) { global.localStorage.setItem(k, v); } } catch (e) { /* noop */ }
|
|
391
|
+
}
|
|
392
|
+
function lsDel(k) {
|
|
393
|
+
try { if (global.localStorage) { global.localStorage.removeItem(k); } } catch (e) { /* noop */ }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function saveRecord(rec) {
|
|
397
|
+
var payload = b64encode(JSON.stringify(rec));
|
|
398
|
+
if (!payload) { return; }
|
|
399
|
+
writeCookie(STORAGE_KEY, payload, ttlDays());
|
|
400
|
+
lsSet(STORAGE_KEY, payload);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function clearRecord() {
|
|
404
|
+
deleteCookie(STORAGE_KEY);
|
|
405
|
+
lsDel(STORAGE_KEY);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function ttlDays() {
|
|
409
|
+
var d = config && Number(config.consentTtlDays);
|
|
410
|
+
return (isFinite(d) && d > 0) ? d : 365;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function parseRecord(payload) {
|
|
414
|
+
if (!payload) { return null; }
|
|
415
|
+
var json = b64decode(payload);
|
|
416
|
+
if (!json) { return null; }
|
|
417
|
+
var rec;
|
|
418
|
+
try { rec = JSON.parse(json); } catch (e) { return null; }
|
|
419
|
+
if (!rec || typeof rec !== 'object' || !rec.categories) { return null; }
|
|
420
|
+
return rec;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Reads cookie first, falls back to localStorage. Returns null when invalid,
|
|
424
|
+
// stale (policyVersion mismatch) or expired.
|
|
425
|
+
function loadRecord() {
|
|
426
|
+
var rec = parseRecord(readCookie(STORAGE_KEY)) || parseRecord(lsGet(STORAGE_KEY));
|
|
427
|
+
if (!rec) { return null; }
|
|
428
|
+
if (String(rec.policyVersion) !== String(config.policyVersion)) {
|
|
429
|
+
clearRecord();
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
var t = Date.parse(rec.ts);
|
|
433
|
+
if (!isFinite(t) || (Date.now() - t) > ttlDays() * 864e5) {
|
|
434
|
+
clearRecord();
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
var cats = emptyCategories();
|
|
438
|
+
CATEGORIES.forEach(function (c) {
|
|
439
|
+
if (c === 'necessary') { return; }
|
|
440
|
+
cats[c] = rec.categories[c] === true;
|
|
441
|
+
});
|
|
442
|
+
return {
|
|
443
|
+
id: rec.id || uuid(),
|
|
444
|
+
ts: rec.ts,
|
|
445
|
+
policyVersion: String(rec.policyVersion),
|
|
446
|
+
categories: cats,
|
|
447
|
+
method: rec.method || 'custom'
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
// State
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
var config = clone(DEFAULT_CONFIG);
|
|
455
|
+
var initialized = false;
|
|
456
|
+
var state = {
|
|
457
|
+
decided: false,
|
|
458
|
+
id: null,
|
|
459
|
+
ts: null,
|
|
460
|
+
policyVersion: String(config.policyVersion),
|
|
461
|
+
categories: emptyCategories(),
|
|
462
|
+
method: null
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
function publicState() {
|
|
466
|
+
return {
|
|
467
|
+
decided: state.decided,
|
|
468
|
+
id: state.id,
|
|
469
|
+
ts: state.ts,
|
|
470
|
+
policyVersion: state.policyVersion,
|
|
471
|
+
categories: {
|
|
472
|
+
necessary: true,
|
|
473
|
+
functional: !!state.categories.functional,
|
|
474
|
+
analytics: !!state.categories.analytics,
|
|
475
|
+
marketing: !!state.categories.marketing
|
|
476
|
+
},
|
|
477
|
+
method: state.method
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function dispatch(name, detail) {
|
|
482
|
+
try {
|
|
483
|
+
if (!doc || typeof doc.dispatchEvent !== 'function') { return; }
|
|
484
|
+
var ev;
|
|
485
|
+
if (typeof global.CustomEvent === 'function') {
|
|
486
|
+
ev = new global.CustomEvent(name, { detail: detail, bubbles: false, cancelable: false });
|
|
487
|
+
} else if (doc.createEvent) {
|
|
488
|
+
ev = doc.createEvent('CustomEvent');
|
|
489
|
+
ev.initCustomEvent(name, false, false, detail);
|
|
490
|
+
} else {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
doc.dispatchEvent(ev);
|
|
494
|
+
} catch (e) { /* UI layer absent or event system unavailable */ }
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
// Google Consent Mode v2
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
function dataLayerPush(args) {
|
|
501
|
+
try {
|
|
502
|
+
global.dataLayer = global.dataLayer || [];
|
|
503
|
+
global.dataLayer.push(args);
|
|
504
|
+
} catch (e) { /* noop */ }
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Internal gtag shim; never clobbers a host-page window.gtag.
|
|
508
|
+
function ckGtag() { dataLayerPush(arguments); }
|
|
509
|
+
|
|
510
|
+
// Pushed exactly once, at parse time — before init() reveals
|
|
511
|
+
// integrations.gcm, and before any tag can load. Consent Mode expects a
|
|
512
|
+
// single 'default'; every later change goes through gcmUpdate().
|
|
513
|
+
function gcmDefault() {
|
|
514
|
+
var signals = gcmSignals(emptyCategories());
|
|
515
|
+
signals.wait_for_update = 500;
|
|
516
|
+
ckGtag('consent', 'default', signals);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Maps consent categories onto GCM v2 signals. Shared by 'default' and
|
|
520
|
+
// 'update' pushes so the mapping lives in exactly one place.
|
|
521
|
+
function gcmSignals(c) {
|
|
522
|
+
var g = function (b) { return b ? 'granted' : 'denied'; };
|
|
523
|
+
return {
|
|
524
|
+
ad_storage: g(c.marketing),
|
|
525
|
+
analytics_storage: g(c.analytics),
|
|
526
|
+
ad_user_data: g(c.marketing),
|
|
527
|
+
ad_personalization: g(c.marketing),
|
|
528
|
+
functionality_storage: g(c.functional),
|
|
529
|
+
personalization_storage: g(c.functional),
|
|
530
|
+
security_storage: 'granted'
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Pushes the current state to both integrations. Withdraw relies on this too:
|
|
535
|
+
// it resets state.categories to all-false first, so every signal goes denied.
|
|
536
|
+
//
|
|
537
|
+
// The two gates are independent by design: Consent Mode signals answer to
|
|
538
|
+
// integrations.gcm, the ck_consent_update event answers to gtmDataLayer.
|
|
539
|
+
// A {gcm:false, gtmDataLayer:true} config must still populate the GTM
|
|
540
|
+
// ck_consent.* variables.
|
|
541
|
+
function gcmUpdate() {
|
|
542
|
+
try {
|
|
543
|
+
var integrations = config.integrations || {};
|
|
544
|
+
var c = state.categories;
|
|
545
|
+
|
|
546
|
+
if (integrations.gcm !== false) {
|
|
547
|
+
ckGtag('consent', 'update', gcmSignals(c));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (integrations.gtmDataLayer !== false) {
|
|
551
|
+
dataLayerPush({
|
|
552
|
+
event: 'ck_consent_update',
|
|
553
|
+
ck_consent: {
|
|
554
|
+
necessary: true,
|
|
555
|
+
functional: !!c.functional,
|
|
556
|
+
analytics: !!c.analytics,
|
|
557
|
+
marketing: !!c.marketing
|
|
558
|
+
},
|
|
559
|
+
ck_method: state.method
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
} catch (e) { /* noop */ }
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Categories that already emitted a ck_consent_<cat> event on this page load.
|
|
566
|
+
// Never cleared: GTM cannot unload a tag that has already fired, so a second
|
|
567
|
+
// event for the same category would double-count. A fresh page load is the
|
|
568
|
+
// only reset boundary — withdraw/reject deliberately leave this set intact.
|
|
569
|
+
var firedCatEvents = {};
|
|
570
|
+
|
|
571
|
+
// Pushes { event: 'ck_consent_<category>' } for each granted opt-in category,
|
|
572
|
+
// once per page load. Gated on gtmDataLayer only: these drive GTM triggers and
|
|
573
|
+
// must fire even when Consent Mode is switched off.
|
|
574
|
+
function pushCategoryEvents() {
|
|
575
|
+
try {
|
|
576
|
+
if (config.integrations && config.integrations.gtmDataLayer === false) { return; }
|
|
577
|
+
OPT_IN.forEach(function (cat) {
|
|
578
|
+
if (!state.categories[cat] || firedCatEvents[cat]) { return; }
|
|
579
|
+
firedCatEvents[cat] = true;
|
|
580
|
+
dataLayerPush({ event: 'ck_consent_' + cat });
|
|
581
|
+
});
|
|
582
|
+
} catch (e) { /* noop */ }
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
// Blocking engine — URL classification
|
|
587
|
+
// ---------------------------------------------------------------------------
|
|
588
|
+
function categoryForUrl(src) {
|
|
589
|
+
if (!src || typeof src !== 'string') { return null; }
|
|
590
|
+
var s = src;
|
|
591
|
+
var host = '';
|
|
592
|
+
try {
|
|
593
|
+
var base = (global.location && global.location.href) || 'http://localhost/';
|
|
594
|
+
var u = new URL(s, base);
|
|
595
|
+
host = (u.hostname || '').toLowerCase();
|
|
596
|
+
s = u.href;
|
|
597
|
+
} catch (e) {
|
|
598
|
+
var m = /^(?:[a-z]+:)?\/\/([^/?#]+)/i.exec(src);
|
|
599
|
+
host = m ? m[1].toLowerCase().replace(/:\d+$/, '') : '';
|
|
600
|
+
}
|
|
601
|
+
if (host) {
|
|
602
|
+
var keys = Object.keys(HOST_DB);
|
|
603
|
+
for (var i = 0; i < keys.length; i++) {
|
|
604
|
+
var e2 = keys[i];
|
|
605
|
+
if (host === e2 || host.length > e2.length && host.slice(-(e2.length + 1)) === '.' + e2) {
|
|
606
|
+
return HOST_DB[e2];
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
var low = String(s).toLowerCase();
|
|
611
|
+
var pkeys = Object.keys(PATH_DB);
|
|
612
|
+
for (var j = 0; j < pkeys.length; j++) {
|
|
613
|
+
if (low.indexOf(pkeys[j]) > -1) { return PATH_DB[pkeys[j]]; }
|
|
614
|
+
}
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function allowed(cat) {
|
|
619
|
+
if (cat === 'necessary' || !cat) { return true; }
|
|
620
|
+
if (CATEGORIES.indexOf(cat) === -1) { return true; }
|
|
621
|
+
return state.categories[cat] === true;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// True when the URL is a known tracker whose category is not (yet) granted.
|
|
625
|
+
function shouldBlock(src) {
|
|
626
|
+
if (bypass) { return false; }
|
|
627
|
+
var cat = categoryForUrl(src);
|
|
628
|
+
if (!cat) { return false; }
|
|
629
|
+
return !allowed(cat);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function markBlocked(el, src, cat) {
|
|
633
|
+
try {
|
|
634
|
+
if (nativeSetAttribute) {
|
|
635
|
+
nativeSetAttribute.call(el, 'data-ck-blocked', '1');
|
|
636
|
+
nativeSetAttribute.call(el, 'data-ck-src', src);
|
|
637
|
+
nativeSetAttribute.call(el, 'data-ck', cat || 'marketing');
|
|
638
|
+
}
|
|
639
|
+
// Remember the real type (e.g. "module") so revival can restore it.
|
|
640
|
+
var origType = el.getAttribute ? el.getAttribute('type') : null;
|
|
641
|
+
if (origType && origType !== 'text/plain' && nativeSetAttribute) {
|
|
642
|
+
nativeSetAttribute.call(el, 'data-ck-type', origType);
|
|
643
|
+
}
|
|
644
|
+
if (el.type !== 'text/plain') {
|
|
645
|
+
try { el.type = 'text/plain'; } catch (e2) { /* noop */ }
|
|
646
|
+
}
|
|
647
|
+
} catch (e) { /* noop */ }
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ---------------------------------------------------------------------------
|
|
651
|
+
// Blocking engine — patches (installed at parse time)
|
|
652
|
+
// ---------------------------------------------------------------------------
|
|
653
|
+
function installPatches() {
|
|
654
|
+
// 1. HTMLScriptElement.prototype.src setter.
|
|
655
|
+
try {
|
|
656
|
+
if (nativeScriptSrcDesc && nativeScriptSrcDesc.set && nativeScriptSrcDesc.configurable !== false) {
|
|
657
|
+
Object.defineProperty(global.HTMLScriptElement.prototype, 'src', {
|
|
658
|
+
configurable: true,
|
|
659
|
+
enumerable: nativeScriptSrcDesc.enumerable,
|
|
660
|
+
get: function () {
|
|
661
|
+
try { return nativeScriptSrcDesc.get.call(this); } catch (e) { return ''; }
|
|
662
|
+
},
|
|
663
|
+
set: function (v) {
|
|
664
|
+
if (shouldBlock(v)) {
|
|
665
|
+
markBlocked(this, String(v), categoryForUrl(v));
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
try { nativeScriptSrcDesc.set.call(this, v); } catch (e) { /* noop */ }
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
} catch (e) { /* noop */ }
|
|
673
|
+
|
|
674
|
+
// 2. Element.prototype.setAttribute — covers setAttribute('src', ...).
|
|
675
|
+
try {
|
|
676
|
+
if (nativeSetAttribute && global.Element && global.Element.prototype) {
|
|
677
|
+
global.Element.prototype.setAttribute = function (name, value) {
|
|
678
|
+
try {
|
|
679
|
+
if (!bypass && typeof name === 'string' && name.toLowerCase() === 'src' &&
|
|
680
|
+
this && this.tagName && String(this.tagName).toUpperCase() === 'SCRIPT' &&
|
|
681
|
+
shouldBlock(value)) {
|
|
682
|
+
markBlocked(this, String(value), categoryForUrl(value));
|
|
683
|
+
return undefined;
|
|
684
|
+
}
|
|
685
|
+
} catch (e) { /* fall through to native */ }
|
|
686
|
+
return nativeSetAttribute.apply(this, arguments);
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
} catch (e) { /* noop */ }
|
|
690
|
+
|
|
691
|
+
// 3. document.createElement — pre-neutralize dynamically created scripts.
|
|
692
|
+
try {
|
|
693
|
+
if (nativeCreateElement && doc) {
|
|
694
|
+
doc.createElement = function (tag) {
|
|
695
|
+
var el = nativeCreateElement.apply(null, arguments);
|
|
696
|
+
try {
|
|
697
|
+
if (!bypass && typeof tag === 'string' && tag.toLowerCase() === 'script' &&
|
|
698
|
+
nativeScriptSrcDesc && nativeScriptSrcDesc.set) {
|
|
699
|
+
// Own-property guard so the element is covered even if the
|
|
700
|
+
// prototype patch was reverted by another library.
|
|
701
|
+
Object.defineProperty(el, 'src', {
|
|
702
|
+
configurable: true,
|
|
703
|
+
enumerable: false,
|
|
704
|
+
get: function () {
|
|
705
|
+
try { return nativeScriptSrcDesc.get.call(this); } catch (e) { return ''; }
|
|
706
|
+
},
|
|
707
|
+
set: function (v) {
|
|
708
|
+
if (shouldBlock(v)) {
|
|
709
|
+
markBlocked(this, String(v), categoryForUrl(v));
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
try { nativeScriptSrcDesc.set.call(this, v); } catch (e) { /* noop */ }
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
} catch (e) { /* noop */ }
|
|
717
|
+
return el;
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
} catch (e) { /* noop */ }
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// 4. MutationObserver on documentElement — catches markup-inserted scripts.
|
|
724
|
+
var observer = null;
|
|
725
|
+
function installObserver() {
|
|
726
|
+
try {
|
|
727
|
+
if (typeof global.MutationObserver !== 'function' || !doc || !doc.documentElement) { return; }
|
|
728
|
+
observer = new global.MutationObserver(function (records) {
|
|
729
|
+
if (bypass) { return; }
|
|
730
|
+
try {
|
|
731
|
+
for (var i = 0; i < records.length; i++) {
|
|
732
|
+
var added = records[i].addedNodes || [];
|
|
733
|
+
for (var j = 0; j < added.length; j++) { inspectNode(added[j]); }
|
|
734
|
+
}
|
|
735
|
+
} catch (e) { /* noop */ }
|
|
736
|
+
});
|
|
737
|
+
observer.observe(doc.documentElement, { childList: true, subtree: true });
|
|
738
|
+
} catch (e) { /* noop */ }
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function inspectNode(node) {
|
|
742
|
+
try {
|
|
743
|
+
if (!node || node.nodeType !== 1) { return; }
|
|
744
|
+
var tag = node.tagName ? String(node.tagName).toUpperCase() : '';
|
|
745
|
+
if (tag === 'SCRIPT') { inspectScript(node); }
|
|
746
|
+
if (typeof node.querySelectorAll === 'function') {
|
|
747
|
+
var kids = node.querySelectorAll('script');
|
|
748
|
+
for (var i = 0; i < kids.length; i++) { inspectScript(kids[i]); }
|
|
749
|
+
}
|
|
750
|
+
} catch (e) { /* noop */ }
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function inspectScript(el) {
|
|
754
|
+
try {
|
|
755
|
+
if (!el || el.getAttribute === undefined) { return; }
|
|
756
|
+
if (el.getAttribute('data-ck-blocked')) { return; }
|
|
757
|
+
var type = el.getAttribute('type');
|
|
758
|
+
if (type === 'text/plain' && el.getAttribute('data-ck')) { return; } // manual markup
|
|
759
|
+
var src = el.getAttribute('src');
|
|
760
|
+
if (!src) { return; }
|
|
761
|
+
if (shouldBlock(src)) {
|
|
762
|
+
var cat = categoryForUrl(src);
|
|
763
|
+
markBlocked(el, src, cat);
|
|
764
|
+
// Clear the attribute. Note: if the element was already connected the
|
|
765
|
+
// request may already be in flight — the createElement/src patches are
|
|
766
|
+
// the reliable path; this is a late net for markup-inserted scripts.
|
|
767
|
+
try { if (nativeSetAttribute) { nativeSetAttribute.call(el, 'src', ''); } } catch (e2) { /* noop */ }
|
|
768
|
+
try { if (nativeSetAttribute) { nativeSetAttribute.call(el, 'data-ck-src', src); } } catch (e3) { /* noop */ }
|
|
769
|
+
}
|
|
770
|
+
} catch (e) { /* noop */ }
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// ---------------------------------------------------------------------------
|
|
774
|
+
// Blocking engine — unblocking after consent
|
|
775
|
+
// ---------------------------------------------------------------------------
|
|
776
|
+
var SKIP_ATTRS = { type: 1, src: 1, 'data-src': 1, 'data-ck': 1, 'data-ck-src': 1, 'data-ck-blocked': 1 };
|
|
777
|
+
|
|
778
|
+
function reviveScript(old) {
|
|
779
|
+
if (!nativeCreateElement || !old || !old.parentNode) { return; }
|
|
780
|
+
var prev = bypass;
|
|
781
|
+
bypass = true;
|
|
782
|
+
try {
|
|
783
|
+
var fresh = nativeCreateElement('script');
|
|
784
|
+
// Copy attributes, minus our own bookkeeping.
|
|
785
|
+
var attrs = old.attributes || [];
|
|
786
|
+
for (var i = 0; i < attrs.length; i++) {
|
|
787
|
+
var a = attrs[i];
|
|
788
|
+
if (SKIP_ATTRS[String(a.name).toLowerCase()]) { continue; }
|
|
789
|
+
try { nativeSetAttribute.call(fresh, a.name, a.value); } catch (e) { /* noop */ }
|
|
790
|
+
}
|
|
791
|
+
var realType = old.getAttribute('data-ck-type');
|
|
792
|
+
if (realType) {
|
|
793
|
+
try { nativeSetAttribute.call(fresh, 'type', realType); } catch (e) { /* noop */ }
|
|
794
|
+
}
|
|
795
|
+
var src = old.getAttribute('data-src') || old.getAttribute('data-ck-src') || '';
|
|
796
|
+
if (src) {
|
|
797
|
+
// Preserve document order for external scripts.
|
|
798
|
+
try { fresh.async = false; } catch (e) { /* noop */ }
|
|
799
|
+
if (nativeScriptSrcDesc && nativeScriptSrcDesc.set) {
|
|
800
|
+
nativeScriptSrcDesc.set.call(fresh, src);
|
|
801
|
+
} else {
|
|
802
|
+
nativeSetAttribute.call(fresh, 'src', src);
|
|
803
|
+
}
|
|
804
|
+
} else {
|
|
805
|
+
try { fresh.text = old.text || old.textContent || ''; } catch (e) { /* noop */ }
|
|
806
|
+
}
|
|
807
|
+
try { nativeSetAttribute.call(fresh, 'data-ck-restored', '1'); } catch (e) { /* noop */ }
|
|
808
|
+
old.parentNode.insertBefore(fresh, old);
|
|
809
|
+
try { old.parentNode.removeChild(old); } catch (e) { /* noop */ }
|
|
810
|
+
} catch (e) {
|
|
811
|
+
/* noop */
|
|
812
|
+
} finally {
|
|
813
|
+
bypass = prev;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function qsa(sel) {
|
|
818
|
+
try {
|
|
819
|
+
if (!doc || typeof doc.querySelectorAll !== 'function') { return []; }
|
|
820
|
+
return Array.prototype.slice.call(doc.querySelectorAll(sel));
|
|
821
|
+
} catch (e) { return []; }
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Applies the current consent state to the DOM: revives newly-allowed scripts
|
|
825
|
+
// and iframes, in document order.
|
|
826
|
+
function applyConsentToDom() {
|
|
827
|
+
// Manual markup + auto-blocked scripts, one ordered pass.
|
|
828
|
+
var scripts = qsa('script[type="text/plain"][data-ck], script[data-ck-blocked]');
|
|
829
|
+
scripts.forEach(function (el) {
|
|
830
|
+
try {
|
|
831
|
+
if (el.getAttribute('data-ck-restored')) { return; }
|
|
832
|
+
var cat = el.getAttribute('data-ck');
|
|
833
|
+
if (!cat) { cat = categoryForUrl(el.getAttribute('data-src') || el.getAttribute('data-ck-src') || ''); }
|
|
834
|
+
if (!allowed(cat)) { return; }
|
|
835
|
+
reviveScript(el);
|
|
836
|
+
} catch (e) { /* noop */ }
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
// Iframes: set src from data-src once the category is granted.
|
|
840
|
+
qsa('iframe[data-ck][data-src]').forEach(function (el) {
|
|
841
|
+
try {
|
|
842
|
+
var cat = el.getAttribute('data-ck');
|
|
843
|
+
if (!allowed(cat)) { return; }
|
|
844
|
+
if (el.getAttribute('src')) { return; }
|
|
845
|
+
var src = el.getAttribute('data-src');
|
|
846
|
+
if (!src) { return; }
|
|
847
|
+
var prev = bypass;
|
|
848
|
+
bypass = true;
|
|
849
|
+
try { nativeSetAttribute.call(el, 'src', src); } finally { bypass = prev; }
|
|
850
|
+
} catch (e) { /* noop */ }
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Initial sweep for scripts already parsed before the observer attached.
|
|
855
|
+
function initialScan() {
|
|
856
|
+
qsa('script[src]').forEach(inspectScript);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// ---------------------------------------------------------------------------
|
|
860
|
+
// Decisions
|
|
861
|
+
// ---------------------------------------------------------------------------
|
|
862
|
+
function commit(categories, method) {
|
|
863
|
+
var wasDecided = state.decided;
|
|
864
|
+
state.categories = {
|
|
865
|
+
necessary: true,
|
|
866
|
+
functional: categories.functional === true,
|
|
867
|
+
analytics: categories.analytics === true,
|
|
868
|
+
marketing: categories.marketing === true
|
|
869
|
+
};
|
|
870
|
+
state.decided = true;
|
|
871
|
+
state.method = method;
|
|
872
|
+
state.id = state.id || uuid();
|
|
873
|
+
state.ts = new Date().toISOString();
|
|
874
|
+
state.policyVersion = String(config.policyVersion);
|
|
875
|
+
|
|
876
|
+
saveRecord({
|
|
877
|
+
id: state.id,
|
|
878
|
+
ts: state.ts,
|
|
879
|
+
policyVersion: state.policyVersion,
|
|
880
|
+
categories: {
|
|
881
|
+
necessary: true,
|
|
882
|
+
functional: state.categories.functional,
|
|
883
|
+
analytics: state.categories.analytics,
|
|
884
|
+
marketing: state.categories.marketing
|
|
885
|
+
},
|
|
886
|
+
method: state.method
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
gcmUpdate();
|
|
890
|
+
pushCategoryEvents();
|
|
891
|
+
|
|
892
|
+
// Clear cookies of the categories that stay denied; a full reject sweeps
|
|
893
|
+
// the catch-all demo_* prefix too.
|
|
894
|
+
var denied = OPT_IN.filter(function (c) { return !state.categories[c]; });
|
|
895
|
+
if (denied.length) { purgeCookies(denied, denied.length === OPT_IN.length); }
|
|
896
|
+
|
|
897
|
+
applyConsentToDom();
|
|
898
|
+
|
|
899
|
+
if (!wasDecided) { dispatch('ck:consent', { state: publicState() }); }
|
|
900
|
+
dispatch('ck:change', { state: publicState() });
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Only categories enabled in config can be granted.
|
|
904
|
+
function filterByConfig(cats) {
|
|
905
|
+
var out = { functional: false, analytics: false, marketing: false };
|
|
906
|
+
OPT_IN.forEach(function (c) {
|
|
907
|
+
var cfg = config.categories && config.categories[c];
|
|
908
|
+
var enabled = !cfg || cfg.enabled !== false;
|
|
909
|
+
out[c] = enabled && cats[c] === true;
|
|
910
|
+
});
|
|
911
|
+
return out;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function accept(arg) {
|
|
915
|
+
try {
|
|
916
|
+
var cats, method;
|
|
917
|
+
if (arg === 'all' || arg === undefined || arg === null) {
|
|
918
|
+
cats = { functional: true, analytics: true, marketing: true };
|
|
919
|
+
method = 'accept_all';
|
|
920
|
+
} else if (isPlainObject(arg)) {
|
|
921
|
+
cats = { functional: arg.functional === true, analytics: arg.analytics === true, marketing: arg.marketing === true };
|
|
922
|
+
method = 'custom';
|
|
923
|
+
} else {
|
|
924
|
+
cats = { functional: true, analytics: true, marketing: true };
|
|
925
|
+
method = 'accept_all';
|
|
926
|
+
}
|
|
927
|
+
commit(filterByConfig(cats), method);
|
|
928
|
+
} catch (e) { /* noop */ }
|
|
929
|
+
return publicState();
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function rejectAll() {
|
|
933
|
+
try {
|
|
934
|
+
commit({ functional: false, analytics: false, marketing: false }, 'reject_all');
|
|
935
|
+
} catch (e) { /* noop */ }
|
|
936
|
+
return publicState();
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function withdraw() {
|
|
940
|
+
try {
|
|
941
|
+
state.decided = false;
|
|
942
|
+
state.id = null;
|
|
943
|
+
state.ts = null;
|
|
944
|
+
state.method = null;
|
|
945
|
+
state.categories = emptyCategories();
|
|
946
|
+
state.policyVersion = String(config.policyVersion);
|
|
947
|
+
|
|
948
|
+
clearRecord();
|
|
949
|
+
purgeKnownCookies();
|
|
950
|
+
// 'update' with everything denied, not a second 'default': Consent Mode
|
|
951
|
+
// accepts only one default, set before tags load. State was reset above,
|
|
952
|
+
// so gcmUpdate() emits all-denied and honours integrations.gcm.
|
|
953
|
+
gcmUpdate();
|
|
954
|
+
// Loaded scripts are not unloaded; a fresh page load starts clean.
|
|
955
|
+
dispatch('ck:change', { state: publicState() });
|
|
956
|
+
} catch (e) { /* noop */ }
|
|
957
|
+
return publicState();
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// ---------------------------------------------------------------------------
|
|
961
|
+
// Public API
|
|
962
|
+
// ---------------------------------------------------------------------------
|
|
963
|
+
var ConsentKit = {
|
|
964
|
+
version: '0.3.2',
|
|
965
|
+
config: config,
|
|
966
|
+
|
|
967
|
+
init: function (userConfig) {
|
|
968
|
+
try {
|
|
969
|
+
config = mergeConfig(config, userConfig);
|
|
970
|
+
ConsentKit.config = config;
|
|
971
|
+
|
|
972
|
+
if (initialized) {
|
|
973
|
+
// Idempotent: merge config, no re-restore, no duplicate ck:init.
|
|
974
|
+
return publicState();
|
|
975
|
+
}
|
|
976
|
+
initialized = true;
|
|
977
|
+
|
|
978
|
+
var rec = loadRecord();
|
|
979
|
+
if (rec) {
|
|
980
|
+
state.decided = true;
|
|
981
|
+
state.id = rec.id;
|
|
982
|
+
state.ts = rec.ts;
|
|
983
|
+
state.policyVersion = rec.policyVersion;
|
|
984
|
+
state.categories = rec.categories;
|
|
985
|
+
state.method = rec.method;
|
|
986
|
+
gcmUpdate();
|
|
987
|
+
// Return visit: GTM triggers must fire for the restored categories.
|
|
988
|
+
pushCategoryEvents();
|
|
989
|
+
} else {
|
|
990
|
+
state.policyVersion = String(config.policyVersion);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
initialScan();
|
|
994
|
+
applyConsentToDom();
|
|
995
|
+
|
|
996
|
+
dispatch('ck:init', { state: publicState(), config: config });
|
|
997
|
+
} catch (e) { /* core must never throw */ }
|
|
998
|
+
return publicState();
|
|
999
|
+
},
|
|
1000
|
+
|
|
1001
|
+
allowed: function (cat) {
|
|
1002
|
+
try { return allowed(cat); } catch (e) { return false; }
|
|
1003
|
+
},
|
|
1004
|
+
|
|
1005
|
+
getState: function () {
|
|
1006
|
+
try { return publicState(); } catch (e) {
|
|
1007
|
+
return { decided: false, id: null, ts: null, policyVersion: '1', categories: emptyCategories(), method: null };
|
|
1008
|
+
}
|
|
1009
|
+
},
|
|
1010
|
+
|
|
1011
|
+
accept: accept,
|
|
1012
|
+
rejectAll: rejectAll,
|
|
1013
|
+
withdraw: withdraw,
|
|
1014
|
+
|
|
1015
|
+
show: function () { dispatch('ck:ui:open-preferences', { state: publicState(), config: config }); },
|
|
1016
|
+
hide: function () { dispatch('ck:ui:close', { state: publicState() }); },
|
|
1017
|
+
|
|
1018
|
+
// Introspection helpers for the demo status panel (read-only).
|
|
1019
|
+
_categoryForUrl: categoryForUrl,
|
|
1020
|
+
_categories: CATEGORIES.slice()
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
// ---------------------------------------------------------------------------
|
|
1024
|
+
// Bootstrap — runs at parse time, before init().
|
|
1025
|
+
// ---------------------------------------------------------------------------
|
|
1026
|
+
// Each step is isolated: a failure in one (or a double-load re-patching an
|
|
1027
|
+
// already-patched document) must never stop the API from being published.
|
|
1028
|
+
try { gcmDefault(); } catch (e) { /* noop */ }
|
|
1029
|
+
try { installPatches(); } catch (e) { /* noop */ }
|
|
1030
|
+
try { installObserver(); } catch (e) { /* noop */ }
|
|
1031
|
+
try { initialScan(); } catch (e) { /* noop */ }
|
|
1032
|
+
|
|
1033
|
+
try {
|
|
1034
|
+
if (doc && typeof doc.addEventListener === 'function') {
|
|
1035
|
+
doc.addEventListener('DOMContentLoaded', function () {
|
|
1036
|
+
try { initialScan(); applyConsentToDom(); } catch (e) { /* noop */ }
|
|
1037
|
+
}, false);
|
|
1038
|
+
}
|
|
1039
|
+
} catch (e) { /* noop */ }
|
|
1040
|
+
|
|
1041
|
+
global.ConsentKit = ConsentKit;
|
|
1042
|
+
if (typeof module === 'object' && module && module.exports) { module.exports = ConsentKit; }
|
|
1043
|
+
void noop;
|
|
1044
|
+
})(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this));
|