@emplugins/emprivacy 1.0.0 → 2.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 CHANGED
@@ -2,95 +2,166 @@
2
2
  ![EmPrivacy Logo](./artwork/logo/EmPrivacy.png)
3
3
  # EmPrivacy
4
4
 
5
- **EmPrivacy** is an open source [EmDash](https://github.com/emdash-cms/emdash) plugin that adds a cookie consent banner, category-based choices (essential / analytics / marketing), and **client-side** loading of third-party scripts only after consent. It aligns with common regulatory expectations when you configure it honestly and pair it with proper legal documents—but **this software is not legal advice** (see [Disclaimer](#legal-disclaimer)).
5
+ **EmPrivacy** is an open source [EmDash](https://github.com/emdash-cms/emdash) plugin that adds a cookie consent banner, category-based choices (essential / functional / analytics / marketing), **client-side** loading of configured third-party scripts only after consent, and optional placeholders for official EmDash embed blocks. It aligns with common regulatory expectations when you configure it honestly and pair it with proper legal documents—but **this software is not legal advice** (see [Disclaimer](#legal-disclaimer)).
6
+
7
+ ## What it does
8
+
9
+ - Shows a first-visit banner (and a reopen control) with **Accept all**, **Reject non-essential**, and **Customize**.
10
+ - Stores choices in the first-party `emprivacy_cc` cookie as `{ v, f, a, m }` (policy version, functional, analytics, marketing).
11
+ - Loads **only the analytics and marketing scripts you configure** after the matching category is allowed.
12
+ - Optionally replaces official EmDash embed blocks (YouTube, Vimeo, X, Bluesky, Mastodon, Gist, link preview) with a placeholder until the chosen category is allowed.
13
+ - Exposes `window.emprivacy` so themes and other plugins can read consent without parsing the cookie.
14
+ - Builds a **vendor list** from your settings (admin + `GET /_emdash/api/plugins/emprivacy/vendors`).
15
+ - Localizes banner chrome for `en`, `de`, `fr`, `es`, `it`, `nl`, `pt`, and `pl`, with optional JSON overrides for title and message.
16
+ - Lets you theme the banner with hex CSS variables.
17
+
18
+ ## What it does not do
19
+
20
+ - **Does not write or generate a privacy policy, cookie policy, or DPA.** You create those as EmDash Pages (or host them elsewhere) and paste the path or `https` URL.
21
+ - **Does not scan the theme** for leftover `gtag`, pixels, or iframes you pasted yourself. Scripts and iframes outside EmPrivacy still load.
22
+ - **Does not geo-locate visitors** or switch EU/US banners automatically.
23
+ - **Does not block first-party EmDash comments**, sessions, or other essential site cookies.
24
+ - **Does not iframe arbitrary hosts.** Mastodon, Gist, Bluesky, X, and link-preview blocks become **https links** (or a YouTube/Vimeo iframe from an allowlisted player host) after consent.
25
+ - **Is not a sandbox.** It is a **native** plugin (`plugins: []`) and runs with the same trust as your site code.
26
+ - **Is not legal advice** and does not certify GDPR/CCPA/CIPA compliance.
6
27
 
7
28
  ## Requirements
8
29
 
9
- - **EmDash** `>=0.14.0` (**tested with `0.14.0` and `0.29.0`**; see [EMDASH_COMPAT.md](./EMDASH_COMPAT.md)). Sites on EmDash 0.9–0.13: use **emprivacy 0.2.x**.
10
- - Plugins registered in **`astro.config`** in **trusted** mode (the `plugins: []` array).
11
- **Sandboxed** marketplace plugins **cannot** use `page:fragments`; EmPrivacy needs the `hooks.page-fragments:register` capability so head/body contributions run on public pages. See [Plugin System Overview](https://docs.emdashcms.com/plugins/overview/).
30
+ - **EmDash** `^0.38.0` (**tested with `0.38.0`**; re-test on your deployed minor when upgrading).
31
+ - **Node.js** `>= 22.16` (matches EmDash’s engine requirement).
32
+ - Register as a **native** plugin in **`astro.config`** via `plugins: []` (not `sandboxed: []`). EmPrivacy uses `page:fragments` for the public banner and `componentsEntry` for embed placeholders. See [Page fragments](https://docs.emdashcms.com/plugins/creating-native-plugins/page-fragments/) and [Plugin overview](https://docs.emdashcms.com/plugins/overview/).
12
33
 
13
34
  ## Install
14
35
 
15
36
  ```bash
37
+ pnpm add @emplugins/emprivacy
38
+ # or
16
39
  npm install @emplugins/emprivacy
17
40
  ```
18
41
 
19
- Published under the [emplugins](https://www.npmjs.com/org/emplugins) npm org. Source: [github.com/EmPlugins/EmPrivacy](https://github.com/EmPlugins/EmPrivacy).
42
+ Published under the [emplugins](https://www.npmjs.com/org/emplugins) npm org. Source: [github.com/EmPlugins/EmPrivacy](https://github.com/EmPlugins/EmPrivacy). See [EMDASH_COMPAT.md](./EMDASH_COMPAT.md) for EmDash version mapping.
20
43
 
21
- Use a **semver range** if you want controlled upgrades, for example `@emplugins/emprivacy@^1.0.0`. Versioning policy and maintainer release steps: [docs/RELEASES.md](docs/RELEASES.md). Org publish / `NPM_TOKEN` setup: [docs/NPM_ORG_PUBLISH.md](docs/NPM_ORG_PUBLISH.md).
44
+ Use a **semver range** if you want controlled upgrades, for example `@emplugins/emprivacy@^1.0.0`. Versioning: [docs/RELEASES.md](docs/RELEASES.md). Org publish: [docs/NPM_ORG_PUBLISH.md](docs/NPM_ORG_PUBLISH.md).
22
45
 
23
46
  The legacy unscoped package `emprivacy` is deprecated; migrate imports to `@emplugins/emprivacy`.
24
47
 
25
- ## Quick start
48
+ ## Quick start (site managers)
26
49
 
27
- 1. **Register the plugin** in `astro.config.mjs` (or `.ts`) **before** other plugins that inject marketing or analytics, so consent runs first:
50
+ 1. **Register the plugin** in `astro.config.mjs` (or `.ts`):
28
51
 
29
52
  ```ts
30
53
  import { defineConfig } from "astro/config";
31
- import { emdash } from "emdash/astro";
54
+ import emdash from "emdash/astro";
32
55
  import { emprivacyPlugin } from "@emplugins/emprivacy";
33
56
 
34
57
  export default defineConfig({
35
58
  integrations: [
36
59
  emdash({
37
- plugins: [emprivacyPlugin()],
60
+ plugins: [
61
+ emprivacyPlugin(),
62
+ // other native plugins…
63
+ ],
38
64
  }),
39
65
  ],
40
66
  });
41
67
  ```
42
68
 
43
- 2. **Use EmDash page integration** in your layout so fragments render: include `<EmDashHead />`, `<EmDashBodyStart />`, `<EmDashBodyEnd />` (or your theme’s equivalents). See [EmDash docs](https://docs.emdashcms.com/).
69
+ Native descriptors belong in `plugins`, not `sandboxed`. EmDash loads the named `createPlugin()` export from the package at runtime.
70
+
71
+ 2. **Put EmPrivacy first** when other plugins inject `gtag` / analytics in `<head>` (Google Consent Mode must run first).
72
+
73
+ 3. **Put EmPrivacy last** when you want automatic YouTube/Vimeo placeholders (later `blockComponents` win). If you need **both** Consent Mode-first **and** gated embeds, keep EmPrivacy first and pass the wrappers on your Portable Text renderer:
74
+
75
+ ```ts
76
+ import { PortableText } from "emdash/ui";
77
+ import { blockComponents as emprivacyEmbeds } from "@emplugins/emprivacy/astro";
78
+ ```
79
+
80
+ ```astro
81
+ <PortableText value={entry.data.content} components={{ type: emprivacyEmbeds }} />
82
+ ```
83
+
84
+ Site-level `components` always win over every plugin.
44
85
 
45
- 3. **Configure EmPrivacy in the admin** (shield → **EmPrivacy**):
46
- - **Privacy policy** — Point to your existing EmDash **Page** using its **public path** (what you see in the address bar when that Page is open, e.g. `/privacy`), **or** a full `https://…` URL if the policy is hosted elsewhere. Root-relative paths are resolved with EmDash `ctx.url()` so links stay correct across environments.
47
- - **Cookie policy** (optional) — Same rules: EmDash Page path or `https://…`.
48
- - Set banner copy, **Policy / consent version**, **Analytics** (Cloudflare site token, **None**, or **Custom** `https` URLs), and **Marketing** script URLs (`https`, one per line). See [GETTING_STARTED](docs/GETTING_STARTED.md#step-5--configure-emprivacy-in-the-admin).
86
+ 4. **Use EmDash page integration** in your layout: `<EmDashHead />`, `<EmDashBodyStart />`, `<EmDashBodyEnd />` with `createPublicPageContext({ Astro, … })`.
49
87
 
50
- See [docs/GETTING_STARTED.md](docs/GETTING_STARTED.md) for a step-by-step path from “create a Page” to “verify the link in the banner.”
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.
51
89
 
52
- Visitors see a banner on first visit (or when you bump **Policy / consent version**). Configured analytics (e.g. Cloudflare beacon) and marketing scripts load only **after** the user consents to the matching category.
90
+ 6. Pick an analytics preset (Cloudflare, Plausible, Fathom, Umami, Simple Analytics, GA4, GTM, None, or Custom), optional marketing script URLs, and **Save**.
91
+
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
+
94
+ ## Public JavaScript API
95
+
96
+ After the banner script runs:
97
+
98
+ ```ts
99
+ window.emprivacy.get();
100
+ // { v, essential: true, functional, analytics, marketing } | null
101
+
102
+ window.emprivacy.has("analytics"); // boolean
103
+ window.emprivacy.open(); // reopen the panel
104
+ const stop = window.emprivacy.onChange((state) => { /* … */ });
105
+ stop();
106
+ ```
107
+
108
+ `document` also receives `emprivacy:change` with the same state in `event.detail`. Other plugins should gate their own scripts with this API instead of adding a second banner.
53
109
 
54
110
  ## Features
55
111
 
56
112
  | Feature | Description |
57
113
  |--------|-------------|
58
- | Categories | Essential (informational), **Analytics**, **Marketing** |
59
- | Privacy / cookie links | EmDash **Page** as a root path (e.g. `/privacy`) or external `https://` URL; paths resolve via `ctx.url()` in hooks so public URLs stay correct when the site origin changes |
60
- | Analytics platforms | **Cloudflare Web Analytics** — enter your site token; the beacon is injected with `data-cf-beacon` after consent. **None** / **Custom** (`https` URLs) also supported; more platforms can be added over time |
61
- | Marketing scripts | Only **https** URLs you list are injected as `<script src>` after marketing consent—no arbitrary admin HTML |
62
- | Re-open preferences | After the first choice, a **cookie button** (bottom-left) re-opens the same options; saving from that panel **reloads** the page so script loading matches the latest consent (including revoking) |
63
- | Consent cookie | `emprivacy_cc` (`path=/`, `SameSite=Lax`, `Secure` on HTTPS), JSON `{ v, a, m }` (version, analytics, marketing) |
64
- | Optional server log | POST consent snapshots to `/_emdash/api/plugins/emprivacy/record` + optional storage rows (no IP in v1) |
65
- | Google Consent Mode v2 | Optional denied defaults in `<head>` + `gtag('consent','update',…)` — validate with [Google’s docs](https://support.google.com/tagmanager/answer/13695607) |
114
+ | Categories | Essential (always on), **Functional**, **Analytics**, **Marketing** |
115
+ | 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
+ | Marketing scripts | Only **https** URLs you list, injected as `<script src>` after marketing consent |
118
+ | 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
+ | Re-open preferences | Cookie button (bottom-left); saving reloads so script loading matches the latest choice |
120
+ | Consent cookie | `emprivacy_cc` (`path=/`, `SameSite=Lax`, `Secure` on HTTPS, configurable `Max-Age`, default 180 days), JSON `{ v, f, a, m }` |
121
+ | 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
+ | Google Consent Mode v2 | Optional denied defaults in `<head>` + `gtag('consent','update',…)` including `functionality_storage` |
124
+ | 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`) |
126
+
127
+ ## Capabilities
128
+
129
+ | Capability | Why |
130
+ |------------|-----|
131
+ | `hooks.page-fragments:register` | Public banner, styles, and consent bootstrap scripts |
132
+
133
+ Settings, KV, and declared plugin storage need no extra capability. Native plugins are not an isolation boundary: review and install this package like first-party site code.
66
134
 
67
135
  ## Legal disclaimer
68
136
 
69
137
  EmPrivacy helps you implement **technical** consent UX and script loading patterns used for regulations such as **GDPR** and **CCPA/CIPA**. **You** remain responsible for:
70
138
 
71
- - Privacy notices, cookie policies, and lawful bases
72
- - What data your analytics/marketing vendors collect
73
- - Whether your configuration meets obligations in your jurisdictions
139
+ - Privacy notices, cookie policies, and lawful bases
140
+ - What data your analytics/marketing vendors collect
141
+ - Whether your configuration meets obligations in your jurisdictions
142
+ - Any scripts, pixels, or iframes you add outside this plugin
74
143
 
75
144
  **This project does not provide legal advice.** If you need certainty, consult a qualified attorney or privacy professional.
76
145
 
77
146
  ## Registration order
78
147
 
79
- Put **`emprivacyPlugin()` early** in the `plugins` array so `page:metadata` / `page:fragments` run before other plugins that add trackers or metadata.
148
+ - **Consent Mode / other head scripts:** `emprivacyPlugin()` **first** in `plugins`.
149
+ - **Automatic embed placeholders:** `emprivacyPlugin()` **after** `@emdash-cms/plugin-embeds`.
150
+ - **Both:** EmPrivacy first + site-level `PortableText` `components` (see Quick start).
80
151
 
81
152
  ## Development
82
153
 
83
154
  ```bash
84
- npm install
85
- npm run typecheck # TypeScript
86
- npm run build # ESM + types in dist/
87
- npm test # Unit tests (see Testing)
88
- npm run test:watch # Vitest in watch mode (optional)
155
+ pnpm install
156
+ pnpm run typecheck # TypeScript
157
+ pnpm run build # ESM + types in dist/
158
+ pnpm test # Unit tests (see Testing)
159
+ pnpm run test:watch # Vitest in watch mode (optional)
89
160
  ```
90
161
 
91
- Outputs ESM under `dist/` with typings, matching the [plugin layout](https://www.npmjs.com/package/@emdash-cms/plugin-audit-log) used by other EmDash packages.
162
+ Outputs ESM under `dist/` with typings. Layout follows EmDash’s [native plugin packaging](https://docs.emdashcms.com/plugins/creating-native-plugins/distributing/) (`emprivacyPlugin()` descriptor + named `createPlugin()`, plus `./astro` for Portable Text).
92
163
 
93
- Dependency, audit, and deprecation context (Kysely override, `npm audit`): [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).
164
+ Dependency, audit, and deprecation context: [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).
94
165
 
95
166
  ## Testing
96
167
 
@@ -98,47 +169,32 @@ Dependency, audit, and deprecation context (Kysely override, `npm audit`): [docs
98
169
 
99
170
  | Command | Purpose |
100
171
  |---------|---------|
101
- | `npm run typecheck` | Types against `emdash` and this package |
102
- | `npm run build` | `tsdown` → `dist/` and `exports` |
103
- | `npm test` | [Vitest](https://vitest.dev/) tests for `src/config.ts`: URL lists, `normalizeConfig`, policy **path vs https** validation, `resolvePolicyHref`, `jsonForHtmlScript` |
172
+ | `pnpm run typecheck` | Types against `emdash` and this package |
173
+ | `pnpm run build` | `tsdown` → `dist/` and `exports` |
174
+ | `pnpm test` | Vitest: config validation, embed URL allowlists, i18n, vendors, JSON escaping |
104
175
 
105
176
  Automation does **not** start EmDash or a browser; it guards packaging and validation logic.
106
177
 
107
178
  **Suggested CI:**
108
179
 
109
180
  ```bash
110
- npm ci
111
- npm run typecheck
112
- npm run build
113
- npm test
181
+ pnpm install --frozen-lockfile
182
+ pnpm run typecheck
183
+ pnpm run build
184
+ pnpm test
114
185
  ```
115
186
 
116
187
  ### Staging / manual (before production)
117
188
 
118
- EmPrivacy uses EmDash KV, admin Block Kit, `page:fragments`, and plugin routes. Validate on a **real EmDash site** (staging) at the EmDash version you deploy.
119
-
120
- - **[docs/TESTING.md](docs/TESTING.md)** — Full pre-deploy checklist, flows to exercise, limits of automation.
121
- - **No E2E in this repo** — nothing here launches EmDash or drives a browser.
122
-
123
- ### Acceptance checklist (manual QA)
189
+ Validate on a **real EmDash site** (staging) at the EmDash version you deploy.
124
190
 
125
- Details: [docs/TESTING.md](docs/TESTING.md).
126
-
127
- - [ ] `typecheck`, `build`, and `npm test` pass
128
- - [ ] Banner when no cookie or **Policy / consent version** mismatch
129
- - [ ] **Privacy (and cookie) links** open the right EmDash Page or external URL; paths match what you see in the browser for that Page
130
- - [ ] Strict mode: no analytics/marketing scripts until consent (Network tab)
131
- - [ ] Cookie persists; bumping policy version shows the banner again
132
- - [ ] Admin save updates the public banner
133
- - [ ] Optional: server logging + recent records in admin
134
- - [ ] Optional: Google Consent Mode (Tag Assistant / [Google](https://support.google.com/tagmanager/answer/13695607))
135
- - [ ] **Trusted** `plugins: []` (see [Requirements](#requirements))
191
+ - **[docs/TESTING.md](docs/TESTING.md)** — Full pre-deploy checklist.
192
+ - **[docs/GETTING_STARTED.md](docs/GETTING_STARTED.md)** — Install and configure as a site manager.
193
+ - **[docs/PLUGIN_SETTINGS.md](docs/PLUGIN_SETTINGS.md)** — Every admin field.
136
194
 
137
195
  ## Releases
138
196
 
139
- npm releases follow **[Semantic Versioning](https://semver.org/)** (`MAJOR.MINOR.PATCH`). Tags are `v`-prefixed (e.g. `v0.1.0`). See [docs/RELEASES.md](docs/RELEASES.md).
140
-
141
- **EmDash upgrades:** say `update to latest emdash release` in Cursor (skill: [`.cursor/skills/emdash-release`](.cursor/skills/emdash-release/SKILL.md)) to bump, test, and publish to npm + GitHub without a manual merge gate.
197
+ npm releases follow **[Semantic Versioning](https://semver.org/)** (`MAJOR.MINOR.PATCH`). Tags are `v`-prefixed (e.g. `v1.0.0`). See [docs/RELEASES.md](docs/RELEASES.md).
142
198
 
143
199
  ## License
144
200
 
package/dist/index.d.mts CHANGED
@@ -1,11 +1,262 @@
1
- import { a as EmprivacyConfig, c as isRootRelativeSitePath, d as resolvePolicyHref, i as DEFAULT_CONFIG, l as isValidPolicyHrefInput, n as COOKIE_NAME, o as KV_KEY, r as ConsentRecordPayload, s as assertValidCloudflareToken, t as AnalyticsProvider, u as normalizeConfig } from "./config-CqOtRoQR.mjs";
2
1
  import { PluginDescriptor } from "emdash";
3
-
2
+ //#region src/runtime.d.ts
3
+ export declare function createPlugin(): import("emdash").ResolvedPlugin<{
4
+ consentEvents: {
5
+ indexes: string[];
6
+ };
7
+ }>;
8
+ //#endregion
9
+ //#region src/vendors.d.ts
10
+ type ConsentCategory = "essential" | "functional" | "analytics" | "marketing";
11
+ interface VendorRow {
12
+ id: string;
13
+ name: string;
14
+ category: ConsentCategory;
15
+ purpose: string;
16
+ /** First-party docs we control the URL for — never taken from admin HTML */
17
+ policyUrl?: string;
18
+ }
19
+ type AnalyticsLoader = {
20
+ type: "none";
21
+ } | {
22
+ type: "cloudflare";
23
+ token: string;
24
+ } | {
25
+ type: "custom";
26
+ scripts: {
27
+ src: string;
28
+ integrity: string | null;
29
+ }[];
30
+ } | {
31
+ type: "plausible";
32
+ src: string;
33
+ domain: string;
34
+ } | {
35
+ type: "fathom";
36
+ src: string;
37
+ siteId: string;
38
+ } | {
39
+ type: "umami";
40
+ src: string;
41
+ websiteId: string;
42
+ } | {
43
+ type: "simpleanalytics";
44
+ src: string;
45
+ } | {
46
+ type: "ga4";
47
+ measurementId: string;
48
+ } |
49
+ /** Loaded only after Marketing consent — GTM can fire advertising tags. */
50
+ {
51
+ type: "gtm";
52
+ containerId: string;
53
+ };
54
+ export declare function buildAnalyticsLoader(cfg: EmprivacyConfig): AnalyticsLoader;
55
+ export declare function buildVendorList(cfg: EmprivacyConfig): VendorRow[];
56
+ //#endregion
57
+ //#region src/i18n.d.ts
58
+ interface EmprivacyChromeStrings {
59
+ acceptAll: string;
60
+ rejectNonEssential: string;
61
+ customize: string;
62
+ saveChoices: string;
63
+ close: string;
64
+ essential: string;
65
+ functional: string;
66
+ analytics: string;
67
+ marketing: string;
68
+ essentialNote: string;
69
+ privacyPolicy: string;
70
+ cookiePolicy: string;
71
+ cookieSettings: string;
72
+ loadEmbed: string;
73
+ embedBlocked: string;
74
+ embedNeedConsent: string;
75
+ vendorsHeading: string;
76
+ whatWeUse: string;
77
+ }
78
+ interface EmprivacyLocaleCopy {
79
+ bannerTitle: string;
80
+ bannerMessage: string;
81
+ }
82
+ type LocaleOverrides = Record<string, Partial<EmprivacyLocaleCopy>>;
83
+ //#endregion
84
+ //#region src/theme.d.ts
85
+ interface EmprivacyTheme {
86
+ bg: string;
87
+ text: string;
88
+ accent: string;
89
+ radiusPx: number;
90
+ }
91
+ //#endregion
92
+ //#region src/config.d.ts
93
+ export declare const KV_KEY: "consent:config";
94
+ export declare const COOKIE_NAME: "emprivacy_cc";
95
+ type AnalyticsProvider = "cloudflare" | "none" | "custom" | "plausible" | "fathom" | "umami" | "simpleanalytics" | "ga4" | "gtm";
96
+ type EmbedCategory = "functional" | "marketing";
97
+ interface EmprivacyConfig {
98
+ bannerTitle: string;
99
+ bannerMessage: string;
100
+ /**
101
+ * Privacy policy location: full `https://…` URL **or** a root-relative path to an EmDash **Page**
102
+ * (e.g. `/privacy`) resolved with `ctx.url()` on the server.
103
+ */
104
+ privacyPolicyUrl: string;
105
+ /** Optional cookie policy: same rules as `privacyPolicyUrl` (https URL or path like `/cookies`) */
106
+ cookiePolicyUrl: string;
107
+ /** When true, analytics/marketing/functional require explicit opt-in in the UI */
108
+ strictDefaults: boolean;
109
+ /** Bump to invalidate client consent cookies and re-show the banner */
110
+ policyVersion: string;
111
+ /**
112
+ * Analytics when `cloudflare` — **site token** from Cloudflare → Web Analytics (injected with `data-cf-beacon`).
113
+ * Public in the same way as a normal site embed. Empty means no Cloudflare script until a token is saved.
114
+ */
115
+ cloudflareWebAnalyticsToken: string;
116
+ analyticsProvider: AnalyticsProvider;
117
+ /** Provider-specific id (domain, site id, G-…, GTM-…). */
118
+ analyticsId: string;
119
+ /** Required when `analyticsProvider` is `umami` — https script src only. */
120
+ umamiScriptUrl: string;
121
+ /** When `analyticsProvider` is `custom`, these https script URLs load after analytics consent. */
122
+ analyticsScriptUrls: string[];
123
+ /** Third-party script URLs loaded only after marketing consent (https only) */
124
+ marketingScriptUrls: string[];
125
+ /** Optional SRI hashes keyed by script src (sha256/384/512-…) */
126
+ scriptIntegrity: Record<string, string>;
127
+ /**
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.
130
+ */
131
+ scriptHostAllowlist: string[];
132
+ /** Consent cookie Max-Age in days (1–365, default 180) */
133
+ cookieMaxAgeDays: number;
134
+ /** Emit Google Consent Mode v2 defaults (denied) in head; updates after choice */
135
+ googleConsentMode: boolean;
136
+ /** POST consent snapshots to the plugin record route and optional storage */
137
+ logConsentToServer: boolean;
138
+ /** Category that official embed placeholders require */
139
+ embedCategory: EmbedCategory;
140
+ /** When true, EmPrivacy Portable Text components gate official embed blocks */
141
+ gateEmbeds: boolean;
142
+ /** Hide the first-visit banner on privacy/cookie policy paths */
143
+ hideBannerOnPolicyPages: boolean;
144
+ defaultLocale: string;
145
+ localeOverrides: LocaleOverrides;
146
+ theme: EmprivacyTheme;
147
+ }
148
+ export declare const DEFAULT_CONFIG: EmprivacyConfig;
149
+ /** Reject characters that would break JSON or attributes; allow typical Cloudflare site token strings. */
150
+ export declare function assertValidCloudflareToken(t: string): void;
151
+ /**
152
+ * Root-relative public path (EmDash page route), e.g. `/privacy`.
153
+ * Rejects protocol-relative URLs (`//…`) and whitespace.
154
+ */
155
+ export declare function isRootRelativeSitePath(s: string): boolean;
156
+ /** For hooks: turn stored path or absolute URL into a public absolute URL. */
157
+ export declare function resolvePolicyHref(stored: string, ctx: {
158
+ url: (path: string) => string;
159
+ }): string;
160
+ export declare function isValidPolicyHrefInput(s: string): boolean;
161
+ /** True when the public page is the configured privacy or cookie policy. */
162
+ export declare function isPolicyPagePath(pagePath: string, cfg: Pick<EmprivacyConfig, "privacyPolicyUrl" | "cookiePolicyUrl">): boolean;
163
+ export declare function normalizeConfig(raw: unknown): EmprivacyConfig;
164
+ interface ConsentRecordPayload {
165
+ createdAt: string;
166
+ policyVersion: string;
167
+ functional: boolean;
168
+ analytics: boolean;
169
+ marketing: boolean;
170
+ }
171
+ interface ConsentState {
172
+ v: string;
173
+ essential: true;
174
+ functional: boolean;
175
+ analytics: boolean;
176
+ marketing: boolean;
177
+ }
178
+ interface VendorPublicRow {
179
+ id: string;
180
+ name: string;
181
+ category: "essential" | "functional" | "analytics" | "marketing";
182
+ purpose: string;
183
+ policyUrl?: string;
184
+ }
185
+ interface EmprivacyPublicRuntimeConfig {
186
+ bannerTitle: string;
187
+ bannerMessage: string;
188
+ privacyPolicyUrl: string;
189
+ cookiePolicyUrl: string;
190
+ strictDefaults: boolean;
191
+ policyVersion: string;
192
+ googleConsentMode: boolean;
193
+ logConsent: boolean;
194
+ recordPath: string;
195
+ embedCategory: EmbedCategory;
196
+ gateEmbeds: boolean;
197
+ hideBanner: boolean;
198
+ theme: EmprivacyTheme;
199
+ ui: EmprivacyChromeStrings;
200
+ loader: AnalyticsLoader;
201
+ marketingScripts: {
202
+ src: string;
203
+ integrity: string | null;
204
+ }[];
205
+ scriptHostAllowlist: string[];
206
+ scriptIntegrity: Record<string, string>;
207
+ cookieMaxAge: number;
208
+ vendors: VendorPublicRow[];
209
+ }
210
+ //#endregion
211
+ //#region src/public-api.d.ts
212
+ type EmprivacyCategory = "essential" | "functional" | "analytics" | "marketing";
213
+ /**
214
+ * Public browser API attached to `window.emprivacy`.
215
+ * Other plugins and themes should use this instead of reading the cookie directly.
216
+ */
217
+ interface EmprivacyBrowserApi {
218
+ get(): ConsentState | null;
219
+ has(category: EmprivacyCategory): boolean;
220
+ onChange(cb: (state: ConsentState) => void): () => void;
221
+ open(): void;
222
+ }
223
+ declare global {
224
+ interface Window {
225
+ emprivacy?: EmprivacyBrowserApi;
226
+ }
227
+ }
228
+ //#endregion
229
+ //#region src/astro/embed-resolve.d.ts
230
+ /**
231
+ * Validate official EmDash embed block payloads and produce a first-party
232
+ * placeholder plan. Never returns a URL we did not construct or allowlist.
233
+ */
234
+ export declare const EMBED_BLOCK_TYPES: readonly ["youtube", "vimeo", "tweet", "bluesky", "mastodon", "linkPreview", "gist"];
235
+ type EmbedBlockType = (typeof EMBED_BLOCK_TYPES)[number];
236
+ type EmbedRenderMode = "iframe" | "link";
237
+ interface ResolvedEmbed {
238
+ kind: EmbedBlockType;
239
+ mode: EmbedRenderMode;
240
+ /** https iframe src or link href — always produced by this module */
241
+ src: string;
242
+ label: string;
243
+ poster: string | null;
244
+ }
245
+ export declare function resolveEmbed(node: {
246
+ _type?: unknown;
247
+ id?: unknown;
248
+ poster?: unknown;
249
+ title?: unknown;
250
+ }): ResolvedEmbed | null;
251
+ //#endregion
4
252
  //#region src/index.d.ts
5
253
  /**
6
- * EmDash plugin descriptor — add to `plugins: []` in `emdash({ ... })` inside `astro.config`.
7
- * Requires **trusted** registration so `page:metadata` / `page:fragments` run (sandboxed plugins cannot inject fragments).
254
+ * EmDash native plugin descriptor — add to `plugins: []` in `emdash({ ... })` inside `astro.config`.
255
+ *
256
+ * EmPrivacy uses `page:fragments` (banner + consent scripts) and optional Portable Text
257
+ * embed placeholders, which only run for **native** plugins in `plugins: []`.
258
+ * Do not place this descriptor in `sandboxed: []`.
8
259
  */
9
- declare function emprivacyPlugin(): PluginDescriptor;
260
+ export declare function emprivacyPlugin(): PluginDescriptor;
10
261
  //#endregion
11
- export { type AnalyticsProvider, COOKIE_NAME, type ConsentRecordPayload, DEFAULT_CONFIG, type EmprivacyConfig, KV_KEY, assertValidCloudflareToken, emprivacyPlugin, isRootRelativeSitePath, isValidPolicyHrefInput, normalizeConfig, resolvePolicyHref };
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 };