@emplugins/emprivacy 3.0.0 → 3.2.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.
- package/README.md +11 -5
- package/dist/index.d.mts +64 -4
- package/dist/index.mjs +692 -126
- package/docs/GETTING_STARTED.md +5 -3
- package/docs/PLUGIN_SETTINGS.md +52 -13
- package/docs/RELEASES.md +8 -5
- package/docs/RELEASE_NOTES.md +26 -1
- package/docs/TESTING.md +8 -6
- package/package.json +1 -1
- package/src/astro/embed-resolve.ts +4 -16
package/dist/index.mjs
CHANGED
|
@@ -20,7 +20,8 @@ const CHROME = {
|
|
|
20
20
|
embedBlocked: "This embed is blocked until you allow the matching cookie category.",
|
|
21
21
|
embedNeedConsent: "To load this embed, allow the matching category in cookie settings.",
|
|
22
22
|
vendorsHeading: "What this site uses",
|
|
23
|
-
whatWeUse: "What we use"
|
|
23
|
+
whatWeUse: "What we use",
|
|
24
|
+
gpcNote: "Your browser sent Global Privacy Control, so marketing stays off."
|
|
24
25
|
},
|
|
25
26
|
de: {
|
|
26
27
|
acceptAll: "Alle akzeptieren",
|
|
@@ -40,7 +41,8 @@ const CHROME = {
|
|
|
40
41
|
embedBlocked: "Diese Einbettung ist gesperrt, bis Sie die passende Kategorie erlauben.",
|
|
41
42
|
embedNeedConsent: "Um diese Einbettung zu laden, erlauben Sie die passende Kategorie in den Cookie-Einstellungen.",
|
|
42
43
|
vendorsHeading: "Was diese Website verwendet",
|
|
43
|
-
whatWeUse: "Was wir verwenden"
|
|
44
|
+
whatWeUse: "Was wir verwenden",
|
|
45
|
+
gpcNote: "Ihr Browser sendet Global Privacy Control, daher bleibt Marketing deaktiviert."
|
|
44
46
|
},
|
|
45
47
|
fr: {
|
|
46
48
|
acceptAll: "Tout accepter",
|
|
@@ -60,7 +62,8 @@ const CHROME = {
|
|
|
60
62
|
embedBlocked: "Ce contenu est bloqué tant que vous n’avez pas autorisé la catégorie correspondante.",
|
|
61
63
|
embedNeedConsent: "Pour charger ce contenu, autorisez la catégorie correspondante dans les paramètres des cookies.",
|
|
62
64
|
vendorsHeading: "Ce que ce site utilise",
|
|
63
|
-
whatWeUse: "Ce que nous utilisons"
|
|
65
|
+
whatWeUse: "Ce que nous utilisons",
|
|
66
|
+
gpcNote: "Votre navigateur envoie Global Privacy Control, donc le marketing reste désactivé."
|
|
64
67
|
},
|
|
65
68
|
es: {
|
|
66
69
|
acceptAll: "Aceptar todo",
|
|
@@ -80,7 +83,8 @@ const CHROME = {
|
|
|
80
83
|
embedBlocked: "Este contenido está bloqueado hasta que permita la categoría correspondiente.",
|
|
81
84
|
embedNeedConsent: "Para cargar este contenido, permita la categoría correspondiente en los ajustes de cookies.",
|
|
82
85
|
vendorsHeading: "Qué usa este sitio",
|
|
83
|
-
whatWeUse: "Qué usamos"
|
|
86
|
+
whatWeUse: "Qué usamos",
|
|
87
|
+
gpcNote: "Su navegador envía Global Privacy Control, así que el marketing permanece desactivado."
|
|
84
88
|
},
|
|
85
89
|
it: {
|
|
86
90
|
acceptAll: "Accetta tutto",
|
|
@@ -100,7 +104,8 @@ const CHROME = {
|
|
|
100
104
|
embedBlocked: "Questo contenuto è bloccato finché non consenti la categoria corrispondente.",
|
|
101
105
|
embedNeedConsent: "Per caricare questo contenuto, consenti la categoria corrispondente nelle impostazioni cookie.",
|
|
102
106
|
vendorsHeading: "Cosa usa questo sito",
|
|
103
|
-
whatWeUse: "Cosa usiamo"
|
|
107
|
+
whatWeUse: "Cosa usiamo",
|
|
108
|
+
gpcNote: "Il browser invia Global Privacy Control, quindi il marketing resta disattivato."
|
|
104
109
|
},
|
|
105
110
|
nl: {
|
|
106
111
|
acceptAll: "Alles accepteren",
|
|
@@ -120,7 +125,8 @@ const CHROME = {
|
|
|
120
125
|
embedBlocked: "Deze embed is geblokkeerd tot u de bijbehorende categorie toestaat.",
|
|
121
126
|
embedNeedConsent: "Om deze embed te laden, sta de bijbehorende categorie toe in de cookie-instellingen.",
|
|
122
127
|
vendorsHeading: "Wat deze site gebruikt",
|
|
123
|
-
whatWeUse: "Wat wij gebruiken"
|
|
128
|
+
whatWeUse: "Wat wij gebruiken",
|
|
129
|
+
gpcNote: "Uw browser stuurt Global Privacy Control, dus marketing blijft uit."
|
|
124
130
|
},
|
|
125
131
|
pt: {
|
|
126
132
|
acceptAll: "Aceitar tudo",
|
|
@@ -140,7 +146,8 @@ const CHROME = {
|
|
|
140
146
|
embedBlocked: "Este conteúdo está bloqueado até autorizar a categoria correspondente.",
|
|
141
147
|
embedNeedConsent: "Para carregar este conteúdo, autorize a categoria correspondente nas definições de cookies.",
|
|
142
148
|
vendorsHeading: "O que este site usa",
|
|
143
|
-
whatWeUse: "O que usamos"
|
|
149
|
+
whatWeUse: "O que usamos",
|
|
150
|
+
gpcNote: "O seu navegador enviou Global Privacy Control, por isso o marketing permanece desligado."
|
|
144
151
|
},
|
|
145
152
|
pl: {
|
|
146
153
|
acceptAll: "Zaakceptuj wszystkie",
|
|
@@ -160,7 +167,8 @@ const CHROME = {
|
|
|
160
167
|
embedBlocked: "Ta treść jest zablokowana, dopóki nie zezwolisz na odpowiednią kategorię.",
|
|
161
168
|
embedNeedConsent: "Aby wczytać tę treść, zezwól na odpowiednią kategorię w ustawieniach plików cookie.",
|
|
162
169
|
vendorsHeading: "Czego używa ta witryna",
|
|
163
|
-
whatWeUse: "Czego używamy"
|
|
170
|
+
whatWeUse: "Czego używamy",
|
|
171
|
+
gpcNote: "Przeglądarka wysłała Global Privacy Control, więc marketing pozostaje wyłączony."
|
|
164
172
|
}
|
|
165
173
|
};
|
|
166
174
|
function isLocaleKey(s) {
|
|
@@ -235,6 +243,8 @@ const FATHOM_ID = /^[A-Za-z0-9]{4,32}$/;
|
|
|
235
243
|
const UMAMI_ID = /^[A-Za-z0-9-]{8,64}$/;
|
|
236
244
|
const GA4_ID = /^G-[A-Z0-9]{4,14}$/;
|
|
237
245
|
const GTM_ID = /^GTM-[A-Z0-9]{4,12}$/;
|
|
246
|
+
const CLARITY_ID = /^[a-z0-9]{7,20}$/i;
|
|
247
|
+
const UET_ID = /^[0-9]{6,12}$/;
|
|
238
248
|
function isHostname$1(s) {
|
|
239
249
|
const t = s.trim().toLowerCase();
|
|
240
250
|
if (!t || t.length > 253) return false;
|
|
@@ -253,8 +263,23 @@ function isFathomId(s) {
|
|
|
253
263
|
function isUmamiId(s) {
|
|
254
264
|
return UMAMI_ID.test(s.trim());
|
|
255
265
|
}
|
|
266
|
+
function isClarityId(s) {
|
|
267
|
+
return CLARITY_ID.test(s.trim());
|
|
268
|
+
}
|
|
269
|
+
function isUetId(s) {
|
|
270
|
+
return UET_ID.test(s.trim());
|
|
271
|
+
}
|
|
256
272
|
//#endregion
|
|
257
273
|
//#region src/security.ts
|
|
274
|
+
/**
|
|
275
|
+
* Shared URL / request guards used by admin validation and (via mirrored
|
|
276
|
+
* logic) the public bootstrap. Keep client checks in sync with these helpers.
|
|
277
|
+
*/
|
|
278
|
+
const YT_IFRAME = /^https:\/\/(?:www\.)?youtube-nocookie\.com\/embed\/[A-Za-z0-9_-]{11}\/?$/;
|
|
279
|
+
const VIMEO_IFRAME = /^https:\/\/player\.vimeo\.com\/video\/\d{6,12}\/?$/;
|
|
280
|
+
/** Sandbox and feature policy for YouTube/Vimeo iframes. Kept beside the src regexes. */
|
|
281
|
+
const EMBED_IFRAME_SANDBOX = "allow-scripts allow-same-origin allow-presentation";
|
|
282
|
+
const EMBED_IFRAME_ALLOW = "accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; web-share";
|
|
258
283
|
/** Hosts EmPrivacy may load as <script src> for built-in presets. */
|
|
259
284
|
const PRESET_SCRIPT_HOSTS = /* @__PURE__ */ new Set([
|
|
260
285
|
"static.cloudflareinsights.com",
|
|
@@ -262,7 +287,10 @@ const PRESET_SCRIPT_HOSTS = /* @__PURE__ */ new Set([
|
|
|
262
287
|
"cdn.usefathom.com",
|
|
263
288
|
"scripts.simpleanalyticscdn.com",
|
|
264
289
|
"www.googletagmanager.com",
|
|
265
|
-
"googletagmanager.com"
|
|
290
|
+
"googletagmanager.com",
|
|
291
|
+
"www.clarity.ms",
|
|
292
|
+
"clarity.ms",
|
|
293
|
+
"bat.bing.com"
|
|
266
294
|
]);
|
|
267
295
|
const HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i;
|
|
268
296
|
function normalizeCookieMaxAgeDays(n, fallback = 180) {
|
|
@@ -283,6 +311,85 @@ function cookieMaxAgeSeconds(days) {
|
|
|
283
311
|
function hasUnsafeUrlChars$1(s) {
|
|
284
312
|
return /[\u0000-\u001F\u007F\s]/.test(s);
|
|
285
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Browser copy of the non-public host check. `parseStrictHttpsUrl` calls the
|
|
316
|
+
* same source so the inline bootstrap cannot drift from the server.
|
|
317
|
+
* Hostnames are whatever `new URL` already normalized (decimal, hex, and
|
|
318
|
+
* short IPv4 forms become dotted quads; IPv6 keeps brackets).
|
|
319
|
+
*/
|
|
320
|
+
function hostnameBlockScript() {
|
|
321
|
+
return `function hostBlocked(raw){
|
|
322
|
+
var host=String(raw||"").trim().toLowerCase().replace(/^\\[|\\]$/g,"");
|
|
323
|
+
if(!host)return true;
|
|
324
|
+
if(host==="localhost"||endsWith(host,".localhost")||endsWith(host,".local")||endsWith(host,".internal")||endsWith(host,".arpa"))return true;
|
|
325
|
+
if(host.indexOf(":")>=0)return ipv6Blocked(host);
|
|
326
|
+
if(/^[0-9.]+$/.test(host)){
|
|
327
|
+
var v4=parseV4(host);
|
|
328
|
+
if(v4===null)return true;
|
|
329
|
+
return v4Blocked(v4);
|
|
330
|
+
}
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
function endsWith(h,s){return h.length>=s.length&&h.slice(h.length-s.length)===s;}
|
|
334
|
+
function parseV4(host){
|
|
335
|
+
var p=host.split(".");
|
|
336
|
+
if(p.length!==4)return null;
|
|
337
|
+
var n=0;
|
|
338
|
+
for(var i=0;i<p.length;i++){
|
|
339
|
+
var part=p[i];
|
|
340
|
+
if(!/^\\d{1,3}$/.test(part))return null;
|
|
341
|
+
if(part.length>1&&part.charAt(0)==="0")return null;
|
|
342
|
+
var v=+part;
|
|
343
|
+
if(v>255)return null;
|
|
344
|
+
n=n*256+v;
|
|
345
|
+
}
|
|
346
|
+
return n>>>0;
|
|
347
|
+
}
|
|
348
|
+
function v4Blocked(n){
|
|
349
|
+
var a=n>>>24;
|
|
350
|
+
var b=(n>>>16)&255;
|
|
351
|
+
if(a===0||a===10||a===127)return true;
|
|
352
|
+
if(a===100&&b>=64&&b<=127)return true;
|
|
353
|
+
if(a===169&&b===254)return true;
|
|
354
|
+
if(a===172&&b>=16&&b<=31)return true;
|
|
355
|
+
if(a===192&&b===168)return true;
|
|
356
|
+
if(a>=224)return true;
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
function ipv6Blocked(host){
|
|
360
|
+
if(host==="::"||host==="::1")return true;
|
|
361
|
+
var dotted=/^::ffff:(\\d{1,3}(?:\\.\\d{1,3}){3})$/.exec(host);
|
|
362
|
+
if(dotted){
|
|
363
|
+
var d4=parseV4(dotted[1]);
|
|
364
|
+
return d4===null||v4Blocked(d4);
|
|
365
|
+
}
|
|
366
|
+
var mapped=/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(host);
|
|
367
|
+
if(mapped){
|
|
368
|
+
var hi=parseInt(mapped[1],16);
|
|
369
|
+
var lo=parseInt(mapped[2],16);
|
|
370
|
+
if(hi>65535||lo>65535)return true;
|
|
371
|
+
return v4Blocked(((hi<<16)|lo)>>>0);
|
|
372
|
+
}
|
|
373
|
+
if(/\\d{1,3}(?:\\.\\d{1,3}){3}$/.test(host))return true;
|
|
374
|
+
var first;
|
|
375
|
+
if(host.slice(0,2)==="::")first=0;
|
|
376
|
+
else{
|
|
377
|
+
var head=host.split(":")[0];
|
|
378
|
+
if(!/^[0-9a-f]{1,4}$/.test(head))return true;
|
|
379
|
+
first=parseInt(head,16);
|
|
380
|
+
}
|
|
381
|
+
if((first&0xfe00)===0xfc00)return true;
|
|
382
|
+
if((first&0xffc0)===0xfe80)return true;
|
|
383
|
+
if((first&0xffc0)===0xfec0)return true;
|
|
384
|
+
if((first&0xff00)===0xff00)return true;
|
|
385
|
+
return false;
|
|
386
|
+
}`;
|
|
387
|
+
}
|
|
388
|
+
const hostBlockedImpl = new Function(`${hostnameBlockScript()}\nreturn hostBlocked;`)();
|
|
389
|
+
/** True for loopback, private, link-local, shared (CGNAT), and non-global addresses. */
|
|
390
|
+
function isBlockedHostname(hostname) {
|
|
391
|
+
return hostBlockedImpl(hostname);
|
|
392
|
+
}
|
|
286
393
|
function parseStrictHttpsUrl(raw) {
|
|
287
394
|
const t = raw.trim();
|
|
288
395
|
if (!t || t.length > 2048 || hasUnsafeUrlChars$1(t)) return null;
|
|
@@ -291,9 +398,7 @@ function parseStrictHttpsUrl(raw) {
|
|
|
291
398
|
const u = new URL(t);
|
|
292
399
|
if (u.protocol !== "https:") return null;
|
|
293
400
|
if (u.username || u.password) return null;
|
|
294
|
-
|
|
295
|
-
if (host === "localhost" || host.endsWith(".localhost")) return null;
|
|
296
|
-
if (host === "127.0.0.1" || host === "::1" || host.endsWith(".local")) return null;
|
|
401
|
+
if (isBlockedHostname(u.hostname)) return null;
|
|
297
402
|
return u;
|
|
298
403
|
} catch {
|
|
299
404
|
return null;
|
|
@@ -326,9 +431,18 @@ function isScriptHostAllowed(raw, opts) {
|
|
|
326
431
|
if (!u) return false;
|
|
327
432
|
const host = u.hostname.toLowerCase();
|
|
328
433
|
if (opts.allowPresets && PRESET_SCRIPT_HOSTS.has(host)) return true;
|
|
329
|
-
if (opts.allowlist.length === 0) return true;
|
|
330
434
|
return opts.allowlist.includes(host);
|
|
331
435
|
}
|
|
436
|
+
/** SRI required before a custom or marketing script may load. Empty is not valid. */
|
|
437
|
+
function hasSri(s) {
|
|
438
|
+
return typeof s === "string" && /^sha(?:256|384|512)-[A-Za-z0-9+/=]+$/.test(s);
|
|
439
|
+
}
|
|
440
|
+
function isPublishableCustomScript(src, integrity, allowlist) {
|
|
441
|
+
return hasSri(integrity) && isScriptHostAllowed(src, {
|
|
442
|
+
allowlist: [...allowlist],
|
|
443
|
+
allowPresets: false
|
|
444
|
+
});
|
|
445
|
+
}
|
|
332
446
|
/** Optional SRI: `https://cdn.example/a.js sha384-...` or `... integrity=sha384-...`. */
|
|
333
447
|
function parseScriptUrlWithIntegrity(line) {
|
|
334
448
|
const t = line.trim();
|
|
@@ -368,6 +482,29 @@ function assertSameOriginMutation(request, expectedOrigin) {
|
|
|
368
482
|
if ((request.headers.get("sec-fetch-site") ?? "").toLowerCase() === "same-origin") return null;
|
|
369
483
|
return new Response("forbidden", { status: 403 });
|
|
370
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Compare-and-set increment. Returns false when the bucket is full, the store
|
|
487
|
+
* errors, or eight conflicts in a row fail to land a write.
|
|
488
|
+
*/
|
|
489
|
+
async function tryConsumeRateSlot(kv, key, limit) {
|
|
490
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
491
|
+
let current;
|
|
492
|
+
try {
|
|
493
|
+
current = await kv.getVersioned(key);
|
|
494
|
+
} catch {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
const raw = current?.value;
|
|
498
|
+
const n = typeof raw === "string" && /^\d{1,6}$/.test(raw) ? Number(raw) : 0;
|
|
499
|
+
if (n >= limit) return false;
|
|
500
|
+
try {
|
|
501
|
+
if ((await kv.compareAndSet(key, current ? current.revision : null, String(n + 1))).applied) return true;
|
|
502
|
+
} catch {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
371
508
|
/** Coarse hour bucket for rate limiting (UTC). */
|
|
372
509
|
function rateHourKey(now = /* @__PURE__ */ new Date()) {
|
|
373
510
|
return `${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(2, "0")}${String(now.getUTCDate()).padStart(2, "0")}${String(now.getUTCHours()).padStart(2, "0")}`;
|
|
@@ -376,12 +513,81 @@ function rateHourKey(now = /* @__PURE__ */ new Date()) {
|
|
|
376
513
|
//#region src/theme.ts
|
|
377
514
|
/** Hex colors only — rejects CSS injection via theme fields. */
|
|
378
515
|
const HEX = /^#(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
|
|
516
|
+
/** Fixed banner palettes. Colors are constants, never taken from the admin form. */
|
|
517
|
+
const THEME_PROFILES = {
|
|
518
|
+
slate: {
|
|
519
|
+
bg: "#0f172a",
|
|
520
|
+
text: "#f8fafc",
|
|
521
|
+
accent: "#3b82f6",
|
|
522
|
+
radiusPx: 8
|
|
523
|
+
},
|
|
524
|
+
paper: {
|
|
525
|
+
bg: "#f7f6f3",
|
|
526
|
+
text: "#37352f",
|
|
527
|
+
accent: "#1d4ed8",
|
|
528
|
+
radiusPx: 6
|
|
529
|
+
},
|
|
530
|
+
ink: {
|
|
531
|
+
bg: "#000000",
|
|
532
|
+
text: "#ededed",
|
|
533
|
+
accent: "#0070f3",
|
|
534
|
+
radiusPx: 8
|
|
535
|
+
},
|
|
536
|
+
indigo: {
|
|
537
|
+
bg: "#ffffff",
|
|
538
|
+
text: "#0f172a",
|
|
539
|
+
accent: "#4f46e5",
|
|
540
|
+
radiusPx: 12
|
|
541
|
+
},
|
|
542
|
+
primer: {
|
|
543
|
+
bg: "#f6f8fa",
|
|
544
|
+
text: "#1f2328",
|
|
545
|
+
accent: "#0969da",
|
|
546
|
+
radiusPx: 6
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
const THEME_PROFILE_IDS = [
|
|
550
|
+
"slate",
|
|
551
|
+
"paper",
|
|
552
|
+
"ink",
|
|
553
|
+
"indigo",
|
|
554
|
+
"primer"
|
|
555
|
+
];
|
|
556
|
+
/** First profile. New installs and configs with no theme choice use this. */
|
|
557
|
+
const DEFAULT_THEME_PROFILE = THEME_PROFILE_IDS[0];
|
|
379
558
|
const DEFAULT_THEME = {
|
|
380
|
-
bg:
|
|
381
|
-
text:
|
|
382
|
-
accent:
|
|
383
|
-
radiusPx:
|
|
559
|
+
bg: THEME_PROFILES[DEFAULT_THEME_PROFILE].bg,
|
|
560
|
+
text: THEME_PROFILES[DEFAULT_THEME_PROFILE].text,
|
|
561
|
+
accent: THEME_PROFILES[DEFAULT_THEME_PROFILE].accent,
|
|
562
|
+
radiusPx: THEME_PROFILES[DEFAULT_THEME_PROFILE].radiusPx
|
|
384
563
|
};
|
|
564
|
+
function isThemeProfileId(value) {
|
|
565
|
+
return THEME_PROFILE_IDS.includes(value);
|
|
566
|
+
}
|
|
567
|
+
/** Stored JSON: a missing or unknown choice uses the first profile. Explicit `custom` is kept. */
|
|
568
|
+
function normalizeThemeChoice(value) {
|
|
569
|
+
if (value === "custom") return "custom";
|
|
570
|
+
if (typeof value === "string" && isThemeProfileId(value)) return value;
|
|
571
|
+
return DEFAULT_THEME_PROFILE;
|
|
572
|
+
}
|
|
573
|
+
/** Admin save: blank uses the first profile. `custom` is the manual option. */
|
|
574
|
+
function parseThemeChoice(value) {
|
|
575
|
+
const t = value.trim().toLowerCase();
|
|
576
|
+
if (!t) return DEFAULT_THEME_PROFILE;
|
|
577
|
+
if (t === "custom") return "custom";
|
|
578
|
+
if (isThemeProfileId(t)) return t;
|
|
579
|
+
throw new Error("Banner theme must be Custom, Slate, Paper, Ink, Indigo, or Primer.");
|
|
580
|
+
}
|
|
581
|
+
function themeForChoice(choice, custom) {
|
|
582
|
+
if (choice === "custom") return custom;
|
|
583
|
+
const profile = THEME_PROFILES[choice];
|
|
584
|
+
return {
|
|
585
|
+
bg: profile.bg,
|
|
586
|
+
text: profile.text,
|
|
587
|
+
accent: profile.accent,
|
|
588
|
+
radiusPx: profile.radiusPx
|
|
589
|
+
};
|
|
590
|
+
}
|
|
385
591
|
function isSafeHexColor(s) {
|
|
386
592
|
return HEX.test(s.trim());
|
|
387
593
|
}
|
|
@@ -424,14 +630,17 @@ const DEFAULT_CONFIG = {
|
|
|
424
630
|
marketingScriptUrls: [],
|
|
425
631
|
scriptIntegrity: {},
|
|
426
632
|
scriptHostAllowlist: [],
|
|
633
|
+
embedHostAllowlist: [],
|
|
427
634
|
cookieMaxAgeDays: 180,
|
|
428
635
|
googleConsentMode: false,
|
|
429
636
|
logConsentToServer: false,
|
|
430
637
|
embedCategory: "marketing",
|
|
431
638
|
gateEmbeds: true,
|
|
432
639
|
hideBannerOnPolicyPages: true,
|
|
640
|
+
bannerPosition: "bottom",
|
|
433
641
|
defaultLocale: "en",
|
|
434
642
|
localeOverrides: {},
|
|
643
|
+
themeProfile: "slate",
|
|
435
644
|
theme: { ...DEFAULT_THEME }
|
|
436
645
|
};
|
|
437
646
|
const ANALYTICS_PROVIDERS = [
|
|
@@ -443,8 +652,13 @@ const ANALYTICS_PROVIDERS = [
|
|
|
443
652
|
"umami",
|
|
444
653
|
"simpleanalytics",
|
|
445
654
|
"ga4",
|
|
446
|
-
"gtm"
|
|
655
|
+
"gtm",
|
|
656
|
+
"clarity",
|
|
657
|
+
"uet"
|
|
447
658
|
];
|
|
659
|
+
function normalizeBannerPosition(value) {
|
|
660
|
+
return value === "top" ? "top" : "bottom";
|
|
661
|
+
}
|
|
448
662
|
function hasUnsafeUrlChars(s) {
|
|
449
663
|
return /[\u0000-\u001F\u007F\s]/.test(s);
|
|
450
664
|
}
|
|
@@ -561,6 +775,7 @@ function normalizeConfig(raw) {
|
|
|
561
775
|
if (Object.keys(scriptIntegrity).length >= 100) break;
|
|
562
776
|
}
|
|
563
777
|
const scriptHostAllowlist = (Array.isArray(o.scriptHostAllowlist) ? o.scriptHostAllowlist.filter((x) => typeof x === "string") : []).map((h) => h.trim().toLowerCase()).filter((h) => isHostname$1(h)).slice(0, 50);
|
|
778
|
+
const embedHostAllowlist = (Array.isArray(o.embedHostAllowlist) ? o.embedHostAllowlist.filter((x) => typeof x === "string") : []).map((h) => h.trim().toLowerCase()).filter((h) => isHostname$1(h)).slice(0, 50);
|
|
564
779
|
return {
|
|
565
780
|
bannerTitle: typeof o.bannerTitle === "string" ? o.bannerTitle : DEFAULT_CONFIG.bannerTitle,
|
|
566
781
|
bannerMessage: typeof o.bannerMessage === "string" ? o.bannerMessage : DEFAULT_CONFIG.bannerMessage,
|
|
@@ -576,20 +791,23 @@ function normalizeConfig(raw) {
|
|
|
576
791
|
marketingScriptUrls: marketing,
|
|
577
792
|
scriptIntegrity,
|
|
578
793
|
scriptHostAllowlist,
|
|
794
|
+
embedHostAllowlist,
|
|
579
795
|
cookieMaxAgeDays: normalizeCookieMaxAgeDays(o.cookieMaxAgeDays, DEFAULT_CONFIG.cookieMaxAgeDays),
|
|
580
796
|
googleConsentMode: typeof o.googleConsentMode === "boolean" ? o.googleConsentMode : DEFAULT_CONFIG.googleConsentMode,
|
|
581
797
|
logConsentToServer: typeof o.logConsentToServer === "boolean" ? o.logConsentToServer : DEFAULT_CONFIG.logConsentToServer,
|
|
582
798
|
embedCategory: isEmbedCategory(embedRaw) ? embedRaw : DEFAULT_CONFIG.embedCategory,
|
|
583
799
|
gateEmbeds: typeof o.gateEmbeds === "boolean" ? o.gateEmbeds : DEFAULT_CONFIG.gateEmbeds,
|
|
584
800
|
hideBannerOnPolicyPages: typeof o.hideBannerOnPolicyPages === "boolean" ? o.hideBannerOnPolicyPages : DEFAULT_CONFIG.hideBannerOnPolicyPages,
|
|
801
|
+
bannerPosition: normalizeBannerPosition(o.bannerPosition),
|
|
585
802
|
defaultLocale: typeof o.defaultLocale === "string" && o.defaultLocale ? o.defaultLocale : DEFAULT_CONFIG.defaultLocale,
|
|
586
803
|
localeOverrides: parseLocaleOverrides(o.localeOverrides),
|
|
587
|
-
|
|
804
|
+
themeProfile: normalizeThemeChoice(o.themeProfile),
|
|
805
|
+
theme: themeForChoice(normalizeThemeChoice(o.themeProfile), {
|
|
588
806
|
bg: normalizeHexColor(typeof themeRaw.bg === "string" ? themeRaw.bg : "", DEFAULT_THEME.bg),
|
|
589
807
|
text: normalizeHexColor(typeof themeRaw.text === "string" ? themeRaw.text : "", DEFAULT_THEME.text),
|
|
590
808
|
accent: normalizeHexColor(typeof themeRaw.accent === "string" ? themeRaw.accent : "", DEFAULT_THEME.accent),
|
|
591
809
|
radiusPx: normalizeRadiusPx(themeRaw.radiusPx, DEFAULT_THEME.radiusPx)
|
|
592
|
-
}
|
|
810
|
+
})
|
|
593
811
|
};
|
|
594
812
|
}
|
|
595
813
|
function assertAnalyticsId(provider, id, umamiScriptUrl) {
|
|
@@ -604,7 +822,7 @@ function assertAnalyticsId(provider, id, umamiScriptUrl) {
|
|
|
604
822
|
}
|
|
605
823
|
if (provider === "umami") {
|
|
606
824
|
if (!isUmamiId(t)) throw new Error("Umami website ID looks invalid.");
|
|
607
|
-
if (!
|
|
825
|
+
if (!isSafeHttpsScriptUrl(umamiScriptUrl)) throw new Error("Umami requires an https script URL on a public host (Cloud or your self-hosted tracker).");
|
|
608
826
|
return;
|
|
609
827
|
}
|
|
610
828
|
if (provider === "ga4") {
|
|
@@ -615,6 +833,14 @@ function assertAnalyticsId(provider, id, umamiScriptUrl) {
|
|
|
615
833
|
if (!isGtmId(t)) throw new Error("Google Tag Manager ID must look like GTM-XXXX.");
|
|
616
834
|
return;
|
|
617
835
|
}
|
|
836
|
+
if (provider === "clarity") {
|
|
837
|
+
if (!isClarityId(t)) throw new Error("Clarity project ID must be 7–20 letters or digits.");
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (provider === "uet") {
|
|
841
|
+
if (!isUetId(t)) throw new Error("UET tag ID must be 6–12 digits.");
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
618
844
|
if (provider === "simpleanalytics" && t && !isHostname$1(t)) throw new Error("Simple Analytics hostname must be a domain like example.com, or empty.");
|
|
619
845
|
}
|
|
620
846
|
/** Validate admin-submitted fields; throws an Error with a short message on failure */
|
|
@@ -634,22 +860,23 @@ function assertValidSavedConfig(input) {
|
|
|
634
860
|
const tokenTrim = input.cloudflareToken.trim();
|
|
635
861
|
if (ap === "cloudflare") assertValidCloudflareToken(tokenTrim);
|
|
636
862
|
const umamiScriptUrl = input.umamiScriptUrl.trim();
|
|
637
|
-
if (ap === "umami" && umamiScriptUrl && !
|
|
863
|
+
if (ap === "umami" && umamiScriptUrl && !isSafeHttpsScriptUrl(umamiScriptUrl)) throw new Error(invalidScriptUrlError("umami", umamiScriptUrl));
|
|
638
864
|
if (ap !== "none" && ap !== "custom" && ap !== "cloudflare" && ap !== "simpleanalytics" && ap !== "gtm") assertAnalyticsId(ap, input.analyticsId, umamiScriptUrl);
|
|
639
865
|
else if (ap === "simpleanalytics" && input.analyticsId.trim()) assertAnalyticsId(ap, input.analyticsId, umamiScriptUrl);
|
|
640
866
|
else if (ap === "gtm") assertAnalyticsId(ap, input.analyticsId, umamiScriptUrl);
|
|
641
867
|
const scriptHostAllowlist = parseHostnameAllowlist(input.scriptHostAllowlistText);
|
|
868
|
+
const embedHostAllowlist = parseHostnameAllowlist(input.embedHostAllowlistText);
|
|
642
869
|
const cookieMaxAgeDays = parseCookieMaxAgeDays(input.cookieMaxAgeDays, 180);
|
|
643
870
|
const scriptIntegrity = {};
|
|
644
871
|
const assertScriptLine = (kind, line) => {
|
|
645
872
|
const { src, integrity } = parseScriptUrlWithIntegrity(line);
|
|
646
873
|
if (!src || src.length > 2048) throw new Error(invalidScriptUrlError(kind, line));
|
|
647
874
|
if (!isSafeHttpsScriptUrl(src)) throw new Error(invalidScriptUrlError(kind, src));
|
|
648
|
-
if (
|
|
875
|
+
if (!hasSri(integrity)) throw new Error(`${kind} scripts require Subresource Integrity (sha256-, sha384-, or sha512-) after the URL.`);
|
|
649
876
|
if (!isScriptHostAllowed(src, {
|
|
650
877
|
allowlist: scriptHostAllowlist,
|
|
651
878
|
allowPresets: false
|
|
652
|
-
})) throw new Error(`${kind} script host is not on the allowlist. Add the hostname
|
|
879
|
+
})) throw new Error(`${kind} script host is not on the allowlist. Add the hostname under Script host allowlist.`);
|
|
653
880
|
if (integrity) scriptIntegrity[src] = integrity;
|
|
654
881
|
return src;
|
|
655
882
|
};
|
|
@@ -657,7 +884,7 @@ function assertValidSavedConfig(input) {
|
|
|
657
884
|
if (!isScriptHostAllowed(umamiScriptUrl, {
|
|
658
885
|
allowlist: scriptHostAllowlist,
|
|
659
886
|
allowPresets: false
|
|
660
|
-
})) throw new Error("Umami script host is not on the allowlist. Add the hostname
|
|
887
|
+
})) throw new Error("Umami script host is not on the allowlist. Add the hostname under Script host allowlist.");
|
|
661
888
|
}
|
|
662
889
|
const analyticsScriptUrls = [];
|
|
663
890
|
if (ap === "custom") {
|
|
@@ -673,17 +900,22 @@ function assertValidSavedConfig(input) {
|
|
|
673
900
|
if (!isEmbedCategory(embedCategory)) throw new Error("Embeds must require Functional or Marketing consent.");
|
|
674
901
|
const defaultLocale = input.defaultLocale.trim().toLowerCase() || "en";
|
|
675
902
|
if (!/^[a-z]{2,3}(?:-[a-z]{2})?$/.test(defaultLocale)) throw new Error("Default locale must look like en or pt-BR.");
|
|
903
|
+
const bannerPosition = input.bannerPosition.trim().toLowerCase();
|
|
904
|
+
if (bannerPosition !== "top" && bannerPosition !== "bottom") throw new Error("Banner position must be top or bottom.");
|
|
676
905
|
const localeOverrides = parseLocaleOverridesText(input.localeOverridesText);
|
|
677
906
|
for (const pack of Object.values(localeOverrides)) {
|
|
678
907
|
if (pack.bannerTitle !== void 0) assertOptionalLength("Translated banner title", pack.bannerTitle, 120);
|
|
679
908
|
if (pack.bannerMessage !== void 0) assertOptionalLength("Translated short notice", pack.bannerMessage, 600);
|
|
680
909
|
}
|
|
910
|
+
const themeProfile = parseThemeChoice(input.themeProfile);
|
|
681
911
|
const themeBg = input.themeBg.trim() || DEFAULT_THEME.bg;
|
|
682
912
|
const themeText = input.themeText.trim() || DEFAULT_THEME.text;
|
|
683
913
|
const themeAccent = input.themeAccent.trim() || DEFAULT_THEME.accent;
|
|
684
|
-
if (
|
|
685
|
-
|
|
686
|
-
|
|
914
|
+
if (themeProfile === "custom") {
|
|
915
|
+
if (input.themeBg.trim() && !/^#(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(themeBg)) throw new Error("Banner background must be a hex color like #111111.");
|
|
916
|
+
if (input.themeText.trim() && !/^#(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(themeText)) throw new Error("Banner text color must be a hex color like #eeeeee.");
|
|
917
|
+
if (input.themeAccent.trim() && !/^#(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(themeAccent)) throw new Error("Banner accent color must be a hex color like #3b82f6.");
|
|
918
|
+
}
|
|
687
919
|
return normalizeConfig({
|
|
688
920
|
bannerTitle: input.bannerTitle.trim() || DEFAULT_CONFIG.bannerTitle,
|
|
689
921
|
bannerMessage: input.bannerMessage.trim() || DEFAULT_CONFIG.bannerMessage,
|
|
@@ -699,20 +931,23 @@ function assertValidSavedConfig(input) {
|
|
|
699
931
|
marketingScriptUrls,
|
|
700
932
|
scriptIntegrity,
|
|
701
933
|
scriptHostAllowlist,
|
|
934
|
+
embedHostAllowlist,
|
|
702
935
|
cookieMaxAgeDays,
|
|
703
936
|
googleConsentMode: input.googleConsentMode,
|
|
704
937
|
logConsentToServer: input.logConsentToServer,
|
|
705
938
|
embedCategory,
|
|
706
939
|
gateEmbeds: input.gateEmbeds,
|
|
707
940
|
hideBannerOnPolicyPages: input.hideBannerOnPolicyPages,
|
|
941
|
+
bannerPosition,
|
|
708
942
|
defaultLocale,
|
|
709
943
|
localeOverrides,
|
|
710
|
-
|
|
944
|
+
themeProfile,
|
|
945
|
+
theme: themeForChoice(themeProfile, themeProfile === "custom" ? {
|
|
711
946
|
bg: themeBg,
|
|
712
947
|
text: themeText,
|
|
713
948
|
accent: themeAccent,
|
|
714
949
|
radiusPx: parseRadiusInput(input.themeRadius, DEFAULT_THEME.radiusPx)
|
|
715
|
-
}
|
|
950
|
+
} : { ...DEFAULT_THEME })
|
|
716
951
|
});
|
|
717
952
|
}
|
|
718
953
|
/** Safe to embed in `<script type="application/json">` (breakout-safe) */
|
|
@@ -830,19 +1065,83 @@ async function fetchLatestPublishedVersion(options) {
|
|
|
830
1065
|
}
|
|
831
1066
|
}
|
|
832
1067
|
//#endregion
|
|
1068
|
+
//#region src/consent-csv.ts
|
|
1069
|
+
const FORMULA_PREFIX = /^[\t\r ]*[=+\-@]/;
|
|
1070
|
+
/** Quote a cell and neutralize spreadsheet formula injection. */
|
|
1071
|
+
function csvCell(value) {
|
|
1072
|
+
let s = value.replace(/\r\n|\n|\r/g, " ");
|
|
1073
|
+
if (FORMULA_PREFIX.test(s)) s = `'${s}`;
|
|
1074
|
+
return `"${s.replace(/"/g, "\"\"")}"`;
|
|
1075
|
+
}
|
|
1076
|
+
function boolCell(value) {
|
|
1077
|
+
return value === true ? "true" : value === false ? "false" : "";
|
|
1078
|
+
}
|
|
1079
|
+
function buildConsentCsv(rows) {
|
|
1080
|
+
const lines = [[
|
|
1081
|
+
"createdAt",
|
|
1082
|
+
"policyVersion",
|
|
1083
|
+
"functional",
|
|
1084
|
+
"analytics",
|
|
1085
|
+
"marketing",
|
|
1086
|
+
"gpc"
|
|
1087
|
+
].map(csvCell).join(",")];
|
|
1088
|
+
for (const row of rows) lines.push([
|
|
1089
|
+
csvCell(typeof row.createdAt === "string" ? row.createdAt : ""),
|
|
1090
|
+
csvCell(typeof row.policyVersion === "string" ? row.policyVersion : ""),
|
|
1091
|
+
csvCell(boolCell(row.functional)),
|
|
1092
|
+
csvCell(boolCell(row.analytics)),
|
|
1093
|
+
csvCell(boolCell(row.marketing)),
|
|
1094
|
+
csvCell(row.gpc === true ? "true" : "")
|
|
1095
|
+
].join(","));
|
|
1096
|
+
return `\uFEFF${lines.join("\r\n")}\r\n`;
|
|
1097
|
+
}
|
|
1098
|
+
//#endregion
|
|
1099
|
+
//#region src/cookie-purge.ts
|
|
1100
|
+
const ANALYTICS_COOKIE_EXACT = [
|
|
1101
|
+
"_ga",
|
|
1102
|
+
"_gid",
|
|
1103
|
+
"_gat"
|
|
1104
|
+
];
|
|
1105
|
+
const ANALYTICS_COOKIE_PREFIX = ["_ga_", "_gat_"];
|
|
1106
|
+
const MARKETING_COOKIE_EXACT = [
|
|
1107
|
+
"_gcl_au",
|
|
1108
|
+
"_gcl_aw",
|
|
1109
|
+
"_gcl_dc",
|
|
1110
|
+
"_clck",
|
|
1111
|
+
"_clsk",
|
|
1112
|
+
"CLID",
|
|
1113
|
+
"_uetsid",
|
|
1114
|
+
"_uetvid"
|
|
1115
|
+
];
|
|
1116
|
+
const MARKETING_COOKIE_PREFIX = ["_gcl_"];
|
|
1117
|
+
const PURGE_RULES = {
|
|
1118
|
+
analytics: {
|
|
1119
|
+
exact: [...ANALYTICS_COOKIE_EXACT],
|
|
1120
|
+
prefix: [...ANALYTICS_COOKIE_PREFIX]
|
|
1121
|
+
},
|
|
1122
|
+
marketing: {
|
|
1123
|
+
exact: [...MARKETING_COOKIE_EXACT],
|
|
1124
|
+
prefix: [...MARKETING_COOKIE_PREFIX]
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
//#endregion
|
|
833
1128
|
//#region src/public-bootstrap.ts
|
|
834
1129
|
/**
|
|
835
1130
|
* Public-site bootstrap. Banner copy is applied with textContent / createTextNode only.
|
|
836
1131
|
* Embed/script URLs are re-validated in the browser (never trust DOM data-src alone).
|
|
837
1132
|
*/
|
|
838
1133
|
function buildBodyBootstrap(pr) {
|
|
1134
|
+
const jsonLiteral = jsonForHtmlScript(pr);
|
|
1135
|
+
const purgeLiteral = jsonForHtmlScript(PURGE_RULES);
|
|
839
1136
|
return `(function(){
|
|
840
|
-
var C=${
|
|
1137
|
+
var C=${jsonLiteral};
|
|
841
1138
|
var CN="${COOKIE_NAME}";
|
|
1139
|
+
var PURGE=${purgeLiteral};
|
|
842
1140
|
var listeners=[];
|
|
843
|
-
var PRESET_HOSTS
|
|
844
|
-
var YT_IFRAME
|
|
845
|
-
var VIMEO_IFRAME
|
|
1141
|
+
var PRESET_HOSTS=${jsonForHtmlScript(Object.fromEntries([...PRESET_SCRIPT_HOSTS].map((host) => [host, 1])))};
|
|
1142
|
+
var YT_IFRAME=${YT_IFRAME.toString()};
|
|
1143
|
+
var VIMEO_IFRAME=${VIMEO_IFRAME.toString()};
|
|
1144
|
+
${hostnameBlockScript()}
|
|
846
1145
|
function readCookie(){
|
|
847
1146
|
try{
|
|
848
1147
|
var m=document.cookie.match(new RegExp("(?:^|;\\\\s*)"+CN+"=([^;]*)"));
|
|
@@ -867,7 +1166,64 @@ if(!flagOk(o.a)||!flagOk(o.m)||!flagOk(o.f))return null;
|
|
|
867
1166
|
return {v:o.v,essential:true,functional:!!o.f,analytics:!!o.a,marketing:!!o.m};
|
|
868
1167
|
}catch(e){return null;}
|
|
869
1168
|
}
|
|
870
|
-
function
|
|
1169
|
+
function gpcOn(){
|
|
1170
|
+
try{return navigator.globalPrivacyControl===true;}catch(e){return false;}
|
|
1171
|
+
}
|
|
1172
|
+
function effective(st){
|
|
1173
|
+
if(!st)return null;
|
|
1174
|
+
var g=gpcOn();
|
|
1175
|
+
return {v:st.v,essential:true,functional:!!st.functional,analytics:!!st.analytics,marketing:g?false:!!st.marketing,gpc:g};
|
|
1176
|
+
}
|
|
1177
|
+
function currentState(){return effective(parseState(readCookie()));}
|
|
1178
|
+
function cookieNameOk(name){return /^[A-Za-z0-9_]{1,64}$/.test(name);}
|
|
1179
|
+
function expireCookie(name){
|
|
1180
|
+
if(!cookieNameOk(name)||name===CN)return;
|
|
1181
|
+
var secure=document.location.protocol==="https:"?"; Secure":"";
|
|
1182
|
+
var base=name+"=; Max-Age=0; Path=/; SameSite=Lax"+secure;
|
|
1183
|
+
document.cookie=base;
|
|
1184
|
+
var host=String(document.location.hostname||"");
|
|
1185
|
+
if(!/^[A-Za-z0-9.-]{1,253}$/.test(host)||host.indexOf("..")>=0)return;
|
|
1186
|
+
var hosts=[host];
|
|
1187
|
+
var labels=host.split(".");
|
|
1188
|
+
if(labels.length>=3)hosts.push(labels.slice(1).join("."));
|
|
1189
|
+
for(var i=0;i<hosts.length;i++){
|
|
1190
|
+
var h=hosts[i];
|
|
1191
|
+
if(!/^[A-Za-z0-9.-]{1,253}$/.test(h))continue;
|
|
1192
|
+
document.cookie=base+"; Domain="+h;
|
|
1193
|
+
document.cookie=base+"; Domain=."+h;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
function purgeSet(exact,prefixes){
|
|
1197
|
+
var raw="";
|
|
1198
|
+
try{raw=document.cookie;}catch(e){return;}
|
|
1199
|
+
if(!raw)return;
|
|
1200
|
+
if(raw.length>8192)raw=raw.slice(0,8192);
|
|
1201
|
+
raw.split(";").forEach(function(part){
|
|
1202
|
+
var name=(part.split("=")[0]||"").replace(/^\\s+/,"");
|
|
1203
|
+
if(!cookieNameOk(name)||name===CN)return;
|
|
1204
|
+
var hit=exact.indexOf(name)>=0;
|
|
1205
|
+
if(!hit){
|
|
1206
|
+
for(var i=0;i<prefixes.length;i++){
|
|
1207
|
+
if(prefixes[i]&&name.indexOf(prefixes[i])===0){hit=true;break;}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
if(hit)expireCookie(name);
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
function purgeDenied(st){
|
|
1214
|
+
var rules=PURGE||{};
|
|
1215
|
+
if(!st.analytics){
|
|
1216
|
+
var a=rules.analytics||{};
|
|
1217
|
+
purgeSet(a.exact||[],a.prefix||[]);
|
|
1218
|
+
}
|
|
1219
|
+
if(!st.marketing){
|
|
1220
|
+
var m=rules.marketing||{};
|
|
1221
|
+
purgeSet(m.exact||[],m.prefix||[]);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
function deniedState(){
|
|
1225
|
+
return {v:"",essential:true,functional:false,analytics:false,marketing:false,gpc:gpcOn()};
|
|
1226
|
+
}
|
|
871
1227
|
function emit(st){
|
|
872
1228
|
listeners.slice().forEach(function(fn){try{fn(st);}catch(e){}});
|
|
873
1229
|
try{document.dispatchEvent(new CustomEvent("emprivacy:change",{detail:st}));}catch(e){}
|
|
@@ -884,8 +1240,7 @@ if(/%0d|%0a|%09|%0b|%0c|%20/i.test(raw))return null;
|
|
|
884
1240
|
var u=new URL(raw);
|
|
885
1241
|
if(u.protocol!=="https:")return null;
|
|
886
1242
|
if(u.username||u.password)return null;
|
|
887
|
-
|
|
888
|
-
if(h==="localhost"||h.endsWith(".localhost")||h==="127.0.0.1"||h==="::1")return null;
|
|
1243
|
+
if(hostBlocked(u.hostname))return null;
|
|
889
1244
|
return u;
|
|
890
1245
|
}catch(e){return null;}
|
|
891
1246
|
}
|
|
@@ -893,7 +1248,6 @@ function hostAllowed(u,allowPresets){
|
|
|
893
1248
|
var h=u.hostname.toLowerCase();
|
|
894
1249
|
if(allowPresets&&PRESET_HOSTS[h])return true;
|
|
895
1250
|
var list=C.scriptHostAllowlist||[];
|
|
896
|
-
if(!list.length)return true;
|
|
897
1251
|
return list.indexOf(h)>=0;
|
|
898
1252
|
}
|
|
899
1253
|
function safeScriptUrl(raw,allowPresets){
|
|
@@ -903,7 +1257,7 @@ if(!hostAllowed(u,!!allowPresets))return null;
|
|
|
903
1257
|
return u.href;
|
|
904
1258
|
}
|
|
905
1259
|
function allowedIframe(src){return YT_IFRAME.test(src)||VIMEO_IFRAME.test(src);}
|
|
906
|
-
function allowedLink(src
|
|
1260
|
+
function allowedLink(src){
|
|
907
1261
|
var u=parseHttps(src);
|
|
908
1262
|
if(!u)return false;
|
|
909
1263
|
var h=u.hostname.toLowerCase();
|
|
@@ -916,20 +1270,22 @@ return /^\\/profile\\/[^/]+\\/post\\/[^/]+\\/?$/.test(u.pathname);
|
|
|
916
1270
|
if(h==="gist.github.com"){
|
|
917
1271
|
return /^\\/[A-Za-z0-9-]{1,39}\\/[a-f0-9]{8,64}\\/?$/i.test(u.pathname);
|
|
918
1272
|
}
|
|
919
|
-
|
|
920
|
-
return
|
|
1273
|
+
var list=C.embedHostAllowlist||[];
|
|
1274
|
+
return list.indexOf(h)>=0;
|
|
921
1275
|
}
|
|
922
1276
|
function alreadyScriptSrc(u){
|
|
923
1277
|
try{
|
|
924
1278
|
return Array.prototype.some.call(document.getElementsByTagName("script"),function(s){return s.src===u;});
|
|
925
1279
|
}catch(e){return false;}
|
|
926
1280
|
}
|
|
927
|
-
function loadScript(src, attrs, integrity, allowPresets){
|
|
1281
|
+
function loadScript(src, attrs, integrity, allowPresets, requireIntegrity){
|
|
928
1282
|
var safe=safeScriptUrl(src,allowPresets);
|
|
929
1283
|
if(!safe||alreadyScriptSrc(safe))return;
|
|
1284
|
+
var sriOk=integrity&&/^sha(?:256|384|512)-[A-Za-z0-9+/=]+$/.test(integrity);
|
|
1285
|
+
if(requireIntegrity&&!sriOk)return;
|
|
930
1286
|
var e=document.createElement("script");
|
|
931
1287
|
e.src=safe;e.async=true;e.referrerPolicy="no-referrer-when-downgrade";
|
|
932
|
-
if(
|
|
1288
|
+
if(sriOk){
|
|
933
1289
|
e.integrity=integrity;
|
|
934
1290
|
e.crossOrigin="anonymous";
|
|
935
1291
|
}
|
|
@@ -951,10 +1307,10 @@ e.setAttribute("data-cf-beacon",JSON.stringify({token:token}));
|
|
|
951
1307
|
e.referrerPolicy="no-referrer-when-downgrade";
|
|
952
1308
|
document.head.appendChild(e);
|
|
953
1309
|
}
|
|
954
|
-
function applyAnalytics(){
|
|
1310
|
+
function applyAnalytics(st){
|
|
955
1311
|
var L=C.loader||{type:"none"};
|
|
956
1312
|
if(L.type==="cloudflare") loadCloudflareBeacon(L.token);
|
|
957
|
-
else if(L.type==="custom") (L.scripts||[]).forEach(function(s){loadScript(s.src,null,s.integrity,false);});
|
|
1313
|
+
else if(L.type==="custom") (L.scripts||[]).forEach(function(s){loadScript(s.src,null,s.integrity,false,true);});
|
|
958
1314
|
else if(L.type==="plausible") loadScript(L.src,{"data-domain":L.domain},null,true);
|
|
959
1315
|
else if(L.type==="fathom") loadScript(L.src,{"data-site":L.siteId},null,true);
|
|
960
1316
|
else if(L.type==="umami") loadScript(L.src,{"data-website-id":L.websiteId},null,false);
|
|
@@ -966,20 +1322,48 @@ loadScript("https://www.googletagmanager.com/gtag/js?id="+encodeURIComponent(L.m
|
|
|
966
1322
|
window.gtag("js",new Date());
|
|
967
1323
|
window.gtag("config",L.measurementId);
|
|
968
1324
|
}
|
|
1325
|
+
else if(L.type==="clarity"){
|
|
1326
|
+
var id=String(L.projectId||"");
|
|
1327
|
+
if(!/^[a-z0-9]{7,20}$/i.test(id))return;
|
|
1328
|
+
window.clarity=window.clarity||function(){(window.clarity.q=window.clarity.q||[]).push(arguments);};
|
|
1329
|
+
window.clarity("consentv2",{ad_Storage:st.marketing?"granted":"denied",analytics_Storage:"granted"});
|
|
1330
|
+
loadScript("https://www.clarity.ms/tag/"+encodeURIComponent(id),null,null,true);
|
|
1331
|
+
}
|
|
969
1332
|
}
|
|
970
1333
|
function applyMarketing(){
|
|
971
1334
|
var urls=C.marketingScripts||[];
|
|
972
|
-
urls.forEach(function(s){loadScript(s.src,null,s.integrity,false);});
|
|
1335
|
+
urls.forEach(function(s){loadScript(s.src,null,s.integrity,false,true);});
|
|
973
1336
|
var L=C.loader||{type:"none"};
|
|
974
1337
|
if(L.type==="gtm"){
|
|
975
1338
|
window.dataLayer=window.dataLayer||[];
|
|
976
1339
|
window.dataLayer.push({"gtm.start":Date.now(),event:"gtm.js"});
|
|
977
1340
|
loadScript("https://www.googletagmanager.com/gtm.js?id="+encodeURIComponent(L.containerId),null,null,true);
|
|
978
1341
|
}
|
|
1342
|
+
else if(L.type==="uet"){
|
|
1343
|
+
var id=String(L.tagId||"");
|
|
1344
|
+
if(!/^[0-9]{6,12}$/.test(id))return;
|
|
1345
|
+
window.uetq=window.uetq||[];
|
|
1346
|
+
window.uetq.push("consent","update",{"ad_storage":"granted"});
|
|
1347
|
+
var src="https://bat.bing.com/bat.js";
|
|
1348
|
+
if(alreadyScriptSrc(src))return;
|
|
1349
|
+
var n=document.createElement("script");
|
|
1350
|
+
n.src=src;
|
|
1351
|
+
n.async=true;
|
|
1352
|
+
n.onload=function(){
|
|
1353
|
+
try{
|
|
1354
|
+
var q=window.uetq;
|
|
1355
|
+
window.uetq=new window.UET({ti:id,enableAutoSpaTracking:false,q:q});
|
|
1356
|
+
window.uetq.push("pageLoad");
|
|
1357
|
+
}catch(e){}
|
|
1358
|
+
};
|
|
1359
|
+
document.head.appendChild(n);
|
|
1360
|
+
}
|
|
979
1361
|
}
|
|
980
1362
|
function applyScripts(st){
|
|
981
|
-
|
|
982
|
-
|
|
1363
|
+
var eff=effective(st)||st;
|
|
1364
|
+
purgeDenied(eff);
|
|
1365
|
+
if(eff.analytics) applyAnalytics(eff);
|
|
1366
|
+
if(eff.marketing) applyMarketing();
|
|
983
1367
|
}
|
|
984
1368
|
function gtagUpdate(st){
|
|
985
1369
|
if(!C.googleConsentMode||!window.gtag)return;
|
|
@@ -999,7 +1383,7 @@ fetch(C.recordPath,{
|
|
|
999
1383
|
method:"POST",
|
|
1000
1384
|
credentials:"same-origin",
|
|
1001
1385
|
headers:{"Content-Type":"application/json"},
|
|
1002
|
-
body:JSON.stringify({policyVersion:C.policyVersion,functional:!!st.functional,analytics:!!st.analytics,marketing:!!st.marketing})
|
|
1386
|
+
body:JSON.stringify({policyVersion:C.policyVersion,functional:!!st.functional,analytics:!!st.analytics,marketing:!!st.marketing,gpc:!!st.gpc})
|
|
1003
1387
|
}).catch(function(){});
|
|
1004
1388
|
}
|
|
1005
1389
|
function hide(el){if(el)el.setAttribute("hidden","");el&&(el.style.display="none");}
|
|
@@ -1014,17 +1398,17 @@ var f=document.createElement("iframe");
|
|
|
1014
1398
|
f.src=src;
|
|
1015
1399
|
f.setAttribute("loading","lazy");
|
|
1016
1400
|
f.setAttribute("referrerpolicy","strict-origin-when-cross-origin");
|
|
1017
|
-
f.setAttribute("allow"
|
|
1401
|
+
f.setAttribute("allow",${JSON.stringify(EMBED_IFRAME_ALLOW)});
|
|
1018
1402
|
f.setAttribute("allowfullscreen","");
|
|
1019
|
-
f.setAttribute("sandbox"
|
|
1403
|
+
f.setAttribute("sandbox",${JSON.stringify(EMBED_IFRAME_SANDBOX)});
|
|
1020
1404
|
f.setAttribute("title",box.getAttribute("data-label")||"Embed");
|
|
1021
1405
|
f.style.width="100%";
|
|
1022
1406
|
f.style.aspectRatio="16/9";
|
|
1023
1407
|
f.style.border="0";
|
|
1024
1408
|
box.replaceWith(f);
|
|
1025
1409
|
}
|
|
1026
|
-
function mountLink(box,src,label
|
|
1027
|
-
if(!allowedLink(src
|
|
1410
|
+
function mountLink(box,src,label){
|
|
1411
|
+
if(!allowedLink(src))return;
|
|
1028
1412
|
var a=document.createElement("a");
|
|
1029
1413
|
a.href=src;
|
|
1030
1414
|
a.rel="noopener noreferrer nofollow";
|
|
@@ -1043,13 +1427,13 @@ if(loadBtn) loadBtn.textContent=C.ui.loadEmbed;
|
|
|
1043
1427
|
if(!embedAllowed(st))return;
|
|
1044
1428
|
var src=box.getAttribute("data-src")||"";
|
|
1045
1429
|
var mode=box.getAttribute("data-mode")||"";
|
|
1046
|
-
var kind=box.getAttribute("data-emprivacy-embed")||"";
|
|
1047
1430
|
if(mode==="iframe") mountIframe(box,src);
|
|
1048
|
-
else mountLink(box,src,box.getAttribute("data-label")||""
|
|
1431
|
+
else mountLink(box,src,box.getAttribute("data-label")||"");
|
|
1049
1432
|
});
|
|
1050
1433
|
}
|
|
1051
1434
|
function persist(a,m,f,opts){
|
|
1052
|
-
|
|
1435
|
+
if(gpcOn())m=false;
|
|
1436
|
+
var st={v:C.policyVersion,essential:true,functional:!!f,analytics:!!a,marketing:!!m,gpc:gpcOn()};
|
|
1053
1437
|
writeCookie(JSON.stringify({v:st.v,a:st.analytics?1:0,m:st.marketing?1:0,f:st.functional?1:0}));
|
|
1054
1438
|
logServer(st);
|
|
1055
1439
|
gtagUpdate(st);
|
|
@@ -1163,18 +1547,52 @@ ic.appendChild(document.createTextNode("🍪"));
|
|
|
1163
1547
|
trigger.appendChild(ic);
|
|
1164
1548
|
function showTrigger(){trigger.removeAttribute("hidden");}
|
|
1165
1549
|
function hideTrigger(){trigger.setAttribute("hidden","");}
|
|
1550
|
+
function focusables(bar){
|
|
1551
|
+
return Array.prototype.filter.call(bar.querySelectorAll("a[href],button:not([disabled]),input:not([disabled]),summary"),function(el){
|
|
1552
|
+
return !el.closest("[hidden]");
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
function bindDialog(bar,isReopen){
|
|
1556
|
+
bar.tabIndex=-1;
|
|
1557
|
+
function closeReopen(){
|
|
1558
|
+
if(bar.parentNode)bar.parentNode.removeChild(bar);
|
|
1559
|
+
showTrigger();
|
|
1560
|
+
try{trigger.focus();}catch(e){}
|
|
1561
|
+
}
|
|
1562
|
+
bar.addEventListener("keydown",function(ev){
|
|
1563
|
+
if(ev.key==="Escape"){
|
|
1564
|
+
if(!isReopen)return;
|
|
1565
|
+
ev.preventDefault();
|
|
1566
|
+
closeReopen();
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
if(ev.key!=="Tab")return;
|
|
1570
|
+
var nodes=focusables(bar);
|
|
1571
|
+
if(!nodes.length){ev.preventDefault();return;}
|
|
1572
|
+
var first=nodes[0],last=nodes[nodes.length-1],active=document.activeElement;
|
|
1573
|
+
if(ev.shiftKey){
|
|
1574
|
+
if(active===first||active===bar){ev.preventDefault();last.focus();}
|
|
1575
|
+
}else if(active===last){
|
|
1576
|
+
ev.preventDefault();first.focus();
|
|
1577
|
+
}
|
|
1578
|
+
});
|
|
1579
|
+
if(isReopen) bar.__emprivacyClose=closeReopen;
|
|
1580
|
+
try{bar.focus();}catch(e){}
|
|
1581
|
+
}
|
|
1166
1582
|
function buildPanel(isReopen,hint){
|
|
1167
1583
|
var st=parseState(readCookie());
|
|
1168
1584
|
var aOn=st?!!st.analytics:(C.strictDefaults?false:true);
|
|
1169
|
-
var mOn=st?!!st.marketing:(C.strictDefaults?false:true);
|
|
1585
|
+
var mOn=gpcOn()?false:(st?!!st.marketing:(C.strictDefaults?false:true));
|
|
1170
1586
|
var fOn=st?!!st.functional:(C.strictDefaults?false:true);
|
|
1171
1587
|
if(isReopen) hideTrigger();
|
|
1172
1588
|
var bar=document.createElement("div");
|
|
1173
|
-
bar.className="emprivacy-bar"+(isReopen?" emprivacy-bar--reopen":"");
|
|
1589
|
+
bar.className="emprivacy-bar"+(C.bannerPosition==="top"?" emprivacy-bar--top":"")+(isReopen?" emprivacy-bar--reopen":"");
|
|
1174
1590
|
bar.setAttribute("role","dialog");
|
|
1175
1591
|
bar.setAttribute("aria-modal",isReopen?"true":"false");
|
|
1176
1592
|
var title=document.createElement("h2");
|
|
1593
|
+
title.id=isReopen?"emprivacy-reopen-title":"emprivacy-banner-title";
|
|
1177
1594
|
title.className="emprivacy-title";
|
|
1595
|
+
bar.setAttribute("aria-labelledby",title.id);
|
|
1178
1596
|
title.appendChild(document.createTextNode(C.bannerTitle));
|
|
1179
1597
|
var msg=document.createElement("p");
|
|
1180
1598
|
msg.className="emprivacy-msg";
|
|
@@ -1199,6 +1617,7 @@ note.appendChild(document.createTextNode(C.ui.essentialNote));
|
|
|
1199
1617
|
var fr=mkRow(C.ui.functional,isReopen?"emprivacy-rf":"emprivacy-f",fOn,false);
|
|
1200
1618
|
var er=mkRow(C.ui.analytics,isReopen?"emprivacy-ra":"emprivacy-a",aOn,false);
|
|
1201
1619
|
var mr=mkRow(C.ui.marketing,isReopen?"emprivacy-rm":"emprivacy-m",mOn,false);
|
|
1620
|
+
if(gpcOn()){mr.box.checked=false;mr.box.disabled=true;}
|
|
1202
1621
|
opts.appendChild(ess.wrap);
|
|
1203
1622
|
opts.appendChild(note);
|
|
1204
1623
|
opts.appendChild(fr.wrap);
|
|
@@ -1238,14 +1657,21 @@ btnClose.type="button";
|
|
|
1238
1657
|
btnClose.className="emprivacy-btn";
|
|
1239
1658
|
btnClose.appendChild(document.createTextNode(C.ui.close));
|
|
1240
1659
|
btnClose.addEventListener("click",function(){
|
|
1241
|
-
if(bar.
|
|
1242
|
-
showTrigger();
|
|
1660
|
+
if(typeof bar.__emprivacyClose==="function")bar.__emprivacyClose();
|
|
1243
1661
|
});
|
|
1244
1662
|
actions.appendChild(btnClose);
|
|
1245
1663
|
}
|
|
1246
1664
|
actions.appendChild(btnSave);
|
|
1247
1665
|
bar.appendChild(title);
|
|
1248
1666
|
bar.appendChild(msg);
|
|
1667
|
+
if(gpcOn()){
|
|
1668
|
+
var gpcEl=document.createElement("p");
|
|
1669
|
+
gpcEl.className="emprivacy-note";
|
|
1670
|
+
gpcEl.id=isReopen?"emprivacy-gpc-reopen":"emprivacy-gpc-note";
|
|
1671
|
+
gpcEl.appendChild(document.createTextNode(C.ui.gpcNote||""));
|
|
1672
|
+
bar.appendChild(gpcEl);
|
|
1673
|
+
bar.setAttribute("aria-describedby",gpcEl.id);
|
|
1674
|
+
}
|
|
1249
1675
|
bar.appendChild(links);
|
|
1250
1676
|
bar.appendChild(opts);
|
|
1251
1677
|
bar.appendChild(actions);
|
|
@@ -1255,7 +1681,9 @@ function openReopenPanel(hint){
|
|
|
1255
1681
|
if(root.querySelector(".emprivacy-bar--reopen"))return;
|
|
1256
1682
|
var st=parseState(readCookie());
|
|
1257
1683
|
if(!st)return;
|
|
1258
|
-
|
|
1684
|
+
var panel=buildPanel(true,hint||"");
|
|
1685
|
+
root.appendChild(panel);
|
|
1686
|
+
bindDialog(panel,true);
|
|
1259
1687
|
}
|
|
1260
1688
|
openPanel=function(hint){
|
|
1261
1689
|
var st=parseState(readCookie());
|
|
@@ -1270,7 +1698,7 @@ note.appendChild(document.createTextNode(hint));
|
|
|
1270
1698
|
var opts=existing.querySelector(".emprivacy-opts");
|
|
1271
1699
|
if(opts){opts.insertBefore(note,opts.firstChild);show(opts);}
|
|
1272
1700
|
}
|
|
1273
|
-
try{existing.scrollIntoView({block:"end"});}catch(e){}
|
|
1701
|
+
try{existing.scrollIntoView({block:C.bannerPosition==="top"?"start":"end"});}catch(e){}
|
|
1274
1702
|
}
|
|
1275
1703
|
}
|
|
1276
1704
|
};
|
|
@@ -1294,18 +1722,21 @@ openPanel(C.ui.embedNeedConsent);
|
|
|
1294
1722
|
}
|
|
1295
1723
|
wireEmbedButtons();
|
|
1296
1724
|
if(!needBanner()){
|
|
1297
|
-
var st=
|
|
1725
|
+
var st=currentState();
|
|
1298
1726
|
if(st){
|
|
1299
1727
|
applyScripts(st);
|
|
1300
1728
|
gtagUpdate(st);
|
|
1301
1729
|
hydrateEmbeds(st);
|
|
1302
|
-
}
|
|
1730
|
+
}else purgeDenied(deniedState());
|
|
1303
1731
|
showTrigger();
|
|
1304
1732
|
trigger.addEventListener("click",function(){openReopenPanel();});
|
|
1305
1733
|
installApi();
|
|
1306
1734
|
return;
|
|
1307
1735
|
}
|
|
1308
|
-
|
|
1736
|
+
purgeDenied(deniedState());
|
|
1737
|
+
var first=buildPanel(false,"");
|
|
1738
|
+
root.appendChild(first);
|
|
1739
|
+
bindDialog(first,false);
|
|
1309
1740
|
trigger.addEventListener("click",function(){openReopenPanel();});
|
|
1310
1741
|
installApi();
|
|
1311
1742
|
}
|
|
@@ -1316,6 +1747,11 @@ else mount();
|
|
|
1316
1747
|
function buildGoogleConsentHeadScript() {
|
|
1317
1748
|
return `(function(){window.dataLayer=window.dataLayer||[];function g(){window.dataLayer.push(arguments);}window.gtag=g;g("consent","default",{"analytics_storage":"denied","ad_storage":"denied","ad_user_data":"denied","ad_personalization":"denied","functionality_storage":"denied","security_storage":"granted","personalization_storage":"denied"});})();`;
|
|
1318
1749
|
}
|
|
1750
|
+
/** Denied-by-default Microsoft signals. The tag itself loads only after the matching category is allowed. */
|
|
1751
|
+
function buildMicrosoftConsentHeadScript(kind) {
|
|
1752
|
+
if (kind === "clarity") return `(function(){window.clarity=window.clarity||function(){(window.clarity.q=window.clarity.q||[]).push(arguments);};window.clarity("consentv2",{"ad_Storage":"denied","analytics_Storage":"denied"});})();`;
|
|
1753
|
+
return `(function(){window.uetq=window.uetq||[];window.uetq.push("consent","default",{"ad_storage":"denied"});})();`;
|
|
1754
|
+
}
|
|
1319
1755
|
//#endregion
|
|
1320
1756
|
//#region src/vendors.ts
|
|
1321
1757
|
const PLAUSIBLE_SCRIPT = "https://plausible.io/js/script.js";
|
|
@@ -1328,13 +1764,16 @@ function buildAnalyticsLoader(cfg) {
|
|
|
1328
1764
|
type: "cloudflare",
|
|
1329
1765
|
token: cfg.cloudflareWebAnalyticsToken
|
|
1330
1766
|
} : { type: "none" };
|
|
1331
|
-
case "custom":
|
|
1332
|
-
|
|
1333
|
-
scripts: cfg.analyticsScriptUrls.map((src) => ({
|
|
1767
|
+
case "custom": {
|
|
1768
|
+
const scripts = cfg.analyticsScriptUrls.filter((src) => isPublishableCustomScript(src, cfg.scriptIntegrity[src], cfg.scriptHostAllowlist)).map((src) => ({
|
|
1334
1769
|
src,
|
|
1335
1770
|
integrity: cfg.scriptIntegrity[src] ?? null
|
|
1336
|
-
}))
|
|
1337
|
-
|
|
1771
|
+
}));
|
|
1772
|
+
return scripts.length ? {
|
|
1773
|
+
type: "custom",
|
|
1774
|
+
scripts
|
|
1775
|
+
} : { type: "none" };
|
|
1776
|
+
}
|
|
1338
1777
|
case "plausible": return isHostname$1(cfg.analyticsId) ? {
|
|
1339
1778
|
type: "plausible",
|
|
1340
1779
|
src: PLAUSIBLE_SCRIPT,
|
|
@@ -1345,7 +1784,10 @@ function buildAnalyticsLoader(cfg) {
|
|
|
1345
1784
|
src: FATHOM_SCRIPT,
|
|
1346
1785
|
siteId: cfg.analyticsId.trim()
|
|
1347
1786
|
} : { type: "none" };
|
|
1348
|
-
case "umami": return isUmamiId(cfg.analyticsId) && cfg.umamiScriptUrl
|
|
1787
|
+
case "umami": return isUmamiId(cfg.analyticsId) && cfg.umamiScriptUrl && isScriptHostAllowed(cfg.umamiScriptUrl, {
|
|
1788
|
+
allowlist: cfg.scriptHostAllowlist,
|
|
1789
|
+
allowPresets: false
|
|
1790
|
+
}) ? {
|
|
1349
1791
|
type: "umami",
|
|
1350
1792
|
src: cfg.umamiScriptUrl,
|
|
1351
1793
|
websiteId: cfg.analyticsId.trim()
|
|
@@ -1362,6 +1804,14 @@ function buildAnalyticsLoader(cfg) {
|
|
|
1362
1804
|
type: "gtm",
|
|
1363
1805
|
containerId: cfg.analyticsId.trim()
|
|
1364
1806
|
} : { type: "none" };
|
|
1807
|
+
case "clarity": return isClarityId(cfg.analyticsId) ? {
|
|
1808
|
+
type: "clarity",
|
|
1809
|
+
projectId: cfg.analyticsId.trim()
|
|
1810
|
+
} : { type: "none" };
|
|
1811
|
+
case "uet": return isUetId(cfg.analyticsId) ? {
|
|
1812
|
+
type: "uet",
|
|
1813
|
+
tagId: cfg.analyticsId.trim()
|
|
1814
|
+
} : { type: "none" };
|
|
1365
1815
|
default: return { type: "none" };
|
|
1366
1816
|
}
|
|
1367
1817
|
}
|
|
@@ -1428,6 +1878,20 @@ function buildVendorList(cfg) {
|
|
|
1428
1878
|
purpose: "Loads GTM only after Marketing consent (containers often fire ads/remarketing). You remain responsible for tags inside the container.",
|
|
1429
1879
|
policyUrl: "https://policies.google.com/privacy"
|
|
1430
1880
|
});
|
|
1881
|
+
else if (loader.type === "clarity") rows.push({
|
|
1882
|
+
id: "clarity",
|
|
1883
|
+
name: "Microsoft Clarity",
|
|
1884
|
+
category: "analytics",
|
|
1885
|
+
purpose: "Clarity loads only after Analytics consent. Advertising storage stays denied until Marketing is also allowed. The project ID is not shown here.",
|
|
1886
|
+
policyUrl: "https://www.microsoft.com/privacy"
|
|
1887
|
+
});
|
|
1888
|
+
else if (loader.type === "uet") rows.push({
|
|
1889
|
+
id: "uet",
|
|
1890
|
+
name: "Microsoft Advertising (UET)",
|
|
1891
|
+
category: "marketing",
|
|
1892
|
+
purpose: "The UET tag loads only after Marketing consent. Until then, advertising storage stays denied and the tag is not injected.",
|
|
1893
|
+
policyUrl: "https://www.microsoft.com/privacy"
|
|
1894
|
+
});
|
|
1431
1895
|
else if (loader.type === "custom") for (const [i, script] of loader.scripts.entries()) {
|
|
1432
1896
|
let host = "custom script";
|
|
1433
1897
|
try {
|
|
@@ -1463,11 +1927,13 @@ function buildVendorList(cfg) {
|
|
|
1463
1927
|
}
|
|
1464
1928
|
//#endregion
|
|
1465
1929
|
//#region src/version.ts
|
|
1466
|
-
const VERSION = "3.
|
|
1930
|
+
const VERSION = "3.2.0";
|
|
1467
1931
|
//#endregion
|
|
1468
1932
|
//#region src/runtime.ts
|
|
1469
|
-
/** Soft cap for anonymous consent POSTs per client
|
|
1933
|
+
/** Soft cap for anonymous consent POSTs per trusted client address per UTC hour. */
|
|
1470
1934
|
const RECORD_RATE_LIMIT = 30;
|
|
1935
|
+
/** Site-wide cap so one client cannot fill the consent log in a single hour. */
|
|
1936
|
+
const RECORD_GLOBAL_LIMIT = 60;
|
|
1471
1937
|
const RECORD_LOG_CAP = 500;
|
|
1472
1938
|
const ADMIN_SETTINGS_PATH = "/settings";
|
|
1473
1939
|
const SAVE_ACTION_ID = "emprivacy-save";
|
|
@@ -1475,8 +1941,10 @@ const recordInput = z.object({
|
|
|
1475
1941
|
policyVersion: z.string().trim().min(1).max(64),
|
|
1476
1942
|
functional: z.boolean(),
|
|
1477
1943
|
analytics: z.boolean(),
|
|
1478
|
-
marketing: z.boolean()
|
|
1944
|
+
marketing: z.boolean(),
|
|
1945
|
+
gpc: z.boolean().optional()
|
|
1479
1946
|
});
|
|
1947
|
+
const EXPORT_PATH = `/_emdash/api/plugins/${PLUGIN_ID}/consent-export`;
|
|
1480
1948
|
async function loadConfig(ctx) {
|
|
1481
1949
|
const raw = await ctx.kv.get(KV_KEY);
|
|
1482
1950
|
if (!raw) return normalizeConfig({});
|
|
@@ -1502,28 +1970,17 @@ function absolutePolicyHref(stored, ctx) {
|
|
|
1502
1970
|
return null;
|
|
1503
1971
|
}
|
|
1504
1972
|
}
|
|
1505
|
-
function clientFingerKey(request) {
|
|
1506
|
-
const cf = request.headers.get("cf-connecting-ip")?.trim();
|
|
1507
|
-
const xff = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
|
|
1508
|
-
const ua = (request.headers.get("user-agent") ?? "").slice(0, 120);
|
|
1509
|
-
return `${cf || xff || "unknown"}|${ua}`;
|
|
1510
|
-
}
|
|
1511
1973
|
async function hashFinger(s) {
|
|
1512
1974
|
const data = new TextEncoder().encode(`emprivacy-rl:${s}`);
|
|
1513
1975
|
const dig = await crypto.subtle.digest("SHA-256", data);
|
|
1514
1976
|
return Array.from(new Uint8Array(dig)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 32);
|
|
1515
1977
|
}
|
|
1516
|
-
async function allowRecordWrite(ctx
|
|
1517
|
-
const
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
await ctx.kv.set(key, String(n + 1));
|
|
1523
|
-
return true;
|
|
1524
|
-
} catch {
|
|
1525
|
-
return false;
|
|
1526
|
-
}
|
|
1978
|
+
async function allowRecordWrite(ctx) {
|
|
1979
|
+
const hour = rateHourKey();
|
|
1980
|
+
const ip = ctx.requestMeta?.ip?.trim() ?? "";
|
|
1981
|
+
const bucket = ip ? `ip:${await hashFinger(ip)}` : "untrusted";
|
|
1982
|
+
if (!await tryConsumeRateSlot(ctx.kv, `consent:rl:${hour}:global`, RECORD_GLOBAL_LIMIT)) return false;
|
|
1983
|
+
return tryConsumeRateSlot(ctx.kv, `consent:rl:${hour}:${bucket}`, RECORD_RATE_LIMIT);
|
|
1527
1984
|
}
|
|
1528
1985
|
function publicRuntime(cfg, ctx, page) {
|
|
1529
1986
|
const copy = resolvePublicCopy(cfg, page.locale);
|
|
@@ -1541,14 +1998,16 @@ function publicRuntime(cfg, ctx, page) {
|
|
|
1541
1998
|
embedCategory: cfg.embedCategory,
|
|
1542
1999
|
gateEmbeds: cfg.gateEmbeds,
|
|
1543
2000
|
hideBanner,
|
|
2001
|
+
bannerPosition: cfg.bannerPosition,
|
|
1544
2002
|
theme: cfg.theme,
|
|
1545
2003
|
ui: copy.ui,
|
|
1546
2004
|
loader: buildAnalyticsLoader(cfg),
|
|
1547
|
-
marketingScripts: cfg.marketingScriptUrls.map((src) => ({
|
|
2005
|
+
marketingScripts: cfg.marketingScriptUrls.filter((src) => isPublishableCustomScript(src, cfg.scriptIntegrity[src], cfg.scriptHostAllowlist)).map((src) => ({
|
|
1548
2006
|
src,
|
|
1549
2007
|
integrity: cfg.scriptIntegrity[src] ?? null
|
|
1550
2008
|
})),
|
|
1551
2009
|
scriptHostAllowlist: cfg.scriptHostAllowlist,
|
|
2010
|
+
embedHostAllowlist: cfg.embedHostAllowlist,
|
|
1552
2011
|
scriptIntegrity: cfg.scriptIntegrity,
|
|
1553
2012
|
cookieMaxAge: cookieMaxAgeSeconds(cfg.cookieMaxAgeDays),
|
|
1554
2013
|
vendors: buildVendorList(cfg)
|
|
@@ -1556,8 +2015,9 @@ function publicRuntime(cfg, ctx, page) {
|
|
|
1556
2015
|
}
|
|
1557
2016
|
function themeStyle() {
|
|
1558
2017
|
return `<style id="emprivacy-style">
|
|
1559
|
-
#emprivacy-root{font-family:system-ui,sans-serif;font-size:14px;--emprivacy-bg:#
|
|
1560
|
-
.emprivacy-bar{position:fixed;z-index:99999;
|
|
2018
|
+
#emprivacy-root{font-family:system-ui,sans-serif;font-size:14px;--emprivacy-bg:#0f172a;--emprivacy-text:#f8fafc;--emprivacy-accent:#3b82f6;--emprivacy-radius:8px}
|
|
2019
|
+
.emprivacy-bar{position:fixed;z-index:99999;inset-inline:0;bottom:0;background:var(--emprivacy-bg);color:var(--emprivacy-text);padding:16px 20px max(20px, env(safe-area-inset-bottom));box-shadow:0 -4px 24px rgba(0,0,0,.25);max-height:min(45vh, 100dvh);overflow:auto}
|
|
2020
|
+
.emprivacy-bar--top{top:0;bottom:auto;padding:max(16px, env(safe-area-inset-top)) 20px 16px;box-shadow:0 4px 24px rgba(0,0,0,.25)}
|
|
1561
2021
|
.emprivacy-title{margin:0 0 8px;font-size:1.1rem}
|
|
1562
2022
|
.emprivacy-msg,.emprivacy-note{margin:0 0 10px;line-height:1.4;opacity:.95}
|
|
1563
2023
|
.emprivacy-note{font-size:.85rem}
|
|
@@ -1566,6 +2026,8 @@ function themeStyle() {
|
|
|
1566
2026
|
.emprivacy-switch{display:block;margin:6px 0}
|
|
1567
2027
|
.emprivacy-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
|
|
1568
2028
|
.emprivacy-btn{border-radius:var(--emprivacy-radius);border:1px solid color-mix(in srgb,var(--emprivacy-text) 35%,transparent);background:transparent;color:var(--emprivacy-text);padding:8px 12px;cursor:pointer}
|
|
2029
|
+
.emprivacy-btn:focus-visible,.emprivacy-switch input:focus-visible,.emprivacy-links a:focus-visible,.emprivacy-vendors summary:focus-visible,.emprivacy-bar:focus-visible{outline:2px solid var(--emprivacy-accent);outline-offset:2px}
|
|
2030
|
+
.emprivacy-btn-primary:focus-visible{outline-color:#fff}
|
|
1569
2031
|
.emprivacy-btn-primary{background:var(--emprivacy-accent);border-color:var(--emprivacy-accent);color:#fff}
|
|
1570
2032
|
.emprivacy-cookie-trigger{position:fixed;z-index:99998;left:16px;bottom:16px;width:48px;height:48px;border-radius:50%;border:1px solid color-mix(in srgb,var(--emprivacy-text) 35%,transparent);background:var(--emprivacy-bg);color:var(--emprivacy-text);box-shadow:0 2px 12px rgba(0,0,0,.2);cursor:pointer;padding:0;display:flex;align-items:center;justify-content:center}
|
|
1571
2033
|
.emprivacy-cookie-trigger:hover{filter:brightness(1.1)}
|
|
@@ -1636,6 +2098,12 @@ function createPlugin() {
|
|
|
1636
2098
|
key: "emprivacy:gcm-default",
|
|
1637
2099
|
code: buildGoogleConsentHeadScript()
|
|
1638
2100
|
});
|
|
2101
|
+
if (cfg.analyticsProvider === "clarity" || cfg.analyticsProvider === "uet") headScripts.push({
|
|
2102
|
+
kind: "inline-script",
|
|
2103
|
+
placement: "head",
|
|
2104
|
+
key: "emprivacy:ms-consent-default",
|
|
2105
|
+
code: buildMicrosoftConsentHeadScript(cfg.analyticsProvider)
|
|
2106
|
+
});
|
|
1639
2107
|
const boot = {
|
|
1640
2108
|
kind: "inline-script",
|
|
1641
2109
|
placement: "body:end",
|
|
@@ -1652,6 +2120,7 @@ function createPlugin() {
|
|
|
1652
2120
|
},
|
|
1653
2121
|
routes: {
|
|
1654
2122
|
admin: { handler: async (ctx) => {
|
|
2123
|
+
if (!ctx.user) return new Response("unauthorized", { status: 401 });
|
|
1655
2124
|
const interaction = ctx.input;
|
|
1656
2125
|
if (interaction?.type === "page_load" && interaction.page === ADMIN_SETTINGS_PATH) return buildSettingsPage(ctx);
|
|
1657
2126
|
if (interaction?.type === "form_submit" && interaction.action_id === SAVE_ACTION_ID) {
|
|
@@ -1677,12 +2146,15 @@ function createPlugin() {
|
|
|
1677
2146
|
analyticsUrlsText: String(v.analytics_urls ?? ""),
|
|
1678
2147
|
marketingUrlsText: String(v.marketing_urls ?? ""),
|
|
1679
2148
|
scriptHostAllowlistText: String(v.script_host_allowlist ?? ""),
|
|
2149
|
+
embedHostAllowlistText: String(v.embed_host_allowlist ?? ""),
|
|
1680
2150
|
cookieMaxAgeDays: String(v.cookie_max_age_days ?? "180"),
|
|
1681
2151
|
googleConsentMode: asBool(v.google_cm),
|
|
1682
2152
|
logConsentToServer: asBool(v.log_server),
|
|
1683
2153
|
embedCategory: String(v.embed_category ?? "marketing"),
|
|
1684
2154
|
gateEmbeds: asBool(v.gate_embeds),
|
|
1685
2155
|
hideBannerOnPolicyPages: asBool(v.hide_on_policy),
|
|
2156
|
+
bannerPosition: String(v.banner_position ?? "bottom"),
|
|
2157
|
+
themeProfile: String(v.theme_profile ?? "slate"),
|
|
1686
2158
|
defaultLocale: String(v.default_locale ?? "en"),
|
|
1687
2159
|
localeOverridesText: String(v.locale_overrides ?? ""),
|
|
1688
2160
|
themeBg: String(v.theme_bg ?? ""),
|
|
@@ -1732,13 +2204,15 @@ function createPlugin() {
|
|
|
1732
2204
|
};
|
|
1733
2205
|
const input = ctx.input;
|
|
1734
2206
|
if (input.policyVersion !== cfg.policyVersion) return new Response("policy_version_mismatch", { status: 400 });
|
|
1735
|
-
if (!await allowRecordWrite(ctx
|
|
2207
|
+
if (!await allowRecordWrite(ctx)) return new Response("rate_limited", { status: 429 });
|
|
2208
|
+
const gpc = input.gpc === true;
|
|
1736
2209
|
const row = {
|
|
1737
2210
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1738
2211
|
policyVersion: input.policyVersion,
|
|
1739
2212
|
functional: input.functional,
|
|
1740
2213
|
analytics: input.analytics,
|
|
1741
|
-
marketing: input.marketing
|
|
2214
|
+
marketing: gpc ? false : input.marketing,
|
|
2215
|
+
...gpc ? { gpc: true } : {}
|
|
1742
2216
|
};
|
|
1743
2217
|
try {
|
|
1744
2218
|
if ((await ctx.storage.consentEvents.query({
|
|
@@ -1758,6 +2232,29 @@ function createPlugin() {
|
|
|
1758
2232
|
return { ok: true };
|
|
1759
2233
|
}
|
|
1760
2234
|
},
|
|
2235
|
+
"consent-export": { handler: async (ctx) => {
|
|
2236
|
+
if (ctx.request.method !== "GET") return new Response("method_not_allowed", { status: 405 });
|
|
2237
|
+
if (!ctx.user) return new Response("unauthorized", { status: 401 });
|
|
2238
|
+
let rows = [];
|
|
2239
|
+
try {
|
|
2240
|
+
rows = (await ctx.storage.consentEvents.query({
|
|
2241
|
+
orderBy: { createdAt: "desc" },
|
|
2242
|
+
limit: RECORD_LOG_CAP
|
|
2243
|
+
})).items.map((item) => item.data);
|
|
2244
|
+
} catch {
|
|
2245
|
+
return new Response("export_unavailable", { status: 503 });
|
|
2246
|
+
}
|
|
2247
|
+
const body = buildConsentCsv(rows);
|
|
2248
|
+
return new Response(body, {
|
|
2249
|
+
status: 200,
|
|
2250
|
+
headers: {
|
|
2251
|
+
"Content-Type": "text/csv; charset=utf-8",
|
|
2252
|
+
"Content-Disposition": "attachment; filename=\"emprivacy-consent.csv\"",
|
|
2253
|
+
"Cache-Control": "private, no-store",
|
|
2254
|
+
"X-Content-Type-Options": "nosniff"
|
|
2255
|
+
}
|
|
2256
|
+
});
|
|
2257
|
+
} },
|
|
1761
2258
|
vendors: {
|
|
1762
2259
|
public: true,
|
|
1763
2260
|
handler: async (ctx) => {
|
|
@@ -1770,7 +2267,7 @@ function createPlugin() {
|
|
|
1770
2267
|
}
|
|
1771
2268
|
async function buildSettingsPage(ctx) {
|
|
1772
2269
|
const [cfg, latestPublished] = await Promise.all([loadConfig(ctx), fetchLatestPublishedVersion({ fetch: ctx.http?.fetch ?? globalThis.fetch })]);
|
|
1773
|
-
const installedVersion = ctx.plugin?.version ?? "3.
|
|
2270
|
+
const installedVersion = ctx.plugin?.version ?? "3.2.0";
|
|
1774
2271
|
const analyticsText = cfg.analyticsScriptUrls.map((src) => {
|
|
1775
2272
|
const i = cfg.scriptIntegrity[src];
|
|
1776
2273
|
return i ? `${src} ${i}` : src;
|
|
@@ -1794,9 +2291,10 @@ async function buildSettingsPage(ctx) {
|
|
|
1794
2291
|
})).items.map((item) => {
|
|
1795
2292
|
const d = item.data;
|
|
1796
2293
|
const functional = typeof d.functional === "boolean" ? ` · functional ${d.functional ? "on" : "off"}` : "";
|
|
2294
|
+
const gpc = d.gpc === true ? " · GPC" : "";
|
|
1797
2295
|
return {
|
|
1798
2296
|
label: d.createdAt,
|
|
1799
|
-
value: `policy ${d.policyVersion}${functional} · analytics ${d.analytics ? "on" : "off"} · marketing ${d.marketing ? "on" : "off"}`
|
|
2297
|
+
value: `policy ${d.policyVersion}${functional} · analytics ${d.analytics ? "on" : "off"} · marketing ${d.marketing ? "on" : "off"}${gpc}`
|
|
1800
2298
|
};
|
|
1801
2299
|
});
|
|
1802
2300
|
} catch {
|
|
@@ -1893,6 +2391,19 @@ async function buildSettingsPage(ctx) {
|
|
|
1893
2391
|
label: "Hide the first-visit banner on privacy and cookie policy pages",
|
|
1894
2392
|
initial_value: cfg.hideBannerOnPolicyPages
|
|
1895
2393
|
},
|
|
2394
|
+
{
|
|
2395
|
+
type: "radio",
|
|
2396
|
+
action_id: "banner_position",
|
|
2397
|
+
label: "Banner position",
|
|
2398
|
+
options: [{
|
|
2399
|
+
value: "bottom",
|
|
2400
|
+
label: "Bottom of the viewport"
|
|
2401
|
+
}, {
|
|
2402
|
+
value: "top",
|
|
2403
|
+
label: "Top of the viewport"
|
|
2404
|
+
}],
|
|
2405
|
+
initial_value: cfg.bannerPosition
|
|
2406
|
+
},
|
|
1896
2407
|
{
|
|
1897
2408
|
type: "radio",
|
|
1898
2409
|
action_id: "analytics_platform",
|
|
@@ -1926,6 +2437,14 @@ async function buildSettingsPage(ctx) {
|
|
|
1926
2437
|
value: "gtm",
|
|
1927
2438
|
label: "Google Tag Manager (requires Marketing consent)"
|
|
1928
2439
|
},
|
|
2440
|
+
{
|
|
2441
|
+
value: "clarity",
|
|
2442
|
+
label: "Microsoft Clarity (requires Analytics consent)"
|
|
2443
|
+
},
|
|
2444
|
+
{
|
|
2445
|
+
value: "uet",
|
|
2446
|
+
label: "Microsoft UET (requires Marketing consent)"
|
|
2447
|
+
},
|
|
1929
2448
|
{
|
|
1930
2449
|
value: "none",
|
|
1931
2450
|
label: "None"
|
|
@@ -1951,8 +2470,8 @@ async function buildSettingsPage(ctx) {
|
|
|
1951
2470
|
{
|
|
1952
2471
|
type: "text_input",
|
|
1953
2472
|
action_id: "analytics_id",
|
|
1954
|
-
label: "Analytics ID (domain, site ID, G-…, or
|
|
1955
|
-
placeholder: "example.com / G-XXXX / GTM-XXXX",
|
|
2473
|
+
label: "Analytics ID (domain, site ID, G-…, GTM-…, Clarity project ID, or UET tag ID)",
|
|
2474
|
+
placeholder: "example.com / G-XXXX / GTM-XXXX / Clarity ID / UET digits",
|
|
1956
2475
|
initial_value: cfg.analyticsId
|
|
1957
2476
|
},
|
|
1958
2477
|
{
|
|
@@ -1969,7 +2488,7 @@ async function buildSettingsPage(ctx) {
|
|
|
1969
2488
|
{
|
|
1970
2489
|
type: "text_input",
|
|
1971
2490
|
action_id: "analytics_urls",
|
|
1972
|
-
label: "Custom analytics script URLs (one https URL per line
|
|
2491
|
+
label: "Custom analytics script URLs (one https URL per line, SRI required: … sha384-…)",
|
|
1973
2492
|
placeholder: "https://cdn.example/a.js sha384-…",
|
|
1974
2493
|
multiline: true,
|
|
1975
2494
|
initial_value: analyticsText,
|
|
@@ -1981,7 +2500,7 @@ async function buildSettingsPage(ctx) {
|
|
|
1981
2500
|
{
|
|
1982
2501
|
type: "text_input",
|
|
1983
2502
|
action_id: "marketing_urls",
|
|
1984
|
-
label: "Marketing script URLs (one https URL per line
|
|
2503
|
+
label: "Marketing script URLs (one https URL per line, SRI required: … sha384-…)",
|
|
1985
2504
|
placeholder: "https://… (one per line)",
|
|
1986
2505
|
multiline: true,
|
|
1987
2506
|
initial_value: marketingText
|
|
@@ -1989,11 +2508,19 @@ async function buildSettingsPage(ctx) {
|
|
|
1989
2508
|
{
|
|
1990
2509
|
type: "text_input",
|
|
1991
2510
|
action_id: "script_host_allowlist",
|
|
1992
|
-
label: "Script host allowlist (
|
|
2511
|
+
label: "Script host allowlist (required for Custom, Umami, and marketing URLs; one hostname per line)",
|
|
1993
2512
|
placeholder: "cdn.example.com\njs.stripe.com",
|
|
1994
2513
|
multiline: true,
|
|
1995
2514
|
initial_value: cfg.scriptHostAllowlist.join("\n")
|
|
1996
2515
|
},
|
|
2516
|
+
{
|
|
2517
|
+
type: "text_input",
|
|
2518
|
+
action_id: "embed_host_allowlist",
|
|
2519
|
+
label: "Embed link hosts (one hostname per line). Mastodon and link previews become links only when listed. X, Bluesky, Gist, YouTube, and Vimeo stay built in.",
|
|
2520
|
+
placeholder: "mastodon.social\nexample.com",
|
|
2521
|
+
multiline: true,
|
|
2522
|
+
initial_value: cfg.embedHostAllowlist.join("\n")
|
|
2523
|
+
},
|
|
1997
2524
|
{
|
|
1998
2525
|
type: "text_input",
|
|
1999
2526
|
action_id: "cookie_max_age_days",
|
|
@@ -2026,32 +2553,80 @@ async function buildSettingsPage(ctx) {
|
|
|
2026
2553
|
}],
|
|
2027
2554
|
initial_value: cfg.embedCategory
|
|
2028
2555
|
},
|
|
2556
|
+
{
|
|
2557
|
+
type: "radio",
|
|
2558
|
+
action_id: "theme_profile",
|
|
2559
|
+
label: "Banner theme",
|
|
2560
|
+
options: [
|
|
2561
|
+
{
|
|
2562
|
+
value: "slate",
|
|
2563
|
+
label: "Slate"
|
|
2564
|
+
},
|
|
2565
|
+
{
|
|
2566
|
+
value: "paper",
|
|
2567
|
+
label: "Paper"
|
|
2568
|
+
},
|
|
2569
|
+
{
|
|
2570
|
+
value: "ink",
|
|
2571
|
+
label: "Ink"
|
|
2572
|
+
},
|
|
2573
|
+
{
|
|
2574
|
+
value: "indigo",
|
|
2575
|
+
label: "Indigo"
|
|
2576
|
+
},
|
|
2577
|
+
{
|
|
2578
|
+
value: "primer",
|
|
2579
|
+
label: "Primer"
|
|
2580
|
+
},
|
|
2581
|
+
{
|
|
2582
|
+
value: "custom",
|
|
2583
|
+
label: "Custom colors"
|
|
2584
|
+
}
|
|
2585
|
+
],
|
|
2586
|
+
initial_value: cfg.themeProfile
|
|
2587
|
+
},
|
|
2029
2588
|
{
|
|
2030
2589
|
type: "text_input",
|
|
2031
2590
|
action_id: "theme_bg",
|
|
2032
2591
|
label: "Banner background (hex)",
|
|
2033
|
-
placeholder: "#
|
|
2034
|
-
initial_value: cfg.theme.bg
|
|
2592
|
+
placeholder: "#0f172a",
|
|
2593
|
+
initial_value: cfg.theme.bg,
|
|
2594
|
+
condition: {
|
|
2595
|
+
field: "theme_profile",
|
|
2596
|
+
eq: "custom"
|
|
2597
|
+
}
|
|
2035
2598
|
},
|
|
2036
2599
|
{
|
|
2037
2600
|
type: "text_input",
|
|
2038
2601
|
action_id: "theme_text",
|
|
2039
2602
|
label: "Banner text (hex)",
|
|
2040
|
-
placeholder: "#
|
|
2041
|
-
initial_value: cfg.theme.text
|
|
2603
|
+
placeholder: "#f8fafc",
|
|
2604
|
+
initial_value: cfg.theme.text,
|
|
2605
|
+
condition: {
|
|
2606
|
+
field: "theme_profile",
|
|
2607
|
+
eq: "custom"
|
|
2608
|
+
}
|
|
2042
2609
|
},
|
|
2043
2610
|
{
|
|
2044
2611
|
type: "text_input",
|
|
2045
2612
|
action_id: "theme_accent",
|
|
2046
2613
|
label: "Banner accent (hex)",
|
|
2047
2614
|
placeholder: "#3b82f6",
|
|
2048
|
-
initial_value: cfg.theme.accent
|
|
2615
|
+
initial_value: cfg.theme.accent,
|
|
2616
|
+
condition: {
|
|
2617
|
+
field: "theme_profile",
|
|
2618
|
+
eq: "custom"
|
|
2619
|
+
}
|
|
2049
2620
|
},
|
|
2050
2621
|
{
|
|
2051
2622
|
type: "text_input",
|
|
2052
2623
|
action_id: "theme_radius",
|
|
2053
2624
|
label: "Banner corner radius (0–24 px)",
|
|
2054
|
-
|
|
2625
|
+
condition: {
|
|
2626
|
+
field: "theme_profile",
|
|
2627
|
+
eq: "custom"
|
|
2628
|
+
},
|
|
2629
|
+
placeholder: "8",
|
|
2055
2630
|
initial_value: String(cfg.theme.radiusPx)
|
|
2056
2631
|
},
|
|
2057
2632
|
{
|
|
@@ -2079,16 +2654,20 @@ async function buildSettingsPage(ctx) {
|
|
|
2079
2654
|
type: "fields",
|
|
2080
2655
|
fields: vendorFields
|
|
2081
2656
|
},
|
|
2082
|
-
...cfg.logConsentToServer
|
|
2657
|
+
...cfg.logConsentToServer ? [
|
|
2083
2658
|
{ type: "divider" },
|
|
2084
2659
|
{
|
|
2085
2660
|
type: "header",
|
|
2086
2661
|
text: "Recent consent records"
|
|
2087
2662
|
},
|
|
2088
2663
|
{
|
|
2664
|
+
type: "context",
|
|
2665
|
+
text: `Download the log (up to ${RECORD_LOG_CAP} rows, no IP): ${EXPORT_PATH}. Signed-in admins only. Cells that start with = + - or @ are prefixed so a spreadsheet does not treat them as formulas.`
|
|
2666
|
+
},
|
|
2667
|
+
...consentFields.length > 0 ? [{
|
|
2089
2668
|
type: "fields",
|
|
2090
2669
|
fields: consentFields
|
|
2091
|
-
}
|
|
2670
|
+
}] : []
|
|
2092
2671
|
] : []
|
|
2093
2672
|
] };
|
|
2094
2673
|
}
|
|
@@ -2141,22 +2720,9 @@ const POSTER_HOSTS = /* @__PURE__ */ new Set([
|
|
|
2141
2720
|
"img.youtube.com",
|
|
2142
2721
|
"i.vimeocdn.com"
|
|
2143
2722
|
]);
|
|
2144
|
-
|
|
2145
|
-
return /[\u0000-\u001F\u007F\s]/.test(s);
|
|
2146
|
-
}
|
|
2723
|
+
/** Same non-public host rules as script URLs. */
|
|
2147
2724
|
function parseHttpsUrl(raw) {
|
|
2148
|
-
|
|
2149
|
-
if (!t || t.length > 2048 || hasUnsafeChars(t)) return null;
|
|
2150
|
-
if (/%0d|%0a|%09|%0b|%0c|%20/i.test(t)) return null;
|
|
2151
|
-
try {
|
|
2152
|
-
const u = new URL(t);
|
|
2153
|
-
if (u.protocol !== "https:") return null;
|
|
2154
|
-
if (u.username || u.password) return null;
|
|
2155
|
-
if (u.hostname === "localhost" || u.hostname.endsWith(".localhost")) return null;
|
|
2156
|
-
return u;
|
|
2157
|
-
} catch {
|
|
2158
|
-
return null;
|
|
2159
|
-
}
|
|
2725
|
+
return parseStrictHttpsUrl(raw);
|
|
2160
2726
|
}
|
|
2161
2727
|
function hostOf(u) {
|
|
2162
2728
|
return u.hostname.toLowerCase();
|