@emplugins/emprivacy 1.0.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/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ SPDX-License-Identifier: MIT
2
+
3
+ MIT License
4
+
5
+ Copyright (c) 2026 EmPrivacy contributors
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ <!-- SPDX-License-Identifier: MIT -->
2
+ ![EmPrivacy Logo](./artwork/logo/EmPrivacy.png)
3
+ # EmPrivacy
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)).
6
+
7
+ ## Requirements
8
+
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/).
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @emplugins/emprivacy
17
+ ```
18
+
19
+ Published under the [emplugins](https://www.npmjs.com/org/emplugins) npm org. Source: [github.com/EmPlugins/EmPrivacy](https://github.com/EmPlugins/EmPrivacy).
20
+
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).
22
+
23
+ The legacy unscoped package `emprivacy` is deprecated; migrate imports to `@emplugins/emprivacy`.
24
+
25
+ ## Quick start
26
+
27
+ 1. **Register the plugin** in `astro.config.mjs` (or `.ts`) **before** other plugins that inject marketing or analytics, so consent runs first:
28
+
29
+ ```ts
30
+ import { defineConfig } from "astro/config";
31
+ import { emdash } from "emdash/astro";
32
+ import { emprivacyPlugin } from "@emplugins/emprivacy";
33
+
34
+ export default defineConfig({
35
+ integrations: [
36
+ emdash({
37
+ plugins: [emprivacyPlugin()],
38
+ }),
39
+ ],
40
+ });
41
+ ```
42
+
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/).
44
+
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).
49
+
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.”
51
+
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.
53
+
54
+ ## Features
55
+
56
+ | Feature | Description |
57
+ |--------|-------------|
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) |
66
+
67
+ ## Legal disclaimer
68
+
69
+ 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
+
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
74
+
75
+ **This project does not provide legal advice.** If you need certainty, consult a qualified attorney or privacy professional.
76
+
77
+ ## Registration order
78
+
79
+ Put **`emprivacyPlugin()` early** in the `plugins` array so `page:metadata` / `page:fragments` run before other plugins that add trackers or metadata.
80
+
81
+ ## Development
82
+
83
+ ```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)
89
+ ```
90
+
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.
92
+
93
+ Dependency, audit, and deprecation context (Kysely override, `npm audit`): [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).
94
+
95
+ ## Testing
96
+
97
+ ### Automated (local or CI)
98
+
99
+ | Command | Purpose |
100
+ |---------|---------|
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` |
104
+
105
+ Automation does **not** start EmDash or a browser; it guards packaging and validation logic.
106
+
107
+ **Suggested CI:**
108
+
109
+ ```bash
110
+ npm ci
111
+ npm run typecheck
112
+ npm run build
113
+ npm test
114
+ ```
115
+
116
+ ### Staging / manual (before production)
117
+
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)
124
+
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))
136
+
137
+ ## Releases
138
+
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.
142
+
143
+ ## License
144
+
145
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,166 @@
1
+ //#region src/config.ts
2
+ /**
3
+ * EmPrivacy KV document (`consent:config`) and shared helpers.
4
+ */
5
+ const KV_KEY = "consent:config";
6
+ const COOKIE_NAME = "emprivacy_cc";
7
+ const PLUGIN_ID = "emprivacy";
8
+ const DEFAULT_CONFIG = {
9
+ bannerTitle: "Cookies & privacy",
10
+ bannerMessage: "We use cookies to run this site and optionally for analytics and marketing. You can accept or customize your choices.",
11
+ privacyPolicyUrl: "/privacy",
12
+ cookiePolicyUrl: "",
13
+ strictDefaults: true,
14
+ policyVersion: "1",
15
+ cloudflareWebAnalyticsToken: "",
16
+ analyticsProvider: "cloudflare",
17
+ analyticsScriptUrls: [],
18
+ marketingScriptUrls: [],
19
+ googleConsentMode: false,
20
+ logConsentToServer: false
21
+ };
22
+ function hasUnsafeUrlChars(s) {
23
+ return /[\u0000-\u001F\u007F\s]/.test(s);
24
+ }
25
+ function isHttpsUrl(s) {
26
+ try {
27
+ const t = s.trim();
28
+ if (!t) return false;
29
+ if (hasUnsafeUrlChars(t)) return false;
30
+ if (/%0d|%0a|%09|%0b|%0c|%20/i.test(t)) return false;
31
+ const u = new URL(t);
32
+ if (u.protocol !== "https:") return false;
33
+ if (u.username || u.password) return false;
34
+ return true;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+ function invalidScriptUrlError(kind, u) {
40
+ if (u.includes("<") || u.includes(">")) return `Invalid ${kind} script URL (https only). Paste one full https:// URL per line (the script src only), not a <script> tag or HTML comment. Example: https://static.cloudflareinsights.com/beacon.min.js. The invalid line was: ${u}`;
41
+ return `Invalid ${kind} script URL (https only): ${u}`;
42
+ }
43
+ function isAnalyticsProvider(s) {
44
+ return s === "cloudflare" || s === "none" || s === "custom";
45
+ }
46
+ /** Reject characters that would break JSON or attributes; allow typical Cloudflare site token strings. */
47
+ function assertValidCloudflareToken(t) {
48
+ const s = t.trim();
49
+ if (s.length === 0) return;
50
+ if (s.length > 256) throw new Error("Cloudflare site token is too long.");
51
+ if (/[\s<>'"&]/.test(s)) throw new Error("Cloudflare site token contains invalid characters.");
52
+ }
53
+ /**
54
+ * Root-relative public path (EmDash page route), e.g. `/privacy`.
55
+ * Rejects protocol-relative URLs (`//…`) and whitespace.
56
+ */
57
+ function isRootRelativeSitePath(s) {
58
+ const t = s.trim();
59
+ if (t.length === 0) return false;
60
+ if (!t.startsWith("/")) return false;
61
+ if (t.startsWith("//")) return false;
62
+ if (hasUnsafeUrlChars(t)) return false;
63
+ return true;
64
+ }
65
+ /** For hooks: turn stored path or absolute URL into a public absolute URL. */
66
+ function resolvePolicyHref(stored, ctx) {
67
+ const t = stored.trim();
68
+ if (!t) return t;
69
+ if (isRootRelativeSitePath(t)) return ctx.url(t);
70
+ return t;
71
+ }
72
+ function isValidPolicyHrefInput(s) {
73
+ const t = s.trim();
74
+ if (!t) return false;
75
+ return isHttpsUrl(t) || isRootRelativeSitePath(t);
76
+ }
77
+ function parseUrlList(text) {
78
+ const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
79
+ return [...new Set(lines)];
80
+ }
81
+ function assertLength(label, s, min, max) {
82
+ const t = s.trim();
83
+ if (t.length < min) throw new Error(`${label} is required.`);
84
+ if (t.length > max) throw new Error(`${label} is too long.`);
85
+ }
86
+ function assertOptionalLength(label, s, max) {
87
+ if (s.trim().length > max) throw new Error(`${label} is too long.`);
88
+ }
89
+ function normalizeConfig(raw) {
90
+ if (typeof raw !== "object" || raw === null) return { ...DEFAULT_CONFIG };
91
+ const o = raw;
92
+ const analytics = Array.isArray(o.analyticsScriptUrls) ? o.analyticsScriptUrls.filter((x) => typeof x === "string") : DEFAULT_CONFIG.analyticsScriptUrls;
93
+ const marketing = Array.isArray(o.marketingScriptUrls) ? o.marketingScriptUrls.filter((x) => typeof x === "string") : DEFAULT_CONFIG.marketingScriptUrls;
94
+ const cfTok = typeof o.cloudflareWebAnalyticsToken === "string" ? o.cloudflareWebAnalyticsToken : "";
95
+ const apCandidate = typeof o.analyticsProvider === "string" ? o.analyticsProvider.trim() : "";
96
+ let analyticsProvider;
97
+ if (isAnalyticsProvider(apCandidate)) analyticsProvider = apCandidate;
98
+ else if (analytics.length > 0) analyticsProvider = "custom";
99
+ else analyticsProvider = DEFAULT_CONFIG.analyticsProvider;
100
+ return {
101
+ bannerTitle: typeof o.bannerTitle === "string" ? o.bannerTitle : DEFAULT_CONFIG.bannerTitle,
102
+ bannerMessage: typeof o.bannerMessage === "string" ? o.bannerMessage : DEFAULT_CONFIG.bannerMessage,
103
+ privacyPolicyUrl: typeof o.privacyPolicyUrl === "string" ? o.privacyPolicyUrl : DEFAULT_CONFIG.privacyPolicyUrl,
104
+ cookiePolicyUrl: typeof o.cookiePolicyUrl === "string" ? o.cookiePolicyUrl : DEFAULT_CONFIG.cookiePolicyUrl,
105
+ strictDefaults: typeof o.strictDefaults === "boolean" ? o.strictDefaults : DEFAULT_CONFIG.strictDefaults,
106
+ policyVersion: typeof o.policyVersion === "string" && o.policyVersion.length > 0 ? o.policyVersion : DEFAULT_CONFIG.policyVersion,
107
+ cloudflareWebAnalyticsToken: cfTok,
108
+ analyticsProvider,
109
+ analyticsScriptUrls: analytics,
110
+ marketingScriptUrls: marketing,
111
+ googleConsentMode: typeof o.googleConsentMode === "boolean" ? o.googleConsentMode : DEFAULT_CONFIG.googleConsentMode,
112
+ logConsentToServer: typeof o.logConsentToServer === "boolean" ? o.logConsentToServer : DEFAULT_CONFIG.logConsentToServer
113
+ };
114
+ }
115
+ /** Validate admin-submitted fields; throws an Error with a short message on failure */
116
+ function assertValidSavedConfig(input) {
117
+ assertLength("Policy version", input.policyVersion, 1, 64);
118
+ assertOptionalLength("Banner title", input.bannerTitle, 120);
119
+ assertOptionalLength("Short notice", input.bannerMessage, 600);
120
+ const privacyPolicyUrl = input.privacyPolicyUrl.trim();
121
+ if (!isValidPolicyHrefInput(privacyPolicyUrl)) throw new Error("Privacy policy must be a full https:// URL or a site path to your EmDash Page (e.g. /privacy).");
122
+ if (privacyPolicyUrl.length > 2048) throw new Error("Privacy policy URL/path is too long.");
123
+ let cookiePolicyUrl = input.cookiePolicyUrl.trim();
124
+ if (cookiePolicyUrl && !isValidPolicyHrefInput(cookiePolicyUrl)) throw new Error("Cookie policy must be a valid https:// URL or a root-relative path (e.g. /cookies), or empty.");
125
+ if (cookiePolicyUrl.length > 2048) throw new Error("Cookie policy URL/path is too long.");
126
+ if (!cookiePolicyUrl) cookiePolicyUrl = DEFAULT_CONFIG.cookiePolicyUrl;
127
+ const ap = input.analyticsPlatform.trim() || "cloudflare";
128
+ if (!isAnalyticsProvider(ap)) throw new Error("Analytics: choose a supported option (Cloudflare, None, or Custom).");
129
+ const tokenTrim = input.cloudflareToken.trim();
130
+ if (ap === "cloudflare") assertValidCloudflareToken(tokenTrim);
131
+ const analyticsScriptUrls = [];
132
+ if (ap === "custom") {
133
+ const list = parseUrlList(input.analyticsUrlsText);
134
+ if (list.length > 50) throw new Error("Analytics script URL list is too long (max 50).");
135
+ for (const u of list) {
136
+ if (u.length > 2048) throw new Error(invalidScriptUrlError("analytics", u));
137
+ if (!isHttpsUrl(u)) throw new Error(invalidScriptUrlError("analytics", u));
138
+ analyticsScriptUrls.push(u);
139
+ }
140
+ }
141
+ const marketingScriptUrls = [];
142
+ const mlist = parseUrlList(input.marketingUrlsText);
143
+ if (mlist.length > 50) throw new Error("Marketing script URL list is too long (max 50).");
144
+ for (const u of mlist) {
145
+ if (u.length > 2048) throw new Error(invalidScriptUrlError("marketing", u));
146
+ if (!isHttpsUrl(u)) throw new Error(invalidScriptUrlError("marketing", u));
147
+ marketingScriptUrls.push(u);
148
+ }
149
+ return normalizeConfig({
150
+ bannerTitle: input.bannerTitle.trim() || DEFAULT_CONFIG.bannerTitle,
151
+ bannerMessage: input.bannerMessage.trim() || DEFAULT_CONFIG.bannerMessage,
152
+ privacyPolicyUrl,
153
+ cookiePolicyUrl,
154
+ strictDefaults: input.strictDefaults,
155
+ policyVersion: input.policyVersion.trim(),
156
+ analyticsProvider: ap,
157
+ cloudflareWebAnalyticsToken: ap === "cloudflare" ? tokenTrim : "",
158
+ analyticsScriptUrls: ap === "custom" ? analyticsScriptUrls : [],
159
+ marketingScriptUrls,
160
+ googleConsentMode: input.googleConsentMode,
161
+ logConsentToServer: input.logConsentToServer
162
+ });
163
+ }
164
+
165
+ //#endregion
166
+ export { assertValidCloudflareToken as a, isValidPolicyHrefInput as c, PLUGIN_ID as i, normalizeConfig as l, DEFAULT_CONFIG as n, assertValidSavedConfig as o, KV_KEY as r, isRootRelativeSitePath as s, COOKIE_NAME as t, resolvePolicyHref as u };
@@ -0,0 +1,63 @@
1
+ //#region src/config.d.ts
2
+ /**
3
+ * EmPrivacy KV document (`consent:config`) and shared helpers.
4
+ */
5
+ declare const KV_KEY: "consent:config";
6
+ declare const COOKIE_NAME: "emprivacy_cc";
7
+ /** How analytics scripts are chosen after the user consents to analytics. */
8
+ type AnalyticsProvider = "cloudflare" | "none" | "custom";
9
+ interface EmprivacyConfig {
10
+ bannerTitle: string;
11
+ bannerMessage: string;
12
+ /**
13
+ * Privacy policy location: full `https://…` URL **or** a root-relative path to an EmDash **Page**
14
+ * (e.g. `/privacy`) resolved with `ctx.url()` on the server.
15
+ */
16
+ privacyPolicyUrl: string;
17
+ /** Optional cookie policy: same rules as `privacyPolicyUrl` (https URL or path like `/cookies`) */
18
+ cookiePolicyUrl: string;
19
+ /** When true, analytics/marketing require explicit opt-in in the UI */
20
+ strictDefaults: boolean;
21
+ /** Bump to invalidate client consent cookies and re-show the banner */
22
+ policyVersion: string;
23
+ /**
24
+ * Analytics when `cloudflare` — **site token** from Cloudflare → Web Analytics (injected with `data-cf-beacon`).
25
+ * Public in the same way as a normal site embed. Empty means no Cloudflare script until a token is saved.
26
+ */
27
+ cloudflareWebAnalyticsToken: string;
28
+ /**
29
+ * Which analytics load path to use after consent. `cloudflare` = beacon + `cloudflareWebAnalyticsToken`;
30
+ * `custom` = `analyticsScriptUrls` only; `none` = no third-party analytics scripts.
31
+ */
32
+ analyticsProvider: AnalyticsProvider;
33
+ /** When `analyticsProvider` is `custom`, these https script URLs load after analytics consent. */
34
+ analyticsScriptUrls: string[];
35
+ /** Third-party script URLs loaded only after marketing consent (https only) */
36
+ marketingScriptUrls: string[];
37
+ /** Emit Google Consent Mode v2 defaults (denied) in head; updates after choice */
38
+ googleConsentMode: boolean;
39
+ /** POST consent snapshots to the plugin record route and optional storage */
40
+ logConsentToServer: boolean;
41
+ }
42
+ declare const DEFAULT_CONFIG: EmprivacyConfig;
43
+ /** Reject characters that would break JSON or attributes; allow typical Cloudflare site token strings. */
44
+ declare function assertValidCloudflareToken(t: string): void;
45
+ /**
46
+ * Root-relative public path (EmDash page route), e.g. `/privacy`.
47
+ * Rejects protocol-relative URLs (`//…`) and whitespace.
48
+ */
49
+ declare function isRootRelativeSitePath(s: string): boolean;
50
+ /** For hooks: turn stored path or absolute URL into a public absolute URL. */
51
+ declare function resolvePolicyHref(stored: string, ctx: {
52
+ url: (path: string) => string;
53
+ }): string;
54
+ declare function isValidPolicyHrefInput(s: string): boolean;
55
+ declare function normalizeConfig(raw: unknown): EmprivacyConfig;
56
+ interface ConsentRecordPayload {
57
+ createdAt: string;
58
+ policyVersion: string;
59
+ analytics: boolean;
60
+ marketing: boolean;
61
+ }
62
+ //#endregion
63
+ export { EmprivacyConfig as a, isRootRelativeSitePath as c, resolvePolicyHref as d, DEFAULT_CONFIG as i, isValidPolicyHrefInput as l, COOKIE_NAME as n, KV_KEY as o, ConsentRecordPayload as r, assertValidCloudflareToken as s, AnalyticsProvider as t, normalizeConfig as u };
@@ -0,0 +1,11 @@
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
+ import { PluginDescriptor } from "emdash";
3
+
4
+ //#region src/index.d.ts
5
+ /**
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).
8
+ */
9
+ declare function emprivacyPlugin(): PluginDescriptor;
10
+ //#endregion
11
+ export { type AnalyticsProvider, COOKIE_NAME, type ConsentRecordPayload, DEFAULT_CONFIG, type EmprivacyConfig, KV_KEY, assertValidCloudflareToken, emprivacyPlugin, isRootRelativeSitePath, isValidPolicyHrefInput, normalizeConfig, resolvePolicyHref };
package/dist/index.mjs ADDED
@@ -0,0 +1,29 @@
1
+ import { a as assertValidCloudflareToken, c as isValidPolicyHrefInput, i as PLUGIN_ID, l as normalizeConfig, n as DEFAULT_CONFIG, r as KV_KEY, s as isRootRelativeSitePath, t as COOKIE_NAME, u as resolvePolicyHref } from "./config-BJ1LHuri.mjs";
2
+
3
+ //#region src/version.ts
4
+ const VERSION = "1.0.0";
5
+
6
+ //#endregion
7
+ //#region src/index.ts
8
+ /**
9
+ * EmDash plugin descriptor — add to `plugins: []` in `emdash({ ... })` inside `astro.config`.
10
+ * Requires **trusted** registration so `page:metadata` / `page:fragments` run (sandboxed plugins cannot inject fragments).
11
+ */
12
+ function emprivacyPlugin() {
13
+ return {
14
+ id: PLUGIN_ID,
15
+ version: VERSION,
16
+ format: "standard",
17
+ entrypoint: "@emplugins/emprivacy/sandbox",
18
+ capabilities: ["hooks.page-fragments:register"],
19
+ storage: { consentEvents: { indexes: ["createdAt", "policyVersion"] } },
20
+ adminPages: [{
21
+ path: "/settings",
22
+ label: "EmPrivacy",
23
+ icon: "shield"
24
+ }]
25
+ };
26
+ }
27
+
28
+ //#endregion
29
+ export { COOKIE_NAME, DEFAULT_CONFIG, KV_KEY, assertValidCloudflareToken, emprivacyPlugin, isRootRelativeSitePath, isValidPolicyHrefInput, normalizeConfig, resolvePolicyHref };