@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.
@@ -25,7 +25,7 @@ pnpm run build
25
25
 
26
26
  The full install (including the `emdash` **dev** tree) can report transitive advisories from EmDash’s tooling stack. Those are owned by the site’s EmDash upgrade path, not by EmPrivacy’s published tarball.
27
27
 
28
- **This repository pins Kysely** via `pnpm.overrides` in `package.json` (currently `0.29.6`, aligned with EmDash 0.38). Regenerate the lockfile with `pnpm install` after changing overrides.
28
+ **This repository pins Kysely** via `pnpm.overrides` in `package.json` (currently `0.29.6`, aligned with EmDash 1.0.1). Regenerate the lockfile with `pnpm install` after changing overrides.
29
29
 
30
30
  ## Deprecation warnings you may still see
31
31
 
@@ -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
 
@@ -21,7 +23,7 @@ This guide is for **EmDash site managers** who want cookie consent on a live sit
21
23
 
22
24
  ## Before you begin
23
25
 
24
- 1. An EmDash site on **`emdash@^0.38.0`** with the EmDash Astro integration in `astro.config`.
26
+ 1. An EmDash site on **`emdash@^1.0.1`** with the EmDash Astro integration in `astro.config`. EmDash 0.x is not supported.
25
27
  2. Ability to edit **`astro.config`** and redeploy (or run locally).
26
28
  3. Register EmPrivacy as a **native** plugin (`plugins: []`, not `sandboxed: []`).
27
29
 
@@ -35,7 +37,7 @@ pnpm add @emplugins/emprivacy
35
37
  npm install @emplugins/emprivacy
36
38
  ```
37
39
 
38
- Pin a line with a range such as `@emplugins/emprivacy@^1.0.0`. See [RELEASES.md](./RELEASES.md).
40
+ Pin a line with a range such as `@emplugins/emprivacy@^3.0.0`. See [RELEASES.md](./RELEASES.md).
39
41
 
40
42
  ---
41
43
 
@@ -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: hex theme colors, locale JSON overrides, Google Consent Mode, server log.
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
  ---
@@ -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 loads only after **Marketing** consent (containers commonly fire ads and remarketing).
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, etc.) stay allowed for their presets.
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
- Hex colors only (`#111`, `#111111`). Corner radius is an integer **0–24**. Values become CSS variables on `#emprivacy-root`. Other CSS is ignored (no `url()`, no `expression`).
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/RELEASES.md CHANGED
@@ -25,7 +25,7 @@ These guards keep published tarballs consistent with `package.json` and avoid sh
25
25
  - **`dist/` is not committed** — It is listed in `.gitignore`. Build artifacts are produced locally, in CI, and **immediately before publish** via `prepublishOnly`.
26
26
  - **`prepublishOnly`** — Runs `sync:version` (keeps `src/version.ts` aligned with `package.json`), `typecheck`, `build`, `test`, **`verify:exports`**, and `pnpm audit`.
27
27
  - **`kysely` override** — The dev-dependency `emdash` can resolve an older `kysely`; `pnpm.overrides` in `package.json` pins a **patched** Kysely so `pnpm audit` stays clean. See [docs/DEVELOPMENT.md](DEVELOPMENT.md).
28
- - **Install range** — Pin consumers with semver as needed, e.g. `@emplugins/emprivacy@^1.0.0`.
28
+ - **Install range** — Pin consumers with semver as needed, e.g. `@emplugins/emprivacy@^3.0.0`. Requires EmDash `^1.0.1`.
29
29
 
30
30
  Before publishing locally (usually unnecessary once CI is configured):
31
31
 
@@ -77,5 +77,5 @@ See [EMDASH_COMPAT.md](../EMDASH_COMPAT.md) for tested upstream versions and upg
77
77
  Pin with a range that matches your risk tolerance, for example:
78
78
 
79
79
  ```bash
80
- pnpm add @emplugins/emprivacy@^1.0.0
80
+ pnpm add @emplugins/emprivacy@^3.0.0
81
81
  ```
@@ -4,7 +4,26 @@
4
4
 
5
5
  ## Changes
6
6
 
7
- _(none)_
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.
20
+
21
+ # emprivacy v3.0.0
22
+
23
+ ## Changes
24
+
25
+ - **EmDash 1.0 baseline** — Requires **`emdash@^1.0.1`** (tested with **`1.0.1`**). EmDash 0.x is not supported. Sites still on EmDash 0.38 should stay on `@emplugins/emprivacy@2`.
26
+ - Remains a **native** plugin: `format: "native"`, named `createPlugin()`, `page:fragments`, and Portable Text embed placeholders. Register `emprivacyPlugin()` in `plugins: []`. The official plugin registry only accepts sandboxed plugins, so this package stays on npm.
8
27
 
9
28
  # emprivacy v2.1.0
10
29
 
package/docs/TESTING.md CHANGED
@@ -10,29 +10,31 @@ 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
 
17
17
  ## Manual integration QA (required before production)
18
18
 
19
- Use a **staging** EmDash Astro app on **`emdash@^0.38.0`** with `emprivacyPlugin()` in **`plugins: []`**, layout wires (`EmDashHead` / body components with page context), and the same EmDash version you plan to ship.
19
+ Use a **staging** EmDash Astro app on **`emdash@^1.0.1`** with `emprivacyPlugin()` in **`plugins: []`**, layout wires (`EmDashHead` / body components with page context), and the same EmDash version you plan to ship. EmDash 0.x is not supported.
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": "2.1.0",
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",
@@ -39,14 +39,14 @@
39
39
  "license": "MIT",
40
40
  "packageManager": "pnpm@9.15.4",
41
41
  "peerDependencies": {
42
- "emdash": "^0.38.0"
42
+ "emdash": "^1.0.1"
43
43
  },
44
44
  "dependencies": {
45
45
  "zod": "4.6.5"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@changesets/cli": "^3.0.3",
49
- "emdash": "0.38.0",
49
+ "emdash": "^1.0.1",
50
50
  "tsdown": "^0.23.0",
51
51
  "typescript": "^5.9.3",
52
52
  "vitest": "^5.0.1"