@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/README.md +11 -5
- package/dist/index.d.mts +56 -2
- package/dist/index.mjs +503 -55
- package/docs/GETTING_STARTED.md +5 -3
- package/docs/PLUGIN_SETTINGS.md +42 -5
- package/docs/RELEASE_NOTES.md +13 -1
- package/docs/TESTING.md +8 -6
- package/package.json +1 -1
package/docs/GETTING_STARTED.md
CHANGED
|
@@ -9,7 +9,9 @@ This guide is for **EmDash site managers** who want cookie consent on a live sit
|
|
|
9
9
|
- A **banner** on first visit (or after you change **Policy / consent version**, or after an upgrade that adds the functional category).
|
|
10
10
|
- Toggles for **Functional**, **Analytics**, and **Marketing**. Essential cookies stay on.
|
|
11
11
|
- **Privacy and cookie policy links** you control (EmDash Page path or `https://`).
|
|
12
|
-
- Analytics presets (Cloudflare, Plausible, Fathom, Umami, Simple Analytics, GA4, GTM under Marketing) plus Custom / None.
|
|
12
|
+
- Analytics presets (Cloudflare, Plausible, Fathom, Umami, Simple Analytics, GA4, GTM under Marketing, Microsoft Clarity under Analytics, Microsoft UET under Marketing) plus Custom / None.
|
|
13
|
+
- **Global Privacy Control** keeps Marketing off when the browser sends that signal.
|
|
14
|
+
- A signed-in admin download of the consent log when server logging is on.
|
|
13
15
|
- Optional **gated embeds** for official EmDash YouTube / Vimeo / social / Gist blocks.
|
|
14
16
|
- A **vendor list** generated from your settings.
|
|
15
17
|
|
|
@@ -92,7 +94,7 @@ Your base layout must include `<EmDashHead page={page} />`, `<EmDashBodyStart pa
|
|
|
92
94
|
3. Choose an **Analytics platform** and fill the matching ID or token. Leave it on **None** if you do not use analytics.
|
|
93
95
|
4. Optional: marketing script URLs (one `https://` `src` per line — never a `<script>` tag).
|
|
94
96
|
5. Optional: gate embeds; pick Functional vs Marketing.
|
|
95
|
-
6. Optional:
|
|
97
|
+
6. Optional: banner position (bottom or top), a banner theme profile or custom hex colors, locale JSON overrides, Google Consent Mode, server log. When the log is on, the same page links to the CSV download.
|
|
96
98
|
7. **Save settings**.
|
|
97
99
|
|
|
98
100
|
The **What this site uses** table on the same page is generated from the saved config. You can copy it into the cookie policy. Tokens are never listed.
|
|
@@ -136,7 +138,7 @@ Do not read `emprivacy_cc` yourself. The JSON shape can change with a major rele
|
|
|
136
138
|
| YouTube still loads immediately | Embeds plugin is registered **after** EmPrivacy, so its renderers win. Move EmPrivacy last or pass `blockComponents` on `PortableText`. |
|
|
137
139
|
| Embeds stay blocked after accept | Category is Marketing but you only enabled Functional (or the reverse). |
|
|
138
140
|
| Save fails on policy URL | Must be `/path` or `https://…` |
|
|
139
|
-
| Save fails on analytics ID | Plausible wants `example.com` (no scheme). GA4 wants `G-…`. GTM wants `GTM-…`. Umami needs an https script URL. |
|
|
141
|
+
| Save fails on analytics ID | Plausible wants `example.com` (no scheme). GA4 wants `G-…`. GTM wants `GTM-…`. Clarity wants a 7–20 character project ID. UET wants 6–12 digits. Umami needs an https script URL. |
|
|
140
142
|
| Privacy link 404 | Path must match the live Page route |
|
|
141
143
|
|
|
142
144
|
---
|
package/docs/PLUGIN_SETTINGS.md
CHANGED
|
@@ -76,11 +76,17 @@ On: Functional, Analytics, and Marketing start **unchecked**. Off: they start **
|
|
|
76
76
|
|
|
77
77
|
On (default): visitors who open `/privacy` from the banner can read it without another overlay. The cookie button still appears so they can change choices.
|
|
78
78
|
|
|
79
|
+
### Banner position
|
|
80
|
+
|
|
81
|
+
**Bottom of the viewport** (default) or **Top of the viewport**. The banner is `position: fixed`, so it stays on that edge while the page scrolls. EmDash still inserts the markup through `EmDashBodyEnd` (`body:end`). `body:start` would not move a fixed banner. Padding includes `env(safe-area-inset-top)` or `env(safe-area-inset-bottom)` so the bar clears a notch or home indicator. The reopen button stays at the bottom-left.
|
|
82
|
+
|
|
83
|
+
Saved settings without this field keep the bottom banner.
|
|
84
|
+
|
|
79
85
|
---
|
|
80
86
|
|
|
81
87
|
## Analytics platform
|
|
82
88
|
|
|
83
|
-
Scripts for **Analytics** presets load only after **Analytics** consent — except **Google Tag Manager**, which
|
|
89
|
+
Scripts for **Analytics** presets load only after **Analytics** consent — except **Google Tag Manager** and **Microsoft UET**, which load only after **Marketing** consent (those tags commonly fire ads and remarketing).
|
|
84
90
|
|
|
85
91
|
| Value | What you enter | What loads |
|
|
86
92
|
|-------|----------------|------------|
|
|
@@ -91,6 +97,8 @@ Scripts for **Analytics** presets load only after **Analytics** consent — exce
|
|
|
91
97
|
| Simple Analytics | Optional hostname | `https://scripts.simpleanalyticscdn.com/latest.js` |
|
|
92
98
|
| Google Analytics 4 | `G-…` measurement ID | `https://www.googletagmanager.com/gtag/js?id=…` then `gtag('config', …)` |
|
|
93
99
|
| Google Tag Manager | `GTM-…` | `https://www.googletagmanager.com/gtm.js?id=…` after **Marketing** consent. Tags inside the container are still your responsibility. |
|
|
100
|
+
| Microsoft Clarity | Project ID, 7–20 letters or digits | `https://www.clarity.ms/tag/…` after **Analytics** consent. A denied `consentv2` signal is queued in `<head>` first. `ad_Storage` stays denied until Marketing is also allowed. |
|
|
101
|
+
| Microsoft UET | Tag ID, 6–12 digits | `https://bat.bing.com/bat.js` after **Marketing** consent. `ad_storage` defaults to denied in `<head>`. The tag is not injected before that consent. Automatic SPA tracking is off. |
|
|
94
102
|
| None | — | No analytics scripts |
|
|
95
103
|
| Custom | One https script `src` per line (optional trailing SRI) | Those URLs only |
|
|
96
104
|
|
|
@@ -98,7 +106,7 @@ IDs and URLs are validated on save and again in the browser before inject. Token
|
|
|
98
106
|
|
|
99
107
|
### Script host allowlist (optional)
|
|
100
108
|
|
|
101
|
-
One hostname per line (e.g. `cdn.example.com`). When non-empty, **Custom** analytics, **Umami**, and **Marketing** script hosts must appear on the list. Built-in preset CDNs (Plausible, Fathom, GTM,
|
|
109
|
+
One hostname per line (e.g. `cdn.example.com`). When non-empty, **Custom** analytics, **Umami**, and **Marketing** script hosts must appear on the list. Built-in preset CDNs (Plausible, Fathom, GTM, Clarity, UET, and so on) stay allowed for their presets.
|
|
102
110
|
|
|
103
111
|
### Optional Subresource Integrity (SRI)
|
|
104
112
|
|
|
@@ -157,13 +165,42 @@ Registration order: see [GETTING_STARTED](./GETTING_STARTED.md#registration-orde
|
|
|
157
165
|
|
|
158
166
|
## Theme
|
|
159
167
|
|
|
160
|
-
|
|
168
|
+
**Banner theme** is one of five profiles, or **Custom colors**. A new install uses **Slate**, the first profile. A saved config with no theme choice also uses Slate. A profile writes that palette’s fixed background, text color, accent, and button radius. Color fields sent with a profile are ignored, so a profile cannot be combined with other CSS.
|
|
169
|
+
|
|
170
|
+
**Custom colors** opens with the colors already in effect. On Slate, those fields start as `#0f172a`, `#f8fafc`, `#3b82f6`, and 8 px. After a custom save, they show the saved hex values.
|
|
171
|
+
|
|
172
|
+
| Profile | Background | Text | Accent | Radius |
|
|
173
|
+
|---------|------------|------|--------|--------|
|
|
174
|
+
| Slate | `#0f172a` | `#f8fafc` | `#3b82f6` | 8 px |
|
|
175
|
+
| Paper | `#f7f6f3` | `#37352f` | `#1d4ed8` | 6 px |
|
|
176
|
+
| Ink | `#000000` | `#ededed` | `#0070f3` | 8 px |
|
|
177
|
+
| Indigo | `#ffffff` | `#0f172a` | `#4f46e5` | 12 px |
|
|
178
|
+
| Primer | `#f6f8fa` | `#1f2328` | `#0969da` | 6 px |
|
|
179
|
+
|
|
180
|
+
**Custom colors** accepts hex only (`#111`, `#111111`, or `#111111ff`). Corner radius is an integer **0–24**. Those values become CSS variables on `#emprivacy-root`. Other CSS is rejected on save (no `url()`, no `expression`). The full-width bar stays square. Radius applies to the buttons.
|
|
161
181
|
|
|
162
182
|
---
|
|
163
183
|
|
|
184
|
+
## Global Privacy Control
|
|
185
|
+
|
|
186
|
+
There is no admin switch. If the browser sets `navigator.globalPrivacyControl`, EmPrivacy treats Marketing as denied for script loading, Google `ad_*` signals, Clarity `ad_Storage`, and UET. Accept all and Save cannot turn Marketing on while the signal is present. The Marketing checkbox is disabled and the banner explains why. Analytics and Functional are unchanged. Visitors can still open preferences.
|
|
187
|
+
|
|
188
|
+
The choice cookie is not rewritten until the visitor saves. While the signal is on, `window.emprivacy.get()` reports `marketing: false` and `gpc: true` even if an older cookie still has `m: 1`. After the signal is turned off, that saved Marketing choice applies again until they save a new one.
|
|
189
|
+
|
|
190
|
+
## Known-cookie cleanup
|
|
191
|
+
|
|
192
|
+
When Analytics or Marketing is denied (including the first visit, before a choice), EmPrivacy expires a **fixed** list of first-party names:
|
|
193
|
+
|
|
194
|
+
- Analytics: `_ga`, `_gid`, `_gat`, and names starting with `_ga_` or `_gat_`
|
|
195
|
+
- Marketing: `_gcl_au`, `_gcl_aw`, `_gcl_dc`, names starting with `_gcl_`, plus Clarity `_clck`, `_clsk`, `CLID` and UET `_uetsid`, `_uetvid`
|
|
196
|
+
|
|
197
|
+
`emprivacy_cc` is never deleted. Settings cannot add cookie names. Page script cannot clear HttpOnly cookies or cookies set on another site. Expiration uses `Path=/` on the current host and its parent host label.
|
|
198
|
+
|
|
164
199
|
## Server-side logging
|
|
165
200
|
|
|
166
|
-
When enabled, each save POSTs `{ policyVersion, functional, analytics, marketing }` to `/_emdash/api/plugins/emprivacy/record` (same origin, JSON). The handler requires a matching `Origin` **or** `Sec-Fetch-Site: same-origin`, fails closed if storage capacity cannot be checked, and rate-limits anonymous clients (~30/hour fingerprint). No IP is stored in the consent row. The page shows up to 15 recent rows. Logging stops at 500 rows.
|
|
201
|
+
When enabled, each save POSTs `{ policyVersion, functional, analytics, marketing, gpc }` to `/_emdash/api/plugins/emprivacy/record` (same origin, JSON). The handler requires a matching `Origin` **or** `Sec-Fetch-Site: same-origin`, fails closed if storage capacity cannot be checked, and rate-limits anonymous clients (~30/hour fingerprint). No IP is stored in the consent row. If `gpc` is true, the stored `marketing` flag is false. The page shows up to 15 recent rows. Logging stops at 500 rows.
|
|
202
|
+
|
|
203
|
+
Signed-in admins (the plugin settings permission) can download those rows as CSV from `/_emdash/api/plugins/emprivacy/consent-export`. The file is `Cache-Control: private, no-store`. It has no IP, user agent, or rate-limit fingerprint. A cell that starts with `=`, `+`, `-`, or `@` is prefixed with `'` so a spreadsheet does not run it as a formula. The route is not public. A request without a signed-in user is rejected.
|
|
167
204
|
|
|
168
205
|
---
|
|
169
206
|
|
|
@@ -172,7 +209,7 @@ When enabled, each save POSTs `{ policyVersion, functional, analytics, marketing
|
|
|
172
209
|
EmPrivacy injects an **inline** bootstrap via EmDash `page:fragments`. Sites with a strict CSP must allow that script, typically:
|
|
173
210
|
|
|
174
211
|
- `script-src 'unsafe-inline'` (or a hash/nonce of the emitted bootstrap if your host supports it), and
|
|
175
|
-
- `script-src` / `connect-src` / `frame-src` entries for any presets and gated embeds you enable (e.g. `https://plausible.io`, `https://www.youtube-nocookie.com`, `https://player.vimeo.com`).
|
|
212
|
+
- `script-src` / `connect-src` / `frame-src` entries for any presets and gated embeds you enable (e.g. `https://plausible.io`, `https://www.clarity.ms`, `https://bat.bing.com`, `https://www.youtube-nocookie.com`, `https://player.vimeo.com`).
|
|
176
213
|
|
|
177
214
|
YouTube/Vimeo placeholders use a tight iframe `sandbox` (`allow-scripts allow-same-origin allow-presentation`) after consent. Client hydration also re-checks allowlisted `data-src` values so a planted evil URL cannot become an iframe.
|
|
178
215
|
|
package/docs/RELEASE_NOTES.md
CHANGED
|
@@ -4,7 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
## Changes
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
No unreleased changes.
|
|
8
|
+
|
|
9
|
+
# emprivacy v3.1.0
|
|
10
|
+
|
|
11
|
+
## Changes
|
|
12
|
+
|
|
13
|
+
- **Banner position** — Admin setting places the fixed consent banner at the bottom (default) or top of the viewport. Markup still renders through `EmDashBodyEnd`.
|
|
14
|
+
- **Banner themes** — New installs use the Slate profile. Admin can choose Paper, Ink, Indigo, Primer, or Custom colors. Custom opens with the colors already in effect. A profile stores that palette’s fixed colors. Custom colors still accept hex only.
|
|
15
|
+
- **Global Privacy Control** — When the browser sets `navigator.globalPrivacyControl`, Marketing stays denied, including after Accept all. The optional server log stores `gpc: true` and `marketing: false`. Analytics is not forced off. No geolocation.
|
|
16
|
+
- **Cookie cleanup** — Denying Analytics or Marketing expires a fixed list of first-party cookie names used by the presets EmPrivacy injects. The consent cookie is not deleted. HttpOnly and third-party cookies are unchanged.
|
|
17
|
+
- **Keyboard** — Focus moves into the banner, Tab stays inside it, and Escape closes the reopen panel. The first visit stays until the visitor chooses.
|
|
18
|
+
- **Consent log download** — Signed-in admins can GET `/_emdash/api/plugins/emprivacy/consent-export` (CSV, no IP). Formula characters in cells are neutralized.
|
|
19
|
+
- **Microsoft presets** — Clarity loads only after Analytics consent (`www.clarity.ms`). UET loads only after Marketing consent (`bat.bing.com`). Both queue a denied consent signal before the tag is injected.
|
|
8
20
|
|
|
9
21
|
# emprivacy v3.0.0
|
|
10
22
|
|
package/docs/TESTING.md
CHANGED
|
@@ -10,7 +10,7 @@ Testing is split into **automated** checks (CI-friendly) and **manual** checks t
|
|
|
10
10
|
|--------|-------------------|
|
|
11
11
|
| `pnpm run typecheck` | TypeScript types against `emdash` APIs |
|
|
12
12
|
| `pnpm run build` | Bundles ESM + declarations (`dist/`) |
|
|
13
|
-
| `pnpm test` | Config validation, security helpers (embed/script allowlists, CSRF Origin/Sec-Fetch-Site, cookie TTL, SRI), i18n, theme hex, vendor list (no token leak; GTM=marketing), JSON escaping |
|
|
13
|
+
| `pnpm test` | Config validation, security helpers (embed/script allowlists, CSRF Origin/Sec-Fetch-Site, cookie TTL, SRI), i18n, theme hex, vendor list (no token leak; GTM and UET=marketing; Clarity ID not published), cookie-name purge list, CSV formula neutralization, JSON escaping |
|
|
14
14
|
|
|
15
15
|
These scripts do **not** start EmDash or drive a browser.
|
|
16
16
|
|
|
@@ -20,19 +20,21 @@ Use a **staging** EmDash Astro app on **`emdash@^1.0.1`** with `emprivacyPlugin(
|
|
|
20
20
|
|
|
21
21
|
### Core flows
|
|
22
22
|
|
|
23
|
-
1. **Banner** — Incognito: banner appears with no `emprivacy_cc` cookie, when **Policy / consent version** does not match `v`, or when a legacy cookie lacks `f`.
|
|
23
|
+
1. **Banner** — Incognito: banner appears with no `emprivacy_cc` cookie, when **Policy / consent version** does not match `v`, or when a legacy cookie lacks `f`. **Banner position** Bottom keeps it on the bottom edge; Top pins it to the top. The reopen button stays bottom-left.
|
|
24
24
|
2. **Categories** — Customize shows Essential (locked on), Functional, Analytics, Marketing. Reject non-essential: no configured analytics/marketing scripts in Network; embeds stay placeholders.
|
|
25
|
-
3. **Re-open** — Cookie button (bottom-left). Save reloads. Turning a category off removes those scripts after reload.
|
|
26
|
-
4. **Presets** — For the platform you ship, confirm the expected host loads only after the correct category (Analytics for most presets; **Marketing** for GTM). Custom/marketing scripts must respect any host allowlist.
|
|
25
|
+
3. **Re-open** — Cookie button (bottom-left). Save reloads. Turning a category off removes those scripts after reload and expires the known first-party cookie names for that category (`_ga` / `_ga_*` for Analytics; `_clck`, `_uetsid`, and the other fixed names for Marketing). `emprivacy_cc` remains.
|
|
26
|
+
4. **Presets** — For the platform you ship, confirm the expected host loads only after the correct category (Analytics for most presets, including Clarity; **Marketing** for GTM and UET). Custom/marketing scripts must respect any host allowlist. Clarity and UET must not request their hosts before that consent.
|
|
27
27
|
5. **Evil embed plant** — In DevTools, add a placeholder with `data-src="https://evil.example/x"` and grant marketing; assert no iframe is created.
|
|
28
28
|
6. **Embed unlock** — “Allow embeds and load” opens preferences; it must not silently flip categories.
|
|
29
29
|
7. **Embeds** — With `@emdash-cms/plugin-embeds` and EmPrivacy last (or site-level `blockComponents`): a YouTube block is a placeholder first; after allowing the embed category, the iframe `src` is `https://www.youtube-nocookie.com/embed/…`. A junk/javascript URL never becomes an iframe.
|
|
30
30
|
8. **API** — After a choice, `window.emprivacy.get()` returns state; `window.emprivacy.has("analytics")` matches Network.
|
|
31
31
|
9. **i18n** — A `de` (or `fr`) page shows translated buttons when that locale is active.
|
|
32
32
|
10. **Policy pages** — `/privacy` does not show the first-visit overlay when that setting is on; the cookie button still works.
|
|
33
|
-
11. **Admin** — Save persists; **What this site uses** matches vendors; tokens do not appear there or at `/_emdash/api/plugins/emprivacy/vendors`.
|
|
34
|
-
12. **Optional server log** — POST requires matching Origin or `Sec-Fetch-Site: same-origin`; Origin mismatch is 403. Body includes `functional`.
|
|
33
|
+
11. **Admin** — Save persists; **What this site uses** matches vendors; tokens do not appear there or at `/_emdash/api/plugins/emprivacy/vendors`. **Banner theme** Slate, Paper, Ink, Indigo, or Primer replaces the colors. Custom still rejects a non-hex value.
|
|
34
|
+
12. **Optional server log** — POST requires matching Origin or `Sec-Fetch-Site: same-origin`; Origin mismatch is 403. Body includes `functional`. The CSV download works while signed in to admin and fails when signed out. A policy version that starts with `=` is stored as text in the file, not as a formula.
|
|
35
35
|
13. **Optional Google Consent Mode** — Denied defaults in `<head>` before interaction, including `functionality_storage`.
|
|
36
|
+
14. **Global Privacy Control** — With the signal enabled, Marketing stays unchecked and disabled, Accept all does not set `m` to 1, and marketing scripts do not load. Analytics can still be allowed. Turning the signal off leaves a previously saved Marketing choice in effect until the visitor saves again.
|
|
37
|
+
15. **Keyboard** — Tab cycles inside the banner. Escape closes only the reopen panel and returns focus to the cookie button. The first visit does not dismiss on Escape. Buttons show a visible focus ring.
|
|
36
38
|
|
|
37
39
|
### Regression triggers
|
|
38
40
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "SPDX-License-Identifier: MIT",
|
|
3
3
|
"name": "@emplugins/emprivacy",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.1.0",
|
|
5
5
|
"description": "Cookie consent, gated embeds, and privacy notices for EmDash (GDPR, CCPA/CIPA-oriented configuration)",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.mjs",
|