@emplugins/emprivacy 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -87,7 +87,7 @@ The legacy unscoped package `emprivacy` is deprecated; migrate imports to `@empl
87
87
 
88
88
  5. **Create a Privacy Policy Page** (and optional Cookie Policy Page) in EmDash, then in admin (shield → **EmPrivacy**) set the public path (`/privacy`) or an `https://` URL.
89
89
 
90
- 6. Pick an analytics preset (Cloudflare, Plausible, Fathom, Umami, Simple Analytics, GA4, GTM, None, or Custom), optional marketing script URLs, and **Save**.
90
+ 6. Pick an analytics preset (Cloudflare, Plausible, Fathom, Umami, Simple Analytics, GA4, GTM, Microsoft Clarity, Microsoft UET, None, or Custom), optional marketing script URLs, and **Save**.
91
91
 
92
92
  Visitors see a banner on first visit, when **Policy / consent version** changes, or when an older cookie is missing the functional flag (`f`). Existing `{ v, a, m }` cookies are treated as expired so visitors choose the new category.
93
93
 
@@ -97,7 +97,8 @@ After the banner script runs:
97
97
 
98
98
  ```ts
99
99
  window.emprivacy.get();
100
- // { v, essential: true, functional, analytics, marketing } | null
100
+ // { v, essential: true, functional, analytics, marketing, gpc } | null
101
+ // gpc is true when Global Privacy Control forced marketing off
101
102
 
102
103
  window.emprivacy.has("analytics"); // boolean
103
104
  window.emprivacy.open(); // reopen the panel
@@ -113,16 +114,21 @@ stop();
113
114
  |--------|-------------|
114
115
  | Categories | Essential (always on), **Functional**, **Analytics**, **Marketing** |
115
116
  | Privacy / cookie links | EmDash **Page** path or `https://` URL; resolved with `ctx.url()` |
116
- | Analytics presets | Cloudflare Web Analytics, Plausible, Fathom, Umami, Simple Analytics, GA4, **GTM (Marketing)**, None, Custom `https` URLs (+ optional SRI / host allowlist) |
117
+ | Analytics presets | Cloudflare Web Analytics, Plausible, Fathom, Umami, Simple Analytics, GA4, **GTM (Marketing)**, **Microsoft Clarity (Analytics)**, **Microsoft UET (Marketing)**, None, Custom `https` URLs (SRI and script host allowlist required) |
117
118
  | Marketing scripts | Only **https** URLs you list, injected as `<script src>` after marketing consent |
118
119
  | Official embeds | Optional placeholders for `@emdash-cms/plugin-embeds` block types; YouTube/Vimeo use allowlisted player URLs (`youtube-nocookie`, `player.vimeo.com`); client re-checks URLs before mount |
119
120
  | Re-open preferences | Cookie button (bottom-left); saving reloads so script loading matches the latest choice |
120
121
  | Consent cookie | `emprivacy_cc` (`path=/`, `SameSite=Lax`, `Secure` on HTTPS, configurable `Max-Age`, default 180 days), JSON `{ v, f, a, m }` |
121
122
  | Vendor list | Derived from settings; shown under Customize; JSON at `/_emdash/api/plugins/emprivacy/vendors` (no tokens) |
122
- | Optional server log | POST `{ policyVersion, functional, analytics, marketing }` to `/_emdash/api/plugins/emprivacy/record` (Origin / Sec-Fetch-Site, rate-limited, no IP) |
123
+ | Optional server log | POST `{ policyVersion, functional, analytics, marketing, gpc }` to `/_emdash/api/plugins/emprivacy/record` (Origin / Sec-Fetch-Site, rate-limited, no IP). Signed-in admins can download `/_emdash/api/plugins/emprivacy/consent-export` as CSV |
124
+ | Global Privacy Control | When `navigator.globalPrivacyControl` is true, Marketing stays off (including Accept all) until the signal is gone. Analytics is unchanged. Visitors can still open preferences |
125
+ | Cookie cleanup | Turning Analytics or Marketing off expires a fixed list of first-party cookie names for the presets EmPrivacy loads. The consent cookie is kept. HttpOnly and third-party cookies cannot be cleared from the page |
126
+ | Keyboard | Focus moves into the banner. Tab stays inside it. Escape closes the reopen panel and returns focus to the cookie button. The first visit stays open until a choice |
123
127
  | Google Consent Mode v2 | Optional denied defaults in `<head>` + `gtag('consent','update',…)` including `functionality_storage` |
128
+ | Microsoft consent | Clarity and UET presets queue denied signals in `<head>`. The Clarity tag loads only after Analytics consent; the UET tag loads only after Marketing consent |
124
129
  | i18n | Built-in chrome for common locales; optional JSON title/message overrides |
125
- | Theme | Hex CSS variables (`--emprivacy-bg`, `--emprivacy-text`, `--emprivacy-accent`, `--emprivacy-radius`) |
130
+ | Theme | Starts on the Slate profile. Also Paper, Ink, Indigo, Primer, or custom hex. Custom opens with the colors already in effect. Profiles use fixed colors only |
131
+ | Banner position | Fixed to the bottom (default) or top of the viewport. The fragment still renders at `body:end` |
126
132
 
127
133
  ## Capabilities
128
134
 
package/dist/index.d.mts CHANGED
@@ -50,6 +50,16 @@ type AnalyticsLoader = {
50
50
  {
51
51
  type: "gtm";
52
52
  containerId: string;
53
+ } |
54
+ /** Loaded only after Analytics consent. Advertising storage follows Marketing. */
55
+ {
56
+ type: "clarity";
57
+ projectId: string;
58
+ } |
59
+ /** Loaded only after Marketing consent. */
60
+ {
61
+ type: "uet";
62
+ tagId: string;
53
63
  };
54
64
  export declare function buildAnalyticsLoader(cfg: EmprivacyConfig): AnalyticsLoader;
55
65
  export declare function buildVendorList(cfg: EmprivacyConfig): VendorRow[];
@@ -74,6 +84,7 @@ interface EmprivacyChromeStrings {
74
84
  embedNeedConsent: string;
75
85
  vendorsHeading: string;
76
86
  whatWeUse: string;
87
+ gpcNote: string;
77
88
  }
78
89
  interface EmprivacyLocaleCopy {
79
90
  bannerTitle: string;
@@ -88,11 +99,46 @@ interface EmprivacyTheme {
88
99
  accent: string;
89
100
  radiusPx: number;
90
101
  }
102
+ /** Fixed banner palettes. Colors are constants, never taken from the admin form. */
103
+ declare const THEME_PROFILES: {
104
+ readonly slate: {
105
+ readonly bg: "#0f172a";
106
+ readonly text: "#f8fafc";
107
+ readonly accent: "#3b82f6";
108
+ readonly radiusPx: 8;
109
+ };
110
+ readonly paper: {
111
+ readonly bg: "#f7f6f3";
112
+ readonly text: "#37352f";
113
+ readonly accent: "#1d4ed8";
114
+ readonly radiusPx: 6;
115
+ };
116
+ readonly ink: {
117
+ readonly bg: "#000000";
118
+ readonly text: "#ededed";
119
+ readonly accent: "#0070f3";
120
+ readonly radiusPx: 8;
121
+ };
122
+ readonly indigo: {
123
+ readonly bg: "#ffffff";
124
+ readonly text: "#0f172a";
125
+ readonly accent: "#4f46e5";
126
+ readonly radiusPx: 12;
127
+ };
128
+ readonly primer: {
129
+ readonly bg: "#f6f8fa";
130
+ readonly text: "#1f2328";
131
+ readonly accent: "#0969da";
132
+ readonly radiusPx: 6;
133
+ };
134
+ };
135
+ type ThemeProfileId = keyof typeof THEME_PROFILES;
136
+ type ThemeChoice = ThemeProfileId | "custom";
91
137
  //#endregion
92
138
  //#region src/config.d.ts
93
139
  export declare const KV_KEY: "consent:config";
94
140
  export declare const COOKIE_NAME: "emprivacy_cc";
95
- type AnalyticsProvider = "cloudflare" | "none" | "custom" | "plausible" | "fathom" | "umami" | "simpleanalytics" | "ga4" | "gtm";
141
+ type AnalyticsProvider = "cloudflare" | "none" | "custom" | "plausible" | "fathom" | "umami" | "simpleanalytics" | "ga4" | "gtm" | "clarity" | "uet";
96
142
  type EmbedCategory = "functional" | "marketing";
97
143
  interface EmprivacyConfig {
98
144
  bannerTitle: string;
@@ -125,10 +171,15 @@ interface EmprivacyConfig {
125
171
  /** Optional SRI hashes keyed by script src (sha256/384/512-…) */
126
172
  scriptIntegrity: Record<string, string>;
127
173
  /**
128
- * When non-empty, Custom analytics and marketing script hosts must be on this list.
129
- * Preset CDN hosts remain allowed for built-in providers.
174
+ * Hostnames allowed for Custom analytics, Umami, and marketing scripts.
175
+ * An empty list denies those URLs. Preset CDN hosts stay allowed for built-in providers.
130
176
  */
131
177
  scriptHostAllowlist: string[];
178
+ /**
179
+ * Hosts that may become https links for Mastodon and link previews.
180
+ * X, Bluesky, Gist, YouTube, and Vimeo do not use this list.
181
+ */
182
+ embedHostAllowlist: string[];
132
183
  /** Consent cookie Max-Age in days (1–365, default 180) */
133
184
  cookieMaxAgeDays: number;
134
185
  /** Emit Google Consent Mode v2 defaults (denied) in head; updates after choice */
@@ -141,10 +192,15 @@ interface EmprivacyConfig {
141
192
  gateEmbeds: boolean;
142
193
  /** Hide the first-visit banner on privacy/cookie policy paths */
143
194
  hideBannerOnPolicyPages: boolean;
195
+ /** Viewport edge for the fixed consent banner. Fragment placement stays `body:end`. */
196
+ bannerPosition: BannerPosition;
144
197
  defaultLocale: string;
145
198
  localeOverrides: LocaleOverrides;
199
+ /** `custom` uses `theme`. A profile id replaces `theme` with that profile’s fixed colors. */
200
+ themeProfile: ThemeChoice;
146
201
  theme: EmprivacyTheme;
147
202
  }
203
+ type BannerPosition = "bottom" | "top";
148
204
  export declare const DEFAULT_CONFIG: EmprivacyConfig;
149
205
  /** Reject characters that would break JSON or attributes; allow typical Cloudflare site token strings. */
150
206
  export declare function assertValidCloudflareToken(t: string): void;
@@ -167,6 +223,8 @@ interface ConsentRecordPayload {
167
223
  functional: boolean;
168
224
  analytics: boolean;
169
225
  marketing: boolean;
226
+ /** Present when the browser's Global Privacy Control signal forced marketing off. */
227
+ gpc?: boolean;
170
228
  }
171
229
  interface ConsentState {
172
230
  v: string;
@@ -195,6 +253,7 @@ interface EmprivacyPublicRuntimeConfig {
195
253
  embedCategory: EmbedCategory;
196
254
  gateEmbeds: boolean;
197
255
  hideBanner: boolean;
256
+ bannerPosition: BannerPosition;
198
257
  theme: EmprivacyTheme;
199
258
  ui: EmprivacyChromeStrings;
200
259
  loader: AnalyticsLoader;
@@ -203,6 +262,7 @@ interface EmprivacyPublicRuntimeConfig {
203
262
  integrity: string | null;
204
263
  }[];
205
264
  scriptHostAllowlist: string[];
265
+ embedHostAllowlist: string[];
206
266
  scriptIntegrity: Record<string, string>;
207
267
  cookieMaxAge: number;
208
268
  vendors: VendorPublicRow[];
@@ -259,4 +319,4 @@ export declare function resolveEmbed(node: {
259
319
  */
260
320
  export declare function emprivacyPlugin(): PluginDescriptor;
261
321
  //#endregion
262
- export { type AnalyticsLoader, type AnalyticsProvider, type ConsentRecordPayload, type ConsentState, type EmbedCategory, type EmprivacyBrowserApi, type EmprivacyCategory, type EmprivacyConfig, type EmprivacyPublicRuntimeConfig, type ResolvedEmbed, type VendorPublicRow, type VendorRow, createPlugin as default };
322
+ export { type AnalyticsLoader, type AnalyticsProvider, type BannerPosition, type ConsentRecordPayload, type ConsentState, type EmbedCategory, type EmprivacyBrowserApi, type EmprivacyCategory, type EmprivacyConfig, type EmprivacyPublicRuntimeConfig, type ResolvedEmbed, type VendorPublicRow, type VendorRow, createPlugin as default };