@emplugins/emprivacy 2.1.0 → 3.1.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 +13 -7
- package/dist/index.d.mts +56 -2
- package/dist/index.mjs +503 -55
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/GETTING_STARTED.md +7 -5
- package/docs/PLUGIN_SETTINGS.md +42 -5
- package/docs/RELEASES.md +2 -2
- package/docs/RELEASE_NOTES.md +20 -1
- package/docs/TESTING.md +9 -7
- package/package.json +3 -3
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,6 +263,12 @@ 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
|
|
258
274
|
/** Hosts EmPrivacy may load as <script src> for built-in presets. */
|
|
@@ -262,7 +278,10 @@ const PRESET_SCRIPT_HOSTS = /* @__PURE__ */ new Set([
|
|
|
262
278
|
"cdn.usefathom.com",
|
|
263
279
|
"scripts.simpleanalyticscdn.com",
|
|
264
280
|
"www.googletagmanager.com",
|
|
265
|
-
"googletagmanager.com"
|
|
281
|
+
"googletagmanager.com",
|
|
282
|
+
"www.clarity.ms",
|
|
283
|
+
"clarity.ms",
|
|
284
|
+
"bat.bing.com"
|
|
266
285
|
]);
|
|
267
286
|
const HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i;
|
|
268
287
|
function normalizeCookieMaxAgeDays(n, fallback = 180) {
|
|
@@ -376,12 +395,81 @@ function rateHourKey(now = /* @__PURE__ */ new Date()) {
|
|
|
376
395
|
//#region src/theme.ts
|
|
377
396
|
/** Hex colors only — rejects CSS injection via theme fields. */
|
|
378
397
|
const HEX = /^#(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
|
|
398
|
+
/** Fixed banner palettes. Colors are constants, never taken from the admin form. */
|
|
399
|
+
const THEME_PROFILES = {
|
|
400
|
+
slate: {
|
|
401
|
+
bg: "#0f172a",
|
|
402
|
+
text: "#f8fafc",
|
|
403
|
+
accent: "#3b82f6",
|
|
404
|
+
radiusPx: 8
|
|
405
|
+
},
|
|
406
|
+
paper: {
|
|
407
|
+
bg: "#f7f6f3",
|
|
408
|
+
text: "#37352f",
|
|
409
|
+
accent: "#1d4ed8",
|
|
410
|
+
radiusPx: 6
|
|
411
|
+
},
|
|
412
|
+
ink: {
|
|
413
|
+
bg: "#000000",
|
|
414
|
+
text: "#ededed",
|
|
415
|
+
accent: "#0070f3",
|
|
416
|
+
radiusPx: 8
|
|
417
|
+
},
|
|
418
|
+
indigo: {
|
|
419
|
+
bg: "#ffffff",
|
|
420
|
+
text: "#0f172a",
|
|
421
|
+
accent: "#4f46e5",
|
|
422
|
+
radiusPx: 12
|
|
423
|
+
},
|
|
424
|
+
primer: {
|
|
425
|
+
bg: "#f6f8fa",
|
|
426
|
+
text: "#1f2328",
|
|
427
|
+
accent: "#0969da",
|
|
428
|
+
radiusPx: 6
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
const THEME_PROFILE_IDS = [
|
|
432
|
+
"slate",
|
|
433
|
+
"paper",
|
|
434
|
+
"ink",
|
|
435
|
+
"indigo",
|
|
436
|
+
"primer"
|
|
437
|
+
];
|
|
438
|
+
/** First profile. New installs and configs with no theme choice use this. */
|
|
439
|
+
const DEFAULT_THEME_PROFILE = THEME_PROFILE_IDS[0];
|
|
379
440
|
const DEFAULT_THEME = {
|
|
380
|
-
bg:
|
|
381
|
-
text:
|
|
382
|
-
accent:
|
|
383
|
-
radiusPx:
|
|
441
|
+
bg: THEME_PROFILES[DEFAULT_THEME_PROFILE].bg,
|
|
442
|
+
text: THEME_PROFILES[DEFAULT_THEME_PROFILE].text,
|
|
443
|
+
accent: THEME_PROFILES[DEFAULT_THEME_PROFILE].accent,
|
|
444
|
+
radiusPx: THEME_PROFILES[DEFAULT_THEME_PROFILE].radiusPx
|
|
384
445
|
};
|
|
446
|
+
function isThemeProfileId(value) {
|
|
447
|
+
return THEME_PROFILE_IDS.includes(value);
|
|
448
|
+
}
|
|
449
|
+
/** Stored JSON: a missing or unknown choice uses the first profile. Explicit `custom` is kept. */
|
|
450
|
+
function normalizeThemeChoice(value) {
|
|
451
|
+
if (value === "custom") return "custom";
|
|
452
|
+
if (typeof value === "string" && isThemeProfileId(value)) return value;
|
|
453
|
+
return DEFAULT_THEME_PROFILE;
|
|
454
|
+
}
|
|
455
|
+
/** Admin save: blank uses the first profile. `custom` is the manual option. */
|
|
456
|
+
function parseThemeChoice(value) {
|
|
457
|
+
const t = value.trim().toLowerCase();
|
|
458
|
+
if (!t) return DEFAULT_THEME_PROFILE;
|
|
459
|
+
if (t === "custom") return "custom";
|
|
460
|
+
if (isThemeProfileId(t)) return t;
|
|
461
|
+
throw new Error("Banner theme must be Custom, Slate, Paper, Ink, Indigo, or Primer.");
|
|
462
|
+
}
|
|
463
|
+
function themeForChoice(choice, custom) {
|
|
464
|
+
if (choice === "custom") return custom;
|
|
465
|
+
const profile = THEME_PROFILES[choice];
|
|
466
|
+
return {
|
|
467
|
+
bg: profile.bg,
|
|
468
|
+
text: profile.text,
|
|
469
|
+
accent: profile.accent,
|
|
470
|
+
radiusPx: profile.radiusPx
|
|
471
|
+
};
|
|
472
|
+
}
|
|
385
473
|
function isSafeHexColor(s) {
|
|
386
474
|
return HEX.test(s.trim());
|
|
387
475
|
}
|
|
@@ -430,8 +518,10 @@ const DEFAULT_CONFIG = {
|
|
|
430
518
|
embedCategory: "marketing",
|
|
431
519
|
gateEmbeds: true,
|
|
432
520
|
hideBannerOnPolicyPages: true,
|
|
521
|
+
bannerPosition: "bottom",
|
|
433
522
|
defaultLocale: "en",
|
|
434
523
|
localeOverrides: {},
|
|
524
|
+
themeProfile: "slate",
|
|
435
525
|
theme: { ...DEFAULT_THEME }
|
|
436
526
|
};
|
|
437
527
|
const ANALYTICS_PROVIDERS = [
|
|
@@ -443,8 +533,13 @@ const ANALYTICS_PROVIDERS = [
|
|
|
443
533
|
"umami",
|
|
444
534
|
"simpleanalytics",
|
|
445
535
|
"ga4",
|
|
446
|
-
"gtm"
|
|
536
|
+
"gtm",
|
|
537
|
+
"clarity",
|
|
538
|
+
"uet"
|
|
447
539
|
];
|
|
540
|
+
function normalizeBannerPosition(value) {
|
|
541
|
+
return value === "top" ? "top" : "bottom";
|
|
542
|
+
}
|
|
448
543
|
function hasUnsafeUrlChars(s) {
|
|
449
544
|
return /[\u0000-\u001F\u007F\s]/.test(s);
|
|
450
545
|
}
|
|
@@ -582,14 +677,16 @@ function normalizeConfig(raw) {
|
|
|
582
677
|
embedCategory: isEmbedCategory(embedRaw) ? embedRaw : DEFAULT_CONFIG.embedCategory,
|
|
583
678
|
gateEmbeds: typeof o.gateEmbeds === "boolean" ? o.gateEmbeds : DEFAULT_CONFIG.gateEmbeds,
|
|
584
679
|
hideBannerOnPolicyPages: typeof o.hideBannerOnPolicyPages === "boolean" ? o.hideBannerOnPolicyPages : DEFAULT_CONFIG.hideBannerOnPolicyPages,
|
|
680
|
+
bannerPosition: normalizeBannerPosition(o.bannerPosition),
|
|
585
681
|
defaultLocale: typeof o.defaultLocale === "string" && o.defaultLocale ? o.defaultLocale : DEFAULT_CONFIG.defaultLocale,
|
|
586
682
|
localeOverrides: parseLocaleOverrides(o.localeOverrides),
|
|
587
|
-
|
|
683
|
+
themeProfile: normalizeThemeChoice(o.themeProfile),
|
|
684
|
+
theme: themeForChoice(normalizeThemeChoice(o.themeProfile), {
|
|
588
685
|
bg: normalizeHexColor(typeof themeRaw.bg === "string" ? themeRaw.bg : "", DEFAULT_THEME.bg),
|
|
589
686
|
text: normalizeHexColor(typeof themeRaw.text === "string" ? themeRaw.text : "", DEFAULT_THEME.text),
|
|
590
687
|
accent: normalizeHexColor(typeof themeRaw.accent === "string" ? themeRaw.accent : "", DEFAULT_THEME.accent),
|
|
591
688
|
radiusPx: normalizeRadiusPx(themeRaw.radiusPx, DEFAULT_THEME.radiusPx)
|
|
592
|
-
}
|
|
689
|
+
})
|
|
593
690
|
};
|
|
594
691
|
}
|
|
595
692
|
function assertAnalyticsId(provider, id, umamiScriptUrl) {
|
|
@@ -615,6 +712,14 @@ function assertAnalyticsId(provider, id, umamiScriptUrl) {
|
|
|
615
712
|
if (!isGtmId(t)) throw new Error("Google Tag Manager ID must look like GTM-XXXX.");
|
|
616
713
|
return;
|
|
617
714
|
}
|
|
715
|
+
if (provider === "clarity") {
|
|
716
|
+
if (!isClarityId(t)) throw new Error("Clarity project ID must be 7–20 letters or digits.");
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (provider === "uet") {
|
|
720
|
+
if (!isUetId(t)) throw new Error("UET tag ID must be 6–12 digits.");
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
618
723
|
if (provider === "simpleanalytics" && t && !isHostname$1(t)) throw new Error("Simple Analytics hostname must be a domain like example.com, or empty.");
|
|
619
724
|
}
|
|
620
725
|
/** Validate admin-submitted fields; throws an Error with a short message on failure */
|
|
@@ -673,17 +778,22 @@ function assertValidSavedConfig(input) {
|
|
|
673
778
|
if (!isEmbedCategory(embedCategory)) throw new Error("Embeds must require Functional or Marketing consent.");
|
|
674
779
|
const defaultLocale = input.defaultLocale.trim().toLowerCase() || "en";
|
|
675
780
|
if (!/^[a-z]{2,3}(?:-[a-z]{2})?$/.test(defaultLocale)) throw new Error("Default locale must look like en or pt-BR.");
|
|
781
|
+
const bannerPosition = input.bannerPosition.trim().toLowerCase();
|
|
782
|
+
if (bannerPosition !== "top" && bannerPosition !== "bottom") throw new Error("Banner position must be top or bottom.");
|
|
676
783
|
const localeOverrides = parseLocaleOverridesText(input.localeOverridesText);
|
|
677
784
|
for (const pack of Object.values(localeOverrides)) {
|
|
678
785
|
if (pack.bannerTitle !== void 0) assertOptionalLength("Translated banner title", pack.bannerTitle, 120);
|
|
679
786
|
if (pack.bannerMessage !== void 0) assertOptionalLength("Translated short notice", pack.bannerMessage, 600);
|
|
680
787
|
}
|
|
788
|
+
const themeProfile = parseThemeChoice(input.themeProfile);
|
|
681
789
|
const themeBg = input.themeBg.trim() || DEFAULT_THEME.bg;
|
|
682
790
|
const themeText = input.themeText.trim() || DEFAULT_THEME.text;
|
|
683
791
|
const themeAccent = input.themeAccent.trim() || DEFAULT_THEME.accent;
|
|
684
|
-
if (
|
|
685
|
-
|
|
686
|
-
|
|
792
|
+
if (themeProfile === "custom") {
|
|
793
|
+
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.");
|
|
794
|
+
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.");
|
|
795
|
+
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.");
|
|
796
|
+
}
|
|
687
797
|
return normalizeConfig({
|
|
688
798
|
bannerTitle: input.bannerTitle.trim() || DEFAULT_CONFIG.bannerTitle,
|
|
689
799
|
bannerMessage: input.bannerMessage.trim() || DEFAULT_CONFIG.bannerMessage,
|
|
@@ -705,14 +815,16 @@ function assertValidSavedConfig(input) {
|
|
|
705
815
|
embedCategory,
|
|
706
816
|
gateEmbeds: input.gateEmbeds,
|
|
707
817
|
hideBannerOnPolicyPages: input.hideBannerOnPolicyPages,
|
|
818
|
+
bannerPosition,
|
|
708
819
|
defaultLocale,
|
|
709
820
|
localeOverrides,
|
|
710
|
-
|
|
821
|
+
themeProfile,
|
|
822
|
+
theme: themeForChoice(themeProfile, themeProfile === "custom" ? {
|
|
711
823
|
bg: themeBg,
|
|
712
824
|
text: themeText,
|
|
713
825
|
accent: themeAccent,
|
|
714
826
|
radiusPx: parseRadiusInput(input.themeRadius, DEFAULT_THEME.radiusPx)
|
|
715
|
-
}
|
|
827
|
+
} : { ...DEFAULT_THEME })
|
|
716
828
|
});
|
|
717
829
|
}
|
|
718
830
|
/** Safe to embed in `<script type="application/json">` (breakout-safe) */
|
|
@@ -830,17 +942,80 @@ async function fetchLatestPublishedVersion(options) {
|
|
|
830
942
|
}
|
|
831
943
|
}
|
|
832
944
|
//#endregion
|
|
945
|
+
//#region src/consent-csv.ts
|
|
946
|
+
const FORMULA_PREFIX = /^[\t\r ]*[=+\-@]/;
|
|
947
|
+
/** Quote a cell and neutralize spreadsheet formula injection. */
|
|
948
|
+
function csvCell(value) {
|
|
949
|
+
let s = value.replace(/\r\n|\n|\r/g, " ");
|
|
950
|
+
if (FORMULA_PREFIX.test(s)) s = `'${s}`;
|
|
951
|
+
return `"${s.replace(/"/g, "\"\"")}"`;
|
|
952
|
+
}
|
|
953
|
+
function boolCell(value) {
|
|
954
|
+
return value === true ? "true" : value === false ? "false" : "";
|
|
955
|
+
}
|
|
956
|
+
function buildConsentCsv(rows) {
|
|
957
|
+
const lines = [[
|
|
958
|
+
"createdAt",
|
|
959
|
+
"policyVersion",
|
|
960
|
+
"functional",
|
|
961
|
+
"analytics",
|
|
962
|
+
"marketing",
|
|
963
|
+
"gpc"
|
|
964
|
+
].map(csvCell).join(",")];
|
|
965
|
+
for (const row of rows) lines.push([
|
|
966
|
+
csvCell(typeof row.createdAt === "string" ? row.createdAt : ""),
|
|
967
|
+
csvCell(typeof row.policyVersion === "string" ? row.policyVersion : ""),
|
|
968
|
+
csvCell(boolCell(row.functional)),
|
|
969
|
+
csvCell(boolCell(row.analytics)),
|
|
970
|
+
csvCell(boolCell(row.marketing)),
|
|
971
|
+
csvCell(row.gpc === true ? "true" : "")
|
|
972
|
+
].join(","));
|
|
973
|
+
return `\uFEFF${lines.join("\r\n")}\r\n`;
|
|
974
|
+
}
|
|
975
|
+
//#endregion
|
|
976
|
+
//#region src/cookie-purge.ts
|
|
977
|
+
const ANALYTICS_COOKIE_EXACT = [
|
|
978
|
+
"_ga",
|
|
979
|
+
"_gid",
|
|
980
|
+
"_gat"
|
|
981
|
+
];
|
|
982
|
+
const ANALYTICS_COOKIE_PREFIX = ["_ga_", "_gat_"];
|
|
983
|
+
const MARKETING_COOKIE_EXACT = [
|
|
984
|
+
"_gcl_au",
|
|
985
|
+
"_gcl_aw",
|
|
986
|
+
"_gcl_dc",
|
|
987
|
+
"_clck",
|
|
988
|
+
"_clsk",
|
|
989
|
+
"CLID",
|
|
990
|
+
"_uetsid",
|
|
991
|
+
"_uetvid"
|
|
992
|
+
];
|
|
993
|
+
const MARKETING_COOKIE_PREFIX = ["_gcl_"];
|
|
994
|
+
const PURGE_RULES = {
|
|
995
|
+
analytics: {
|
|
996
|
+
exact: [...ANALYTICS_COOKIE_EXACT],
|
|
997
|
+
prefix: [...ANALYTICS_COOKIE_PREFIX]
|
|
998
|
+
},
|
|
999
|
+
marketing: {
|
|
1000
|
+
exact: [...MARKETING_COOKIE_EXACT],
|
|
1001
|
+
prefix: [...MARKETING_COOKIE_PREFIX]
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
//#endregion
|
|
833
1005
|
//#region src/public-bootstrap.ts
|
|
834
1006
|
/**
|
|
835
1007
|
* Public-site bootstrap. Banner copy is applied with textContent / createTextNode only.
|
|
836
1008
|
* Embed/script URLs are re-validated in the browser (never trust DOM data-src alone).
|
|
837
1009
|
*/
|
|
838
1010
|
function buildBodyBootstrap(pr) {
|
|
1011
|
+
const jsonLiteral = jsonForHtmlScript(pr);
|
|
1012
|
+
const purgeLiteral = jsonForHtmlScript(PURGE_RULES);
|
|
839
1013
|
return `(function(){
|
|
840
|
-
var C=${
|
|
1014
|
+
var C=${jsonLiteral};
|
|
841
1015
|
var CN="${COOKIE_NAME}";
|
|
1016
|
+
var PURGE=${purgeLiteral};
|
|
842
1017
|
var listeners=[];
|
|
843
|
-
var PRESET_HOSTS={"static.cloudflareinsights.com":1,"plausible.io":1,"cdn.usefathom.com":1,"scripts.simpleanalyticscdn.com":1,"www.googletagmanager.com":1,"googletagmanager.com":1};
|
|
1018
|
+
var PRESET_HOSTS={"static.cloudflareinsights.com":1,"plausible.io":1,"cdn.usefathom.com":1,"scripts.simpleanalyticscdn.com":1,"www.googletagmanager.com":1,"googletagmanager.com":1,"www.clarity.ms":1,"clarity.ms":1,"bat.bing.com":1};
|
|
844
1019
|
var YT_IFRAME=/^https:\\/\\/(?:www\\.)?youtube-nocookie\\.com\\/embed\\/[A-Za-z0-9_-]{11}\\/?$/;
|
|
845
1020
|
var VIMEO_IFRAME=/^https:\\/\\/player\\.vimeo\\.com\\/video\\/\\d{6,12}\\/?$/;
|
|
846
1021
|
function readCookie(){
|
|
@@ -867,7 +1042,64 @@ if(!flagOk(o.a)||!flagOk(o.m)||!flagOk(o.f))return null;
|
|
|
867
1042
|
return {v:o.v,essential:true,functional:!!o.f,analytics:!!o.a,marketing:!!o.m};
|
|
868
1043
|
}catch(e){return null;}
|
|
869
1044
|
}
|
|
870
|
-
function
|
|
1045
|
+
function gpcOn(){
|
|
1046
|
+
try{return navigator.globalPrivacyControl===true;}catch(e){return false;}
|
|
1047
|
+
}
|
|
1048
|
+
function effective(st){
|
|
1049
|
+
if(!st)return null;
|
|
1050
|
+
var g=gpcOn();
|
|
1051
|
+
return {v:st.v,essential:true,functional:!!st.functional,analytics:!!st.analytics,marketing:g?false:!!st.marketing,gpc:g};
|
|
1052
|
+
}
|
|
1053
|
+
function currentState(){return effective(parseState(readCookie()));}
|
|
1054
|
+
function cookieNameOk(name){return /^[A-Za-z0-9_]{1,64}$/.test(name);}
|
|
1055
|
+
function expireCookie(name){
|
|
1056
|
+
if(!cookieNameOk(name)||name===CN)return;
|
|
1057
|
+
var secure=document.location.protocol==="https:"?"; Secure":"";
|
|
1058
|
+
var base=name+"=; Max-Age=0; Path=/; SameSite=Lax"+secure;
|
|
1059
|
+
document.cookie=base;
|
|
1060
|
+
var host=String(document.location.hostname||"");
|
|
1061
|
+
if(!/^[A-Za-z0-9.-]{1,253}$/.test(host)||host.indexOf("..")>=0)return;
|
|
1062
|
+
var hosts=[host];
|
|
1063
|
+
var labels=host.split(".");
|
|
1064
|
+
if(labels.length>=3)hosts.push(labels.slice(1).join("."));
|
|
1065
|
+
for(var i=0;i<hosts.length;i++){
|
|
1066
|
+
var h=hosts[i];
|
|
1067
|
+
if(!/^[A-Za-z0-9.-]{1,253}$/.test(h))continue;
|
|
1068
|
+
document.cookie=base+"; Domain="+h;
|
|
1069
|
+
document.cookie=base+"; Domain=."+h;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function purgeSet(exact,prefixes){
|
|
1073
|
+
var raw="";
|
|
1074
|
+
try{raw=document.cookie;}catch(e){return;}
|
|
1075
|
+
if(!raw)return;
|
|
1076
|
+
if(raw.length>8192)raw=raw.slice(0,8192);
|
|
1077
|
+
raw.split(";").forEach(function(part){
|
|
1078
|
+
var name=(part.split("=")[0]||"").replace(/^\\s+/,"");
|
|
1079
|
+
if(!cookieNameOk(name)||name===CN)return;
|
|
1080
|
+
var hit=exact.indexOf(name)>=0;
|
|
1081
|
+
if(!hit){
|
|
1082
|
+
for(var i=0;i<prefixes.length;i++){
|
|
1083
|
+
if(prefixes[i]&&name.indexOf(prefixes[i])===0){hit=true;break;}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if(hit)expireCookie(name);
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
function purgeDenied(st){
|
|
1090
|
+
var rules=PURGE||{};
|
|
1091
|
+
if(!st.analytics){
|
|
1092
|
+
var a=rules.analytics||{};
|
|
1093
|
+
purgeSet(a.exact||[],a.prefix||[]);
|
|
1094
|
+
}
|
|
1095
|
+
if(!st.marketing){
|
|
1096
|
+
var m=rules.marketing||{};
|
|
1097
|
+
purgeSet(m.exact||[],m.prefix||[]);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
function deniedState(){
|
|
1101
|
+
return {v:"",essential:true,functional:false,analytics:false,marketing:false,gpc:gpcOn()};
|
|
1102
|
+
}
|
|
871
1103
|
function emit(st){
|
|
872
1104
|
listeners.slice().forEach(function(fn){try{fn(st);}catch(e){}});
|
|
873
1105
|
try{document.dispatchEvent(new CustomEvent("emprivacy:change",{detail:st}));}catch(e){}
|
|
@@ -951,7 +1183,7 @@ e.setAttribute("data-cf-beacon",JSON.stringify({token:token}));
|
|
|
951
1183
|
e.referrerPolicy="no-referrer-when-downgrade";
|
|
952
1184
|
document.head.appendChild(e);
|
|
953
1185
|
}
|
|
954
|
-
function applyAnalytics(){
|
|
1186
|
+
function applyAnalytics(st){
|
|
955
1187
|
var L=C.loader||{type:"none"};
|
|
956
1188
|
if(L.type==="cloudflare") loadCloudflareBeacon(L.token);
|
|
957
1189
|
else if(L.type==="custom") (L.scripts||[]).forEach(function(s){loadScript(s.src,null,s.integrity,false);});
|
|
@@ -966,6 +1198,13 @@ loadScript("https://www.googletagmanager.com/gtag/js?id="+encodeURIComponent(L.m
|
|
|
966
1198
|
window.gtag("js",new Date());
|
|
967
1199
|
window.gtag("config",L.measurementId);
|
|
968
1200
|
}
|
|
1201
|
+
else if(L.type==="clarity"){
|
|
1202
|
+
var id=String(L.projectId||"");
|
|
1203
|
+
if(!/^[a-z0-9]{7,20}$/i.test(id))return;
|
|
1204
|
+
window.clarity=window.clarity||function(){(window.clarity.q=window.clarity.q||[]).push(arguments);};
|
|
1205
|
+
window.clarity("consentv2",{ad_Storage:st.marketing?"granted":"denied",analytics_Storage:"granted"});
|
|
1206
|
+
loadScript("https://www.clarity.ms/tag/"+encodeURIComponent(id),null,null,true);
|
|
1207
|
+
}
|
|
969
1208
|
}
|
|
970
1209
|
function applyMarketing(){
|
|
971
1210
|
var urls=C.marketingScripts||[];
|
|
@@ -976,10 +1215,31 @@ window.dataLayer=window.dataLayer||[];
|
|
|
976
1215
|
window.dataLayer.push({"gtm.start":Date.now(),event:"gtm.js"});
|
|
977
1216
|
loadScript("https://www.googletagmanager.com/gtm.js?id="+encodeURIComponent(L.containerId),null,null,true);
|
|
978
1217
|
}
|
|
1218
|
+
else if(L.type==="uet"){
|
|
1219
|
+
var id=String(L.tagId||"");
|
|
1220
|
+
if(!/^[0-9]{6,12}$/.test(id))return;
|
|
1221
|
+
window.uetq=window.uetq||[];
|
|
1222
|
+
window.uetq.push("consent","update",{"ad_storage":"granted"});
|
|
1223
|
+
var src="https://bat.bing.com/bat.js";
|
|
1224
|
+
if(alreadyScriptSrc(src))return;
|
|
1225
|
+
var n=document.createElement("script");
|
|
1226
|
+
n.src=src;
|
|
1227
|
+
n.async=true;
|
|
1228
|
+
n.onload=function(){
|
|
1229
|
+
try{
|
|
1230
|
+
var q=window.uetq;
|
|
1231
|
+
window.uetq=new window.UET({ti:id,enableAutoSpaTracking:false,q:q});
|
|
1232
|
+
window.uetq.push("pageLoad");
|
|
1233
|
+
}catch(e){}
|
|
1234
|
+
};
|
|
1235
|
+
document.head.appendChild(n);
|
|
1236
|
+
}
|
|
979
1237
|
}
|
|
980
1238
|
function applyScripts(st){
|
|
981
|
-
|
|
982
|
-
|
|
1239
|
+
var eff=effective(st)||st;
|
|
1240
|
+
purgeDenied(eff);
|
|
1241
|
+
if(eff.analytics) applyAnalytics(eff);
|
|
1242
|
+
if(eff.marketing) applyMarketing();
|
|
983
1243
|
}
|
|
984
1244
|
function gtagUpdate(st){
|
|
985
1245
|
if(!C.googleConsentMode||!window.gtag)return;
|
|
@@ -999,7 +1259,7 @@ fetch(C.recordPath,{
|
|
|
999
1259
|
method:"POST",
|
|
1000
1260
|
credentials:"same-origin",
|
|
1001
1261
|
headers:{"Content-Type":"application/json"},
|
|
1002
|
-
body:JSON.stringify({policyVersion:C.policyVersion,functional:!!st.functional,analytics:!!st.analytics,marketing:!!st.marketing})
|
|
1262
|
+
body:JSON.stringify({policyVersion:C.policyVersion,functional:!!st.functional,analytics:!!st.analytics,marketing:!!st.marketing,gpc:!!st.gpc})
|
|
1003
1263
|
}).catch(function(){});
|
|
1004
1264
|
}
|
|
1005
1265
|
function hide(el){if(el)el.setAttribute("hidden","");el&&(el.style.display="none");}
|
|
@@ -1049,7 +1309,8 @@ else mountLink(box,src,box.getAttribute("data-label")||"",kind);
|
|
|
1049
1309
|
});
|
|
1050
1310
|
}
|
|
1051
1311
|
function persist(a,m,f,opts){
|
|
1052
|
-
|
|
1312
|
+
if(gpcOn())m=false;
|
|
1313
|
+
var st={v:C.policyVersion,essential:true,functional:!!f,analytics:!!a,marketing:!!m,gpc:gpcOn()};
|
|
1053
1314
|
writeCookie(JSON.stringify({v:st.v,a:st.analytics?1:0,m:st.marketing?1:0,f:st.functional?1:0}));
|
|
1054
1315
|
logServer(st);
|
|
1055
1316
|
gtagUpdate(st);
|
|
@@ -1163,18 +1424,52 @@ ic.appendChild(document.createTextNode("🍪"));
|
|
|
1163
1424
|
trigger.appendChild(ic);
|
|
1164
1425
|
function showTrigger(){trigger.removeAttribute("hidden");}
|
|
1165
1426
|
function hideTrigger(){trigger.setAttribute("hidden","");}
|
|
1427
|
+
function focusables(bar){
|
|
1428
|
+
return Array.prototype.filter.call(bar.querySelectorAll("a[href],button:not([disabled]),input:not([disabled]),summary"),function(el){
|
|
1429
|
+
return !el.closest("[hidden]");
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
function bindDialog(bar,isReopen){
|
|
1433
|
+
bar.tabIndex=-1;
|
|
1434
|
+
function closeReopen(){
|
|
1435
|
+
if(bar.parentNode)bar.parentNode.removeChild(bar);
|
|
1436
|
+
showTrigger();
|
|
1437
|
+
try{trigger.focus();}catch(e){}
|
|
1438
|
+
}
|
|
1439
|
+
bar.addEventListener("keydown",function(ev){
|
|
1440
|
+
if(ev.key==="Escape"){
|
|
1441
|
+
if(!isReopen)return;
|
|
1442
|
+
ev.preventDefault();
|
|
1443
|
+
closeReopen();
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
if(ev.key!=="Tab")return;
|
|
1447
|
+
var nodes=focusables(bar);
|
|
1448
|
+
if(!nodes.length){ev.preventDefault();return;}
|
|
1449
|
+
var first=nodes[0],last=nodes[nodes.length-1],active=document.activeElement;
|
|
1450
|
+
if(ev.shiftKey){
|
|
1451
|
+
if(active===first||active===bar){ev.preventDefault();last.focus();}
|
|
1452
|
+
}else if(active===last){
|
|
1453
|
+
ev.preventDefault();first.focus();
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
if(isReopen) bar.__emprivacyClose=closeReopen;
|
|
1457
|
+
try{bar.focus();}catch(e){}
|
|
1458
|
+
}
|
|
1166
1459
|
function buildPanel(isReopen,hint){
|
|
1167
1460
|
var st=parseState(readCookie());
|
|
1168
1461
|
var aOn=st?!!st.analytics:(C.strictDefaults?false:true);
|
|
1169
|
-
var mOn=st?!!st.marketing:(C.strictDefaults?false:true);
|
|
1462
|
+
var mOn=gpcOn()?false:(st?!!st.marketing:(C.strictDefaults?false:true));
|
|
1170
1463
|
var fOn=st?!!st.functional:(C.strictDefaults?false:true);
|
|
1171
1464
|
if(isReopen) hideTrigger();
|
|
1172
1465
|
var bar=document.createElement("div");
|
|
1173
|
-
bar.className="emprivacy-bar"+(isReopen?" emprivacy-bar--reopen":"");
|
|
1466
|
+
bar.className="emprivacy-bar"+(C.bannerPosition==="top"?" emprivacy-bar--top":"")+(isReopen?" emprivacy-bar--reopen":"");
|
|
1174
1467
|
bar.setAttribute("role","dialog");
|
|
1175
1468
|
bar.setAttribute("aria-modal",isReopen?"true":"false");
|
|
1176
1469
|
var title=document.createElement("h2");
|
|
1470
|
+
title.id=isReopen?"emprivacy-reopen-title":"emprivacy-banner-title";
|
|
1177
1471
|
title.className="emprivacy-title";
|
|
1472
|
+
bar.setAttribute("aria-labelledby",title.id);
|
|
1178
1473
|
title.appendChild(document.createTextNode(C.bannerTitle));
|
|
1179
1474
|
var msg=document.createElement("p");
|
|
1180
1475
|
msg.className="emprivacy-msg";
|
|
@@ -1199,6 +1494,7 @@ note.appendChild(document.createTextNode(C.ui.essentialNote));
|
|
|
1199
1494
|
var fr=mkRow(C.ui.functional,isReopen?"emprivacy-rf":"emprivacy-f",fOn,false);
|
|
1200
1495
|
var er=mkRow(C.ui.analytics,isReopen?"emprivacy-ra":"emprivacy-a",aOn,false);
|
|
1201
1496
|
var mr=mkRow(C.ui.marketing,isReopen?"emprivacy-rm":"emprivacy-m",mOn,false);
|
|
1497
|
+
if(gpcOn()){mr.box.checked=false;mr.box.disabled=true;}
|
|
1202
1498
|
opts.appendChild(ess.wrap);
|
|
1203
1499
|
opts.appendChild(note);
|
|
1204
1500
|
opts.appendChild(fr.wrap);
|
|
@@ -1238,14 +1534,21 @@ btnClose.type="button";
|
|
|
1238
1534
|
btnClose.className="emprivacy-btn";
|
|
1239
1535
|
btnClose.appendChild(document.createTextNode(C.ui.close));
|
|
1240
1536
|
btnClose.addEventListener("click",function(){
|
|
1241
|
-
if(bar.
|
|
1242
|
-
showTrigger();
|
|
1537
|
+
if(typeof bar.__emprivacyClose==="function")bar.__emprivacyClose();
|
|
1243
1538
|
});
|
|
1244
1539
|
actions.appendChild(btnClose);
|
|
1245
1540
|
}
|
|
1246
1541
|
actions.appendChild(btnSave);
|
|
1247
1542
|
bar.appendChild(title);
|
|
1248
1543
|
bar.appendChild(msg);
|
|
1544
|
+
if(gpcOn()){
|
|
1545
|
+
var gpcEl=document.createElement("p");
|
|
1546
|
+
gpcEl.className="emprivacy-note";
|
|
1547
|
+
gpcEl.id=isReopen?"emprivacy-gpc-reopen":"emprivacy-gpc-note";
|
|
1548
|
+
gpcEl.appendChild(document.createTextNode(C.ui.gpcNote||""));
|
|
1549
|
+
bar.appendChild(gpcEl);
|
|
1550
|
+
bar.setAttribute("aria-describedby",gpcEl.id);
|
|
1551
|
+
}
|
|
1249
1552
|
bar.appendChild(links);
|
|
1250
1553
|
bar.appendChild(opts);
|
|
1251
1554
|
bar.appendChild(actions);
|
|
@@ -1255,7 +1558,9 @@ function openReopenPanel(hint){
|
|
|
1255
1558
|
if(root.querySelector(".emprivacy-bar--reopen"))return;
|
|
1256
1559
|
var st=parseState(readCookie());
|
|
1257
1560
|
if(!st)return;
|
|
1258
|
-
|
|
1561
|
+
var panel=buildPanel(true,hint||"");
|
|
1562
|
+
root.appendChild(panel);
|
|
1563
|
+
bindDialog(panel,true);
|
|
1259
1564
|
}
|
|
1260
1565
|
openPanel=function(hint){
|
|
1261
1566
|
var st=parseState(readCookie());
|
|
@@ -1270,7 +1575,7 @@ note.appendChild(document.createTextNode(hint));
|
|
|
1270
1575
|
var opts=existing.querySelector(".emprivacy-opts");
|
|
1271
1576
|
if(opts){opts.insertBefore(note,opts.firstChild);show(opts);}
|
|
1272
1577
|
}
|
|
1273
|
-
try{existing.scrollIntoView({block:"end"});}catch(e){}
|
|
1578
|
+
try{existing.scrollIntoView({block:C.bannerPosition==="top"?"start":"end"});}catch(e){}
|
|
1274
1579
|
}
|
|
1275
1580
|
}
|
|
1276
1581
|
};
|
|
@@ -1294,18 +1599,21 @@ openPanel(C.ui.embedNeedConsent);
|
|
|
1294
1599
|
}
|
|
1295
1600
|
wireEmbedButtons();
|
|
1296
1601
|
if(!needBanner()){
|
|
1297
|
-
var st=
|
|
1602
|
+
var st=currentState();
|
|
1298
1603
|
if(st){
|
|
1299
1604
|
applyScripts(st);
|
|
1300
1605
|
gtagUpdate(st);
|
|
1301
1606
|
hydrateEmbeds(st);
|
|
1302
|
-
}
|
|
1607
|
+
}else purgeDenied(deniedState());
|
|
1303
1608
|
showTrigger();
|
|
1304
1609
|
trigger.addEventListener("click",function(){openReopenPanel();});
|
|
1305
1610
|
installApi();
|
|
1306
1611
|
return;
|
|
1307
1612
|
}
|
|
1308
|
-
|
|
1613
|
+
purgeDenied(deniedState());
|
|
1614
|
+
var first=buildPanel(false,"");
|
|
1615
|
+
root.appendChild(first);
|
|
1616
|
+
bindDialog(first,false);
|
|
1309
1617
|
trigger.addEventListener("click",function(){openReopenPanel();});
|
|
1310
1618
|
installApi();
|
|
1311
1619
|
}
|
|
@@ -1316,6 +1624,11 @@ else mount();
|
|
|
1316
1624
|
function buildGoogleConsentHeadScript() {
|
|
1317
1625
|
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
1626
|
}
|
|
1627
|
+
/** Denied-by-default Microsoft signals. The tag itself loads only after the matching category is allowed. */
|
|
1628
|
+
function buildMicrosoftConsentHeadScript(kind) {
|
|
1629
|
+
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"});})();`;
|
|
1630
|
+
return `(function(){window.uetq=window.uetq||[];window.uetq.push("consent","default",{"ad_storage":"denied"});})();`;
|
|
1631
|
+
}
|
|
1319
1632
|
//#endregion
|
|
1320
1633
|
//#region src/vendors.ts
|
|
1321
1634
|
const PLAUSIBLE_SCRIPT = "https://plausible.io/js/script.js";
|
|
@@ -1362,6 +1675,14 @@ function buildAnalyticsLoader(cfg) {
|
|
|
1362
1675
|
type: "gtm",
|
|
1363
1676
|
containerId: cfg.analyticsId.trim()
|
|
1364
1677
|
} : { type: "none" };
|
|
1678
|
+
case "clarity": return isClarityId(cfg.analyticsId) ? {
|
|
1679
|
+
type: "clarity",
|
|
1680
|
+
projectId: cfg.analyticsId.trim()
|
|
1681
|
+
} : { type: "none" };
|
|
1682
|
+
case "uet": return isUetId(cfg.analyticsId) ? {
|
|
1683
|
+
type: "uet",
|
|
1684
|
+
tagId: cfg.analyticsId.trim()
|
|
1685
|
+
} : { type: "none" };
|
|
1365
1686
|
default: return { type: "none" };
|
|
1366
1687
|
}
|
|
1367
1688
|
}
|
|
@@ -1428,6 +1749,20 @@ function buildVendorList(cfg) {
|
|
|
1428
1749
|
purpose: "Loads GTM only after Marketing consent (containers often fire ads/remarketing). You remain responsible for tags inside the container.",
|
|
1429
1750
|
policyUrl: "https://policies.google.com/privacy"
|
|
1430
1751
|
});
|
|
1752
|
+
else if (loader.type === "clarity") rows.push({
|
|
1753
|
+
id: "clarity",
|
|
1754
|
+
name: "Microsoft Clarity",
|
|
1755
|
+
category: "analytics",
|
|
1756
|
+
purpose: "Clarity loads only after Analytics consent. Advertising storage stays denied until Marketing is also allowed. The project ID is not shown here.",
|
|
1757
|
+
policyUrl: "https://www.microsoft.com/privacy"
|
|
1758
|
+
});
|
|
1759
|
+
else if (loader.type === "uet") rows.push({
|
|
1760
|
+
id: "uet",
|
|
1761
|
+
name: "Microsoft Advertising (UET)",
|
|
1762
|
+
category: "marketing",
|
|
1763
|
+
purpose: "The UET tag loads only after Marketing consent. Until then, advertising storage stays denied and the tag is not injected.",
|
|
1764
|
+
policyUrl: "https://www.microsoft.com/privacy"
|
|
1765
|
+
});
|
|
1431
1766
|
else if (loader.type === "custom") for (const [i, script] of loader.scripts.entries()) {
|
|
1432
1767
|
let host = "custom script";
|
|
1433
1768
|
try {
|
|
@@ -1463,7 +1798,7 @@ function buildVendorList(cfg) {
|
|
|
1463
1798
|
}
|
|
1464
1799
|
//#endregion
|
|
1465
1800
|
//#region src/version.ts
|
|
1466
|
-
const VERSION = "
|
|
1801
|
+
const VERSION = "3.1.0";
|
|
1467
1802
|
//#endregion
|
|
1468
1803
|
//#region src/runtime.ts
|
|
1469
1804
|
/** Soft cap for anonymous consent POSTs per client fingerprint per UTC hour. */
|
|
@@ -1475,8 +1810,10 @@ const recordInput = z.object({
|
|
|
1475
1810
|
policyVersion: z.string().trim().min(1).max(64),
|
|
1476
1811
|
functional: z.boolean(),
|
|
1477
1812
|
analytics: z.boolean(),
|
|
1478
|
-
marketing: z.boolean()
|
|
1813
|
+
marketing: z.boolean(),
|
|
1814
|
+
gpc: z.boolean().optional()
|
|
1479
1815
|
});
|
|
1816
|
+
const EXPORT_PATH = `/_emdash/api/plugins/${PLUGIN_ID}/consent-export`;
|
|
1480
1817
|
async function loadConfig(ctx) {
|
|
1481
1818
|
const raw = await ctx.kv.get(KV_KEY);
|
|
1482
1819
|
if (!raw) return normalizeConfig({});
|
|
@@ -1541,6 +1878,7 @@ function publicRuntime(cfg, ctx, page) {
|
|
|
1541
1878
|
embedCategory: cfg.embedCategory,
|
|
1542
1879
|
gateEmbeds: cfg.gateEmbeds,
|
|
1543
1880
|
hideBanner,
|
|
1881
|
+
bannerPosition: cfg.bannerPosition,
|
|
1544
1882
|
theme: cfg.theme,
|
|
1545
1883
|
ui: copy.ui,
|
|
1546
1884
|
loader: buildAnalyticsLoader(cfg),
|
|
@@ -1556,8 +1894,9 @@ function publicRuntime(cfg, ctx, page) {
|
|
|
1556
1894
|
}
|
|
1557
1895
|
function themeStyle() {
|
|
1558
1896
|
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;
|
|
1897
|
+
#emprivacy-root{font-family:system-ui,sans-serif;font-size:14px;--emprivacy-bg:#0f172a;--emprivacy-text:#f8fafc;--emprivacy-accent:#3b82f6;--emprivacy-radius:8px}
|
|
1898
|
+
.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}
|
|
1899
|
+
.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
1900
|
.emprivacy-title{margin:0 0 8px;font-size:1.1rem}
|
|
1562
1901
|
.emprivacy-msg,.emprivacy-note{margin:0 0 10px;line-height:1.4;opacity:.95}
|
|
1563
1902
|
.emprivacy-note{font-size:.85rem}
|
|
@@ -1566,6 +1905,8 @@ function themeStyle() {
|
|
|
1566
1905
|
.emprivacy-switch{display:block;margin:6px 0}
|
|
1567
1906
|
.emprivacy-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
|
|
1568
1907
|
.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}
|
|
1908
|
+
.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}
|
|
1909
|
+
.emprivacy-btn-primary:focus-visible{outline-color:#fff}
|
|
1569
1910
|
.emprivacy-btn-primary{background:var(--emprivacy-accent);border-color:var(--emprivacy-accent);color:#fff}
|
|
1570
1911
|
.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
1912
|
.emprivacy-cookie-trigger:hover{filter:brightness(1.1)}
|
|
@@ -1636,6 +1977,12 @@ function createPlugin() {
|
|
|
1636
1977
|
key: "emprivacy:gcm-default",
|
|
1637
1978
|
code: buildGoogleConsentHeadScript()
|
|
1638
1979
|
});
|
|
1980
|
+
if (cfg.analyticsProvider === "clarity" || cfg.analyticsProvider === "uet") headScripts.push({
|
|
1981
|
+
kind: "inline-script",
|
|
1982
|
+
placement: "head",
|
|
1983
|
+
key: "emprivacy:ms-consent-default",
|
|
1984
|
+
code: buildMicrosoftConsentHeadScript(cfg.analyticsProvider)
|
|
1985
|
+
});
|
|
1639
1986
|
const boot = {
|
|
1640
1987
|
kind: "inline-script",
|
|
1641
1988
|
placement: "body:end",
|
|
@@ -1683,6 +2030,8 @@ function createPlugin() {
|
|
|
1683
2030
|
embedCategory: String(v.embed_category ?? "marketing"),
|
|
1684
2031
|
gateEmbeds: asBool(v.gate_embeds),
|
|
1685
2032
|
hideBannerOnPolicyPages: asBool(v.hide_on_policy),
|
|
2033
|
+
bannerPosition: String(v.banner_position ?? "bottom"),
|
|
2034
|
+
themeProfile: String(v.theme_profile ?? "slate"),
|
|
1686
2035
|
defaultLocale: String(v.default_locale ?? "en"),
|
|
1687
2036
|
localeOverridesText: String(v.locale_overrides ?? ""),
|
|
1688
2037
|
themeBg: String(v.theme_bg ?? ""),
|
|
@@ -1733,12 +2082,14 @@ function createPlugin() {
|
|
|
1733
2082
|
const input = ctx.input;
|
|
1734
2083
|
if (input.policyVersion !== cfg.policyVersion) return new Response("policy_version_mismatch", { status: 400 });
|
|
1735
2084
|
if (!await allowRecordWrite(ctx, ctx.request)) return new Response("rate_limited", { status: 429 });
|
|
2085
|
+
const gpc = input.gpc === true;
|
|
1736
2086
|
const row = {
|
|
1737
2087
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1738
2088
|
policyVersion: input.policyVersion,
|
|
1739
2089
|
functional: input.functional,
|
|
1740
2090
|
analytics: input.analytics,
|
|
1741
|
-
marketing: input.marketing
|
|
2091
|
+
marketing: gpc ? false : input.marketing,
|
|
2092
|
+
...gpc ? { gpc: true } : {}
|
|
1742
2093
|
};
|
|
1743
2094
|
try {
|
|
1744
2095
|
if ((await ctx.storage.consentEvents.query({
|
|
@@ -1758,6 +2109,29 @@ function createPlugin() {
|
|
|
1758
2109
|
return { ok: true };
|
|
1759
2110
|
}
|
|
1760
2111
|
},
|
|
2112
|
+
"consent-export": { handler: async (ctx) => {
|
|
2113
|
+
if (ctx.request.method !== "GET") return new Response("method_not_allowed", { status: 405 });
|
|
2114
|
+
if (!ctx.user) return new Response("unauthorized", { status: 401 });
|
|
2115
|
+
let rows = [];
|
|
2116
|
+
try {
|
|
2117
|
+
rows = (await ctx.storage.consentEvents.query({
|
|
2118
|
+
orderBy: { createdAt: "desc" },
|
|
2119
|
+
limit: RECORD_LOG_CAP
|
|
2120
|
+
})).items.map((item) => item.data);
|
|
2121
|
+
} catch {
|
|
2122
|
+
return new Response("export_unavailable", { status: 503 });
|
|
2123
|
+
}
|
|
2124
|
+
const body = buildConsentCsv(rows);
|
|
2125
|
+
return new Response(body, {
|
|
2126
|
+
status: 200,
|
|
2127
|
+
headers: {
|
|
2128
|
+
"Content-Type": "text/csv; charset=utf-8",
|
|
2129
|
+
"Content-Disposition": "attachment; filename=\"emprivacy-consent.csv\"",
|
|
2130
|
+
"Cache-Control": "private, no-store",
|
|
2131
|
+
"X-Content-Type-Options": "nosniff"
|
|
2132
|
+
}
|
|
2133
|
+
});
|
|
2134
|
+
} },
|
|
1761
2135
|
vendors: {
|
|
1762
2136
|
public: true,
|
|
1763
2137
|
handler: async (ctx) => {
|
|
@@ -1770,7 +2144,7 @@ function createPlugin() {
|
|
|
1770
2144
|
}
|
|
1771
2145
|
async function buildSettingsPage(ctx) {
|
|
1772
2146
|
const [cfg, latestPublished] = await Promise.all([loadConfig(ctx), fetchLatestPublishedVersion({ fetch: ctx.http?.fetch ?? globalThis.fetch })]);
|
|
1773
|
-
const installedVersion = ctx.plugin?.version ?? "
|
|
2147
|
+
const installedVersion = ctx.plugin?.version ?? "3.1.0";
|
|
1774
2148
|
const analyticsText = cfg.analyticsScriptUrls.map((src) => {
|
|
1775
2149
|
const i = cfg.scriptIntegrity[src];
|
|
1776
2150
|
return i ? `${src} ${i}` : src;
|
|
@@ -1794,9 +2168,10 @@ async function buildSettingsPage(ctx) {
|
|
|
1794
2168
|
})).items.map((item) => {
|
|
1795
2169
|
const d = item.data;
|
|
1796
2170
|
const functional = typeof d.functional === "boolean" ? ` · functional ${d.functional ? "on" : "off"}` : "";
|
|
2171
|
+
const gpc = d.gpc === true ? " · GPC" : "";
|
|
1797
2172
|
return {
|
|
1798
2173
|
label: d.createdAt,
|
|
1799
|
-
value: `policy ${d.policyVersion}${functional} · analytics ${d.analytics ? "on" : "off"} · marketing ${d.marketing ? "on" : "off"}`
|
|
2174
|
+
value: `policy ${d.policyVersion}${functional} · analytics ${d.analytics ? "on" : "off"} · marketing ${d.marketing ? "on" : "off"}${gpc}`
|
|
1800
2175
|
};
|
|
1801
2176
|
});
|
|
1802
2177
|
} catch {
|
|
@@ -1893,6 +2268,19 @@ async function buildSettingsPage(ctx) {
|
|
|
1893
2268
|
label: "Hide the first-visit banner on privacy and cookie policy pages",
|
|
1894
2269
|
initial_value: cfg.hideBannerOnPolicyPages
|
|
1895
2270
|
},
|
|
2271
|
+
{
|
|
2272
|
+
type: "radio",
|
|
2273
|
+
action_id: "banner_position",
|
|
2274
|
+
label: "Banner position",
|
|
2275
|
+
options: [{
|
|
2276
|
+
value: "bottom",
|
|
2277
|
+
label: "Bottom of the viewport"
|
|
2278
|
+
}, {
|
|
2279
|
+
value: "top",
|
|
2280
|
+
label: "Top of the viewport"
|
|
2281
|
+
}],
|
|
2282
|
+
initial_value: cfg.bannerPosition
|
|
2283
|
+
},
|
|
1896
2284
|
{
|
|
1897
2285
|
type: "radio",
|
|
1898
2286
|
action_id: "analytics_platform",
|
|
@@ -1926,6 +2314,14 @@ async function buildSettingsPage(ctx) {
|
|
|
1926
2314
|
value: "gtm",
|
|
1927
2315
|
label: "Google Tag Manager (requires Marketing consent)"
|
|
1928
2316
|
},
|
|
2317
|
+
{
|
|
2318
|
+
value: "clarity",
|
|
2319
|
+
label: "Microsoft Clarity (requires Analytics consent)"
|
|
2320
|
+
},
|
|
2321
|
+
{
|
|
2322
|
+
value: "uet",
|
|
2323
|
+
label: "Microsoft UET (requires Marketing consent)"
|
|
2324
|
+
},
|
|
1929
2325
|
{
|
|
1930
2326
|
value: "none",
|
|
1931
2327
|
label: "None"
|
|
@@ -1951,8 +2347,8 @@ async function buildSettingsPage(ctx) {
|
|
|
1951
2347
|
{
|
|
1952
2348
|
type: "text_input",
|
|
1953
2349
|
action_id: "analytics_id",
|
|
1954
|
-
label: "Analytics ID (domain, site ID, G-…, or
|
|
1955
|
-
placeholder: "example.com / G-XXXX / GTM-XXXX",
|
|
2350
|
+
label: "Analytics ID (domain, site ID, G-…, GTM-…, Clarity project ID, or UET tag ID)",
|
|
2351
|
+
placeholder: "example.com / G-XXXX / GTM-XXXX / Clarity ID / UET digits",
|
|
1956
2352
|
initial_value: cfg.analyticsId
|
|
1957
2353
|
},
|
|
1958
2354
|
{
|
|
@@ -2026,32 +2422,80 @@ async function buildSettingsPage(ctx) {
|
|
|
2026
2422
|
}],
|
|
2027
2423
|
initial_value: cfg.embedCategory
|
|
2028
2424
|
},
|
|
2425
|
+
{
|
|
2426
|
+
type: "radio",
|
|
2427
|
+
action_id: "theme_profile",
|
|
2428
|
+
label: "Banner theme",
|
|
2429
|
+
options: [
|
|
2430
|
+
{
|
|
2431
|
+
value: "slate",
|
|
2432
|
+
label: "Slate"
|
|
2433
|
+
},
|
|
2434
|
+
{
|
|
2435
|
+
value: "paper",
|
|
2436
|
+
label: "Paper"
|
|
2437
|
+
},
|
|
2438
|
+
{
|
|
2439
|
+
value: "ink",
|
|
2440
|
+
label: "Ink"
|
|
2441
|
+
},
|
|
2442
|
+
{
|
|
2443
|
+
value: "indigo",
|
|
2444
|
+
label: "Indigo"
|
|
2445
|
+
},
|
|
2446
|
+
{
|
|
2447
|
+
value: "primer",
|
|
2448
|
+
label: "Primer"
|
|
2449
|
+
},
|
|
2450
|
+
{
|
|
2451
|
+
value: "custom",
|
|
2452
|
+
label: "Custom colors"
|
|
2453
|
+
}
|
|
2454
|
+
],
|
|
2455
|
+
initial_value: cfg.themeProfile
|
|
2456
|
+
},
|
|
2029
2457
|
{
|
|
2030
2458
|
type: "text_input",
|
|
2031
2459
|
action_id: "theme_bg",
|
|
2032
2460
|
label: "Banner background (hex)",
|
|
2033
|
-
placeholder: "#
|
|
2034
|
-
initial_value: cfg.theme.bg
|
|
2461
|
+
placeholder: "#0f172a",
|
|
2462
|
+
initial_value: cfg.theme.bg,
|
|
2463
|
+
condition: {
|
|
2464
|
+
field: "theme_profile",
|
|
2465
|
+
eq: "custom"
|
|
2466
|
+
}
|
|
2035
2467
|
},
|
|
2036
2468
|
{
|
|
2037
2469
|
type: "text_input",
|
|
2038
2470
|
action_id: "theme_text",
|
|
2039
2471
|
label: "Banner text (hex)",
|
|
2040
|
-
placeholder: "#
|
|
2041
|
-
initial_value: cfg.theme.text
|
|
2472
|
+
placeholder: "#f8fafc",
|
|
2473
|
+
initial_value: cfg.theme.text,
|
|
2474
|
+
condition: {
|
|
2475
|
+
field: "theme_profile",
|
|
2476
|
+
eq: "custom"
|
|
2477
|
+
}
|
|
2042
2478
|
},
|
|
2043
2479
|
{
|
|
2044
2480
|
type: "text_input",
|
|
2045
2481
|
action_id: "theme_accent",
|
|
2046
2482
|
label: "Banner accent (hex)",
|
|
2047
2483
|
placeholder: "#3b82f6",
|
|
2048
|
-
initial_value: cfg.theme.accent
|
|
2484
|
+
initial_value: cfg.theme.accent,
|
|
2485
|
+
condition: {
|
|
2486
|
+
field: "theme_profile",
|
|
2487
|
+
eq: "custom"
|
|
2488
|
+
}
|
|
2049
2489
|
},
|
|
2050
2490
|
{
|
|
2051
2491
|
type: "text_input",
|
|
2052
2492
|
action_id: "theme_radius",
|
|
2053
2493
|
label: "Banner corner radius (0–24 px)",
|
|
2054
|
-
|
|
2494
|
+
condition: {
|
|
2495
|
+
field: "theme_profile",
|
|
2496
|
+
eq: "custom"
|
|
2497
|
+
},
|
|
2498
|
+
placeholder: "8",
|
|
2055
2499
|
initial_value: String(cfg.theme.radiusPx)
|
|
2056
2500
|
},
|
|
2057
2501
|
{
|
|
@@ -2079,16 +2523,20 @@ async function buildSettingsPage(ctx) {
|
|
|
2079
2523
|
type: "fields",
|
|
2080
2524
|
fields: vendorFields
|
|
2081
2525
|
},
|
|
2082
|
-
...cfg.logConsentToServer
|
|
2526
|
+
...cfg.logConsentToServer ? [
|
|
2083
2527
|
{ type: "divider" },
|
|
2084
2528
|
{
|
|
2085
2529
|
type: "header",
|
|
2086
2530
|
text: "Recent consent records"
|
|
2087
2531
|
},
|
|
2088
2532
|
{
|
|
2533
|
+
type: "context",
|
|
2534
|
+
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.`
|
|
2535
|
+
},
|
|
2536
|
+
...consentFields.length > 0 ? [{
|
|
2089
2537
|
type: "fields",
|
|
2090
2538
|
fields: consentFields
|
|
2091
|
-
}
|
|
2539
|
+
}] : []
|
|
2092
2540
|
] : []
|
|
2093
2541
|
] };
|
|
2094
2542
|
}
|