@emplugins/emprivacy 3.0.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/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: "#111111",
381
- text: "#eeeeee",
382
- accent: "#3b82f6",
383
- radiusPx: 6
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
- theme: {
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 (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.");
685
- 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.");
686
- 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.");
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
- theme: {
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=${jsonForHtmlScript(pr)};
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 currentState(){return parseState(readCookie());}
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
- if(st.analytics) applyAnalytics();
982
- if(st.marketing) applyMarketing();
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
- var st={v:C.policyVersion,essential:true,functional:!!f,analytics:!!a,marketing:!!m};
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.parentNode)bar.parentNode.removeChild(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
- root.appendChild(buildPanel(true,hint||""));
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=parseState(readCookie());
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
- root.appendChild(buildPanel(false,""));
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 = "3.0.0";
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:#111111;--emprivacy-text:#eeeeee;--emprivacy-accent:#3b82f6;--emprivacy-radius:6px}
1560
- .emprivacy-bar{position:fixed;z-index:99999;left:0;right:0;bottom:0;background:var(--emprivacy-bg);color:var(--emprivacy-text);padding:16px 20px 20px;box-shadow:0 -4px 24px rgba(0,0,0,.25);max-height:45vh;overflow:auto}
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 ?? "3.0.0";
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 GTM-…)",
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: "#111111",
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: "#eeeeee",
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
- placeholder: "6",
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 && consentFields.length > 0 ? [
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
  }