@easy-cookie/policy 0.0.0-stage → 0.1.1

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 easy-cookie contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,183 @@
1
- # Temporary Holding Version
1
+ # @easy-cookie/policy
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Generates the cookie section of your cookie policy from the same easy-cookie config that drives the banner. The banner tables, the policy and the cleanup rules can no longer drift apart.
4
+
5
+ It also renders full **legal documents** (cookie policy, privacy policy, terms) written in Markdown, with directives filled from the config. The consent server uses the same renderer. See [Legal documents](#legal-documents).
6
+
7
+ - DOM-free. It runs at build time, on the server, in a React Server Component or in a CLI.
8
+ - Output formats: an HTML string, Markdown, or React (`@easy-cookie/policy/react`).
9
+ - The HTML has class names only (`ec-policy*`) and no inline styles.
10
+ - Table headers ship in Italian and English. Other languages fall back to English, and every label can be overridden.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ pnpm add @easy-cookie/policy
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ### React / Next.js (server component)
21
+
22
+ ```tsx
23
+ // app/cookie-policy/page.tsx
24
+ import { CookiePolicy } from '@easy-cookie/policy/react';
25
+ import config from '../../easy-cookie.config';
26
+
27
+ export default function Page() {
28
+ return (
29
+ <article>
30
+ <h1>Cookie policy</h1>
31
+ <p>…your own introduction (data controller, legal basis, rights)…</p>
32
+ <CookiePolicy config={config} lang="it" />
33
+ </article>
34
+ );
35
+ }
36
+ ```
37
+
38
+ ### HTML string / Markdown
39
+
40
+ ```ts
41
+ import { buildPolicyModel, renderPolicyHtml, renderPolicyMarkdown } from '@easy-cookie/policy';
42
+
43
+ const model = buildPolicyModel(config, 'en'); // categories → services → cookies, localized
44
+ const html = renderPolicyHtml(model, { headingLevel: 3 });
45
+ const md = renderPolicyMarkdown(model);
46
+ ```
47
+
48
+ `buildPolicyModel(config, lang, { date?, labels? })`:
49
+
50
+ - The date defaults to the revision when the revision starts with `YYYY-MM-DD`.
51
+ - Cookies declared as regular expressions (`pattern: true`) are shown in a readable form, for example `^_ga_.*$` becomes `_ga_*`. The full pattern goes in the `title` attribute.
52
+
53
+ `renderPolicyHtml(model, opts)` options:
54
+
55
+ | Option | Default | |
56
+ |---|---|---|
57
+ | `headingLevel` | `2` | Heading level of the category titles |
58
+ | `idPrefix` | `ec-policy-` | Section ids, for example `#ec-policy-measurement` |
59
+ | `meta` | `true` | Shows the "Last updated · Revision" line |
60
+ | `hideEmpty` | `false` | Skips categories without services |
61
+
62
+ The output has this structure:
63
+
64
+ ```html
65
+ <div class="ec-policy" lang="en">
66
+ <p class="ec-policy__meta">Last updated: <time datetime="2026-11-13">November 13, 2026</time> · Revision: …</p>
67
+ <section class="ec-policy__category" id="ec-policy-measurement" data-category="measurement">
68
+ <h2 class="ec-policy__title">Measurement</h2>
69
+ <p class="ec-policy__description">…</p>
70
+ <div class="ec-policy__table-wrap">
71
+ <table class="ec-policy__table">
72
+ <caption class="ec-policy__caption">…</caption>
73
+ <thead><tr><th>Cookie</th><th>Service</th><th>Provider</th><th>Duration</th><th>Purpose</th></tr></thead>
74
+ <tbody><tr data-service="ga4"><th scope="row"><code class="ec-policy__cookie">_ga</code></th>…
75
+ <td class="ec-policy__provider"><a class="ec-policy__link" href="https://policies.google.com/privacy">Google Ireland Ltd</a></td>…
76
+ ```
77
+
78
+ - A service without cookies still gets a row, with `—` in the cookie column.
79
+ - A read-only category gets `.ec-policy__badge` ("Always on").
80
+ - All text is HTML-escaped.
81
+ - A privacy URL is linked only when it is `http(s):`, `mailto:` or relative.
82
+
83
+ ### autoClear rules
84
+
85
+ ```ts
86
+ import { autoClearRules, matchesRule } from '@easy-cookie/policy';
87
+
88
+ autoClearRules(config);
89
+ // { measurement: [{ name: '^_ga$', pattern: true, service: 'ga4' }, …], marketing: [...] }
90
+ ```
91
+
92
+ The function returns, per category, the cookies to delete when that category is withdrawn. Read-only categories are not included. `matchesRule(rule, cookieName)` tests one cookie name against a rule.
93
+
94
+ ## Legal documents
95
+
96
+ A legal document is Markdown text per language:
97
+
98
+ ```ts
99
+ interface LegalDocContent {
100
+ translations: Record<string, { title: string; body: string /* Markdown */ }>;
101
+ }
102
+ ```
103
+
104
+ ### Rendering
105
+
106
+ ```ts
107
+ import { renderLegalDocument } from '@easy-cookie/policy';
108
+
109
+ const { lang, title, html } = renderLegalDocument(content, 'it', {
110
+ config, // banner config for {{cookie-table}} / {{services-list}}
111
+ version: '2026-11-13', // optional, written as data-version
112
+ publishedAt: '2026-11-13', // for {{last-updated}}; defaults to today (previews)
113
+ site: { name: 'Acme', domain: 'acme.it' }, // for {{site.name}} / {{site.domain}} left in the text
114
+ });
115
+ ```
116
+
117
+ - The translation for `lang` is used. Otherwise the renderer falls back to the base language (`en-GB` → `en`), then to `config.language.default`, then to the first translation. The result reports the language it used.
118
+ - Markdown is rendered with [markdown-it](https://github.com/markdown-it/markdown-it): raw HTML is **disabled** (it shows as text), bare URLs are linked, and links accept only `http:`, `https:`, `mailto:` and relative URLs. External links get `rel="noopener noreferrer"`.
119
+ - The output is `<div class="ec-legal" lang="…">` with an `<h1 class="ec-legal__title">` (pass `heading: false` to leave it out) followed by the body.
120
+
121
+ | Directive | Output |
122
+ |---|---|
123
+ | `{{cookie-table}}` (own line) | The per-category cookie tables of `renderPolicyHtml()` (`<h3>` titles, no meta line). Without `config`, a short notice. |
124
+ | `{{services-list}}` (own line) | `<ul class="ec-legal__services">`: label, provider, purpose, category, privacy link and, for services added from the catalog (`from`), the transfer countries and safeguard. Services without a `provider` (first party) are skipped. |
125
+ | `{{preferences-link}}` | `<a href="#" class="ec-legal__preferences" data-ec-open-preferences>`, which the core runtime binds to `openPreferences()`. With `preferencesHref`, a plain link to that URL instead (for pages without the banner). |
126
+ | `{{last-updated}}` | On its own line: "Ultimo aggiornamento: 13 novembre 2026" (`.ec-legal__updated`). Inline: just the `<time>`. |
127
+ | `{{site.name}}`, `{{site.domain}}` | The `site` values, when given. |
128
+
129
+ `[[Text to fill in]]` placeholders render as `<mark class="ec-legal__todo">Text to fill in</mark>`. `countPlaceholders(content)` counts the ones left.
130
+
131
+ Other helpers: `pickLegalLanguage`, `legalContentError` (validation), `fillSiteVariables`, `isSafeLegalUrl`, `LEGAL_KINDS`.
132
+
133
+ ### Built-in templates
134
+
135
+ `LEGAL_TEMPLATES[kind][lang]` holds IT and EN drafts for `cookie`, `privacy` and `terms`. `legalTemplate(kind, lang, site?)` returns one, with `{{site.*}}` filled in (unknown languages get English).
136
+
137
+ - **Privacy policy:** controller, data categories, purposes and legal bases (art. 6 GDPR), whether data must be provided, retention, recipients, `{{services-list}}`, transfers outside the EEA, rights (artt. 15–22), complaint to the Garante, contact.
138
+ - **Cookie policy:** what cookies are, how they are used (technical vs consent-based), categories, `{{cookie-table}}`, managing consent (`{{preferences-link}}`), browser settings, controller, last update.
139
+ - **Terms:** a generic skeleton for a site or service.
140
+
141
+ They are **starting drafts, not legal advice**. Every owner-specific value is a `[[placeholder]]`, and the first line of each template is a placeholder that asks for a professional review. Delete it once the text is reviewed.
142
+
143
+ ### Fetching published documents
144
+
145
+ ```ts
146
+ import { fetchLegalDocument } from '@easy-cookie/policy/client'; // tiny entry, no Markdown renderer
147
+
148
+ const doc = await fetchLegalDocument({ endpoint: 'https://consent.example.com', siteKey: 'pk_…', kind: 'privacy', lang: 'it' });
149
+ // { kind, lang, languages, title, html, version, publishedAt } or null when nothing is published
150
+ ```
151
+
152
+ `fetchLegalDocument` is also exported from the root and from `/react`. It accepts `init` (extra `fetch` options) and `fetch` (a custom implementation).
153
+
154
+ ### React
155
+
156
+ ```tsx
157
+ import { LegalDocument, LegalDocumentHtml } from '@easy-cookie/policy/react';
158
+
159
+ // Server-rendered (RSC, SSR): fetch first, then render.
160
+ <LegalDocumentHtml doc={doc} fallback={<p>Not published yet.</p>} />
161
+
162
+ // Client-side fetch (a "use client" component).
163
+ <LegalDocument endpoint="https://consent.example.com" siteKey="pk_…" kind="cookie" lang="it" loading={<p>…</p>} />
164
+ ```
165
+
166
+ For Next.js, `getLegalDocument()` in `@easy-cookie/next/server` wraps `fetchLegalDocument` with `next: { revalidate }`.
167
+
168
+ ### Stylesheet
169
+
170
+ `@easy-cookie/policy/legal.css` (`dist/easy-cookie-legal.css`, also served by the consent server at `/static/easy-cookie-legal.css`) is a small, neutral stylesheet for `.ec-legal`: readable measure, headings, lists, tables (including the cookie tables), the services list and the placeholder marks. All selectors use `:where()`, so your own rules always win. Restyle it with these variables, set on `:root`, on `.ec-legal` or on any ancestor:
171
+
172
+ | Variable | Default | Role |
173
+ |---|---|---|
174
+ | `--ec-legal-font` | `system-ui, sans-serif` | Font |
175
+ | `--ec-legal-ink` / `--ec-legal-muted` | `#1c1c1a` / `#5a5a55` | Text, secondary text |
176
+ | `--ec-legal-line` | `#dcdcd6` | Rules and table borders |
177
+ | `--ec-legal-accent` | `#1a56c4` | Links and focus ring |
178
+ | `--ec-legal-todo` | `#fff2a8` | Placeholder highlight |
179
+ | `--ec-legal-max-width` | `72ch` | Measure |
180
+
181
+ ## License
182
+
183
+ MIT
@@ -0,0 +1,23 @@
1
+ // src/legal-client.ts
2
+ var LEGAL_KINDS = ["cookie", "privacy", "terms"];
3
+ var isLegalKind = (v) => typeof v === "string" && LEGAL_KINDS.includes(v);
4
+ function legalDocumentUrl(opts) {
5
+ const base = opts.endpoint.replace(/\/+$/, "");
6
+ const q = opts.lang ? `?lang=${encodeURIComponent(opts.lang)}` : "";
7
+ return `${base}/v1/sites/${encodeURIComponent(opts.siteKey)}/legal/${encodeURIComponent(opts.kind)}.json${q}`;
8
+ }
9
+ async function fetchLegalDocument(opts) {
10
+ const f = opts.fetch ?? fetch;
11
+ const res = await f(legalDocumentUrl(opts), { credentials: "omit", ...opts.init });
12
+ if (res.status === 404) return null;
13
+ if (!res.ok) throw new Error(`[easy-cookie] could not load the ${opts.kind} document (${res.status})`);
14
+ return await res.json();
15
+ }
16
+
17
+ export {
18
+ LEGAL_KINDS,
19
+ isLegalKind,
20
+ legalDocumentUrl,
21
+ fetchLegalDocument
22
+ };
23
+ //# sourceMappingURL=chunk-HDY2VYU4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/legal-client.ts"],"sourcesContent":["/**\n * Legal document types and the fetch client. Kept free of the Markdown\n * renderer so browser bundles (React component, Next.js) stay small.\n */\n\n/** The three legal documents every site has. */\nexport type LegalKind = 'cookie' | 'privacy' | 'terms';\nexport const LEGAL_KINDS: readonly LegalKind[] = ['cookie', 'privacy', 'terms'];\nexport const isLegalKind = (v: unknown): v is LegalKind => typeof v === 'string' && (LEGAL_KINDS as readonly string[]).includes(v);\n\nexport interface LegalTranslation {\n title: string;\n /** Markdown, rendered with raw HTML disabled. */\n body: string;\n}\n\n/** Content of a legal document: one entry per language it is written in. */\nexport interface LegalDocContent {\n translations: Record<string, LegalTranslation>;\n}\n\n/** A published document as served by `GET /v1/sites/:publicKey/legal/:kind.json`. */\nexport interface PublishedLegalDocument {\n kind: LegalKind;\n /** Language actually shown (after fallback). */\n lang: string;\n /** Every language the document is written in. */\n languages: string[];\n title: string;\n /** Rendered, safe HTML wrapped in `<div class=\"ec-legal\" lang=\"…\">`. */\n html: string;\n version: string;\n publishedAt: string;\n}\n\nexport interface FetchLegalDocumentOptions {\n /** Base URL of the consent server. */\n endpoint: string;\n /** Public site key (`pk_…`). */\n siteKey: string;\n kind: LegalKind;\n lang?: string;\n /** Extra fetch options (e.g. Next's `next: { revalidate }`). */\n init?: RequestInit & Record<string, unknown>;\n /** Custom fetch implementation. */\n fetch?: typeof fetch;\n}\n\n/** URL of the published document JSON on the consent server. */\nexport function legalDocumentUrl(opts: Pick<FetchLegalDocumentOptions, 'endpoint' | 'siteKey' | 'kind' | 'lang'>): string {\n const base = opts.endpoint.replace(/\\/+$/, '');\n const q = opts.lang ? `?lang=${encodeURIComponent(opts.lang)}` : '';\n return `${base}/v1/sites/${encodeURIComponent(opts.siteKey)}/legal/${encodeURIComponent(opts.kind)}.json${q}`;\n}\n\n/**\n * Fetch a published legal document from the consent server.\n * Resolves to null when nothing is published yet (404); throws on other errors.\n */\nexport async function fetchLegalDocument(opts: FetchLegalDocumentOptions): Promise<PublishedLegalDocument | null> {\n const f = opts.fetch ?? fetch;\n const res = await f(legalDocumentUrl(opts), { credentials: 'omit', ...opts.init });\n if (res.status === 404) return null;\n if (!res.ok) throw new Error(`[easy-cookie] could not load the ${opts.kind} document (${res.status})`);\n return (await res.json()) as PublishedLegalDocument;\n}\n"],"mappings":";AAOO,IAAM,cAAoC,CAAC,UAAU,WAAW,OAAO;AACvE,IAAM,cAAc,CAAC,MAA+B,OAAO,MAAM,YAAa,YAAkC,SAAS,CAAC;AAyC1H,SAAS,iBAAiB,MAAyF;AACxH,QAAM,OAAO,KAAK,SAAS,QAAQ,QAAQ,EAAE;AAC7C,QAAM,IAAI,KAAK,OAAO,SAAS,mBAAmB,KAAK,IAAI,CAAC,KAAK;AACjE,SAAO,GAAG,IAAI,aAAa,mBAAmB,KAAK,OAAO,CAAC,UAAU,mBAAmB,KAAK,IAAI,CAAC,QAAQ,CAAC;AAC7G;AAMA,eAAsB,mBAAmB,MAAyE;AAChH,QAAM,IAAI,KAAK,SAAS;AACxB,QAAM,MAAM,MAAM,EAAE,iBAAiB,IAAI,GAAG,EAAE,aAAa,QAAQ,GAAG,KAAK,KAAK,CAAC;AACjF,MAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,oCAAoC,KAAK,IAAI,cAAc,IAAI,MAAM,GAAG;AACrG,SAAQ,MAAM,IAAI,KAAK;AACzB;","names":[]}
@@ -0,0 +1,224 @@
1
+ // src/model.ts
2
+ import { localize, resolveConfig } from "@easy-cookie/core/shared";
3
+ var POLICY_LABELS = {
4
+ it: {
5
+ cookie: "Cookie",
6
+ service: "Servizio",
7
+ provider: "Fornitore",
8
+ duration: "Durata",
9
+ purpose: "Finalit\xE0",
10
+ updated: "Ultimo aggiornamento",
11
+ revision: "Revisione",
12
+ alwaysOn: "Sempre attivi",
13
+ privacyPolicy: "Privacy policy",
14
+ noCookies: "Nessun cookie dichiarato in questa categoria.",
15
+ noCookie: "\u2014",
16
+ caption: "Cookie della categoria \xAB{category}\xBB"
17
+ },
18
+ en: {
19
+ cookie: "Cookie",
20
+ service: "Service",
21
+ provider: "Provider",
22
+ duration: "Duration",
23
+ purpose: "Purpose",
24
+ updated: "Last updated",
25
+ revision: "Revision",
26
+ alwaysOn: "Always on",
27
+ privacyPolicy: "Privacy policy",
28
+ noCookies: "No cookies declared in this category.",
29
+ noCookie: "\u2014",
30
+ caption: "Cookies in the \u201C{category}\u201D category"
31
+ }
32
+ };
33
+ function prettyCookiePattern(source) {
34
+ return source.replace(/^\^/, "").replace(/\$$/, "").replace(/\(([^()]*)\)\?/g, "[$1]").replace(/\.\*|\.\+|\[\^[^\]]*\]\*|\\d\+|\\w\+/g, "*").replace(/\\(.)/g, "$1");
35
+ }
36
+ function isoDate(value, revision) {
37
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? void 0 : value.toISOString().slice(0, 10);
38
+ if (typeof value === "string") return value.slice(0, 10);
39
+ const m = /^\d{4}-\d{2}-\d{2}/.exec(revision);
40
+ return m ? m[0] : void 0;
41
+ }
42
+ function buildPolicyModel(config, lang, opts = {}) {
43
+ const resolved = resolveConfig(config);
44
+ const fallback = resolved.language.default;
45
+ const l = (v) => localize(v, lang, fallback);
46
+ const labels = { ...POLICY_LABELS[lang] ?? POLICY_LABELS.en, ...opts.labels };
47
+ const services = config.services ?? {};
48
+ return {
49
+ lang,
50
+ revision: config.revision,
51
+ date: isoDate(opts.date, config.revision),
52
+ policyUrl: config.policyUrl ? l(config.policyUrl) : void 0,
53
+ labels,
54
+ categories: resolved.resolvedCategories.map((cat) => ({
55
+ id: cat.id,
56
+ title: l(cat.title),
57
+ description: l(cat.description),
58
+ readOnly: cat.readOnly,
59
+ color: cat.color,
60
+ services: cat.services.map((id) => {
61
+ const s = services[id];
62
+ return {
63
+ id,
64
+ label: s.label ? l(s.label) : id,
65
+ provider: s.provider ?? "",
66
+ privacyUrl: s.privacyUrl,
67
+ purpose: l(s.purpose),
68
+ cookies: (s.cookies ?? []).map((ck) => ({
69
+ name: ck.name,
70
+ displayName: ck.pattern ? prettyCookiePattern(ck.name) : ck.name,
71
+ pattern: !!ck.pattern,
72
+ duration: l(ck.duration),
73
+ description: l(ck.description),
74
+ domain: ck.domain
75
+ }))
76
+ };
77
+ })
78
+ }))
79
+ };
80
+ }
81
+ function formatPolicyDate(model) {
82
+ if (!model.date) return void 0;
83
+ const d = /* @__PURE__ */ new Date(`${model.date}T00:00:00Z`);
84
+ if (Number.isNaN(d.getTime())) return model.date;
85
+ try {
86
+ return new Intl.DateTimeFormat(model.lang, { dateStyle: "long", timeZone: "UTC" }).format(d);
87
+ } catch {
88
+ return model.date;
89
+ }
90
+ }
91
+ function autoClearRules(config) {
92
+ const out = {};
93
+ for (const [id, cat] of Object.entries(config.categories)) if (!cat.readOnly) out[id] = [];
94
+ for (const [service, s] of Object.entries(config.services ?? {})) {
95
+ const list = out[s.category];
96
+ if (!list) continue;
97
+ for (const ck of s.cookies ?? [])
98
+ list.push({ name: ck.name, pattern: !!ck.pattern, ...ck.domain ? { domain: ck.domain } : {}, service });
99
+ }
100
+ return out;
101
+ }
102
+ function matchesRule(rule, cookieName) {
103
+ if (!rule.pattern) return rule.name === cookieName;
104
+ try {
105
+ return new RegExp(rule.name).test(cookieName);
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ // src/render.ts
112
+ var ESC = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
113
+ function escapeHtml(value) {
114
+ return value.replace(/[&<>"']/g, (c) => ESC[c]);
115
+ }
116
+ function safeUrl(url) {
117
+ return /^(https?:|mailto:|\/|#)/i.test(url.trim()) ? url : void 0;
118
+ }
119
+ function policyRows(cat) {
120
+ const rows = [];
121
+ for (const service of cat.services) {
122
+ if (service.cookies.length === 0) rows.push({ service, first: true });
123
+ else service.cookies.forEach((cookie, i) => rows.push({ service, cookie, first: i === 0 }));
124
+ }
125
+ return rows;
126
+ }
127
+ function visible(model, opts) {
128
+ return opts.hideEmpty ? model.categories.filter((c) => c.services.length > 0) : model.categories;
129
+ }
130
+ function renderPolicyHtml(model, opts = {}) {
131
+ const h = `h${opts.headingLevel ?? 2}`;
132
+ const prefix = opts.idPrefix ?? "ec-policy-";
133
+ const L = model.labels;
134
+ const e = escapeHtml;
135
+ const out = [`<div class="ec-policy" lang="${e(model.lang)}">`];
136
+ if (opts.meta !== false) {
137
+ const parts = [];
138
+ const date = formatPolicyDate(model);
139
+ if (date) parts.push(`${e(L.updated)}: <time datetime="${e(model.date)}">${e(date)}</time>`);
140
+ parts.push(`${e(L.revision)}: <span class="ec-policy__revision">${e(model.revision)}</span>`);
141
+ out.push(`<p class="ec-policy__meta">${parts.join(" \xB7 ")}</p>`);
142
+ }
143
+ for (const cat of visible(model, opts)) {
144
+ const id = `${prefix}${cat.id}`;
145
+ out.push(
146
+ `<section class="ec-policy__category" id="${e(id)}" data-category="${e(cat.id)}" aria-labelledby="${e(id)}-title">`
147
+ );
148
+ out.push(`<${h} class="ec-policy__title" id="${e(id)}-title">${e(cat.title)}</${h}>`);
149
+ if (cat.readOnly) out.push(`<p class="ec-policy__badge">${e(L.alwaysOn)}</p>`);
150
+ if (cat.description) out.push(`<p class="ec-policy__description">${e(cat.description)}</p>`);
151
+ const rows = policyRows(cat);
152
+ if (rows.length === 0) {
153
+ out.push(`<p class="ec-policy__empty">${e(L.noCookies)}</p>`);
154
+ } else {
155
+ out.push('<div class="ec-policy__table-wrap">');
156
+ out.push('<table class="ec-policy__table">');
157
+ out.push(`<caption class="ec-policy__caption">${e(L.caption.replace("{category}", cat.title))}</caption>`);
158
+ out.push(
159
+ "<thead><tr>" + [L.cookie, L.service, L.provider, L.duration, L.purpose].map((t) => `<th scope="col">${e(t)}</th>`).join("") + "</tr></thead>"
160
+ );
161
+ out.push("<tbody>");
162
+ for (const { service, cookie } of rows) {
163
+ const name = cookie ? `<code class="ec-policy__cookie"${cookie.pattern ? ` title="${e(cookie.name)}"` : ""}>${e(cookie.displayName)}</code>` + (cookie.domain ? ` <span class="ec-policy__domain">${e(cookie.domain)}</span>` : "") : `<span class="ec-policy__none">${e(L.noCookie)}</span>`;
164
+ const url = service.privacyUrl ? safeUrl(service.privacyUrl) : void 0;
165
+ const provider = url ? `<a class="ec-policy__link" href="${e(url)}" rel="noopener noreferrer" target="_blank">${e(service.provider || L.privacyPolicy)}</a>` : e(service.provider);
166
+ const purpose = cookie?.description || service.purpose;
167
+ out.push(
168
+ `<tr data-service="${e(service.id)}"><th scope="row">${name}</th><td class="ec-policy__service">${e(service.label)}</td><td class="ec-policy__provider">${provider}</td><td class="ec-policy__duration">${e(cookie?.duration ?? "")}</td><td class="ec-policy__purpose">${e(purpose)}</td></tr>`
169
+ );
170
+ }
171
+ out.push("</tbody></table></div>");
172
+ }
173
+ out.push("</section>");
174
+ }
175
+ out.push("</div>");
176
+ return out.join("\n");
177
+ }
178
+ function mdCell(value) {
179
+ return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
180
+ }
181
+ function renderPolicyMarkdown(model, opts = {}) {
182
+ const hashes = "#".repeat(opts.headingLevel ?? 2);
183
+ const L = model.labels;
184
+ const out = [];
185
+ if (opts.meta !== false) {
186
+ const date = formatPolicyDate(model);
187
+ out.push([date ? `${L.updated}: ${date}` : "", `${L.revision}: ${model.revision}`].filter(Boolean).join(" \xB7 "), "");
188
+ }
189
+ for (const cat of visible(model, opts)) {
190
+ out.push(`${hashes} ${cat.title}`, "");
191
+ if (cat.readOnly) out.push(`*${L.alwaysOn}*`, "");
192
+ if (cat.description) out.push(cat.description, "");
193
+ const rows = policyRows(cat);
194
+ if (rows.length === 0) {
195
+ out.push(L.noCookies, "");
196
+ continue;
197
+ }
198
+ out.push(`| ${[L.cookie, L.service, L.provider, L.duration, L.purpose].map(mdCell).join(" | ")} |`);
199
+ out.push("| --- | --- | --- | --- | --- |");
200
+ for (const { service, cookie } of rows) {
201
+ const name = cookie ? `\`${cookie.displayName.replace(/`/g, "")}\`` : L.noCookie;
202
+ const provider = service.privacyUrl ? `[${mdCell(service.provider || L.privacyPolicy).replace(/[[\]]/g, "\\$&")}](${service.privacyUrl.replace(/[()\s]/g, encodeURIComponent)})` : mdCell(service.provider);
203
+ out.push(
204
+ `| ${name} | ${mdCell(service.label)} | ${provider} | ${mdCell(cookie?.duration ?? "")} | ${mdCell(cookie?.description || service.purpose)} |`
205
+ );
206
+ }
207
+ out.push("");
208
+ }
209
+ return out.join("\n").trimEnd() + "\n";
210
+ }
211
+
212
+ export {
213
+ POLICY_LABELS,
214
+ prettyCookiePattern,
215
+ buildPolicyModel,
216
+ formatPolicyDate,
217
+ autoClearRules,
218
+ matchesRule,
219
+ escapeHtml,
220
+ policyRows,
221
+ renderPolicyHtml,
222
+ renderPolicyMarkdown
223
+ };
224
+ //# sourceMappingURL=chunk-P3F5NQPR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/model.ts","../src/render.ts"],"sourcesContent":["import { localize, resolveConfig, type EasyCookieConfig } from '@easy-cookie/core/shared';\n\nexport interface PolicyLabels {\n cookie: string;\n service: string;\n provider: string;\n duration: string;\n purpose: string;\n /** Prefix of the \"last updated\" line. */\n updated: string;\n revision: string;\n alwaysOn: string;\n privacyPolicy: string;\n /** Shown in a category without services. */\n noCookies: string;\n /** Shown in the cookie column for a service that sets no cookies. */\n noCookie: string;\n /** Table caption: `{category}` is replaced with the category title. */\n caption: string;\n}\n\n/** Built-in table headers and labels. Unknown languages fall back to English. */\nexport const POLICY_LABELS: Record<'it' | 'en', PolicyLabels> = {\n it: {\n cookie: 'Cookie',\n service: 'Servizio',\n provider: 'Fornitore',\n duration: 'Durata',\n purpose: 'Finalità',\n updated: 'Ultimo aggiornamento',\n revision: 'Revisione',\n alwaysOn: 'Sempre attivi',\n privacyPolicy: 'Privacy policy',\n noCookies: 'Nessun cookie dichiarato in questa categoria.',\n noCookie: '—',\n caption: 'Cookie della categoria «{category}»',\n },\n en: {\n cookie: 'Cookie',\n service: 'Service',\n provider: 'Provider',\n duration: 'Duration',\n purpose: 'Purpose',\n updated: 'Last updated',\n revision: 'Revision',\n alwaysOn: 'Always on',\n privacyPolicy: 'Privacy policy',\n noCookies: 'No cookies declared in this category.',\n noCookie: '—',\n caption: 'Cookies in the “{category}” category',\n },\n};\n\nexport interface PolicyCookie {\n /** Name as declared (a regular expression source when `pattern` is true). */\n name: string;\n /** Human-friendly name: anchors stripped, `.*` shown as `*` for patterns. */\n displayName: string;\n pattern: boolean;\n duration: string;\n description: string;\n domain?: string;\n}\n\nexport interface PolicyService {\n id: string;\n label: string;\n provider: string;\n privacyUrl?: string;\n purpose: string;\n cookies: PolicyCookie[];\n}\n\nexport interface PolicyCategory {\n id: string;\n title: string;\n description: string;\n readOnly: boolean;\n color: string;\n services: PolicyService[];\n}\n\nexport interface PolicyModel {\n lang: string;\n revision: string;\n /** ISO date (YYYY-MM-DD) of the revision, when known. */\n date?: string;\n policyUrl?: string;\n labels: PolicyLabels;\n categories: PolicyCategory[];\n}\n\nexport interface BuildPolicyOptions {\n /** Date of this revision (ISO). Defaults to the revision itself when it starts with YYYY-MM-DD. */\n date?: string | Date;\n /** Override some labels. */\n labels?: Partial<PolicyLabels>;\n}\n\n/** Make a cookie-name regular expression readable: `^_ga_.*$` → `_ga_*`. */\nexport function prettyCookiePattern(source: string): string {\n return source\n .replace(/^\\^/, '')\n .replace(/\\$$/, '')\n .replace(/\\(([^()]*)\\)\\?/g, '[$1]')\n .replace(/\\.\\*|\\.\\+|\\[\\^[^\\]]*\\]\\*|\\\\d\\+|\\\\w\\+/g, '*')\n .replace(/\\\\(.)/g, '$1');\n}\n\nfunction isoDate(value: string | Date | undefined, revision: string): string | undefined {\n if (value instanceof Date) return Number.isNaN(value.getTime()) ? undefined : value.toISOString().slice(0, 10);\n if (typeof value === 'string') return value.slice(0, 10);\n const m = /^\\d{4}-\\d{2}-\\d{2}/.exec(revision);\n return m ? m[0] : undefined;\n}\n\n/**\n * Turn the config into a localized, render-agnostic cookie policy model:\n * categories → services → cookies. Pure and DOM-free.\n */\nexport function buildPolicyModel(config: EasyCookieConfig, lang: string, opts: BuildPolicyOptions = {}): PolicyModel {\n const resolved = resolveConfig(config);\n const fallback = resolved.language.default;\n const l = (v: Parameters<typeof localize>[0]) => localize(v, lang, fallback);\n const labels = { ...((POLICY_LABELS as Record<string, PolicyLabels>)[lang] ?? POLICY_LABELS.en), ...opts.labels };\n const services = config.services ?? {};\n\n return {\n lang,\n revision: config.revision,\n date: isoDate(opts.date, config.revision),\n policyUrl: config.policyUrl ? l(config.policyUrl) : undefined,\n labels,\n categories: resolved.resolvedCategories.map((cat) => ({\n id: cat.id,\n title: l(cat.title),\n description: l(cat.description),\n readOnly: cat.readOnly,\n color: cat.color,\n services: cat.services.map((id) => {\n const s = services[id];\n return {\n id,\n label: s.label ? l(s.label) : id,\n provider: s.provider ?? '',\n privacyUrl: s.privacyUrl,\n purpose: l(s.purpose),\n cookies: (s.cookies ?? []).map((ck) => ({\n name: ck.name,\n displayName: ck.pattern ? prettyCookiePattern(ck.name) : ck.name,\n pattern: !!ck.pattern,\n duration: l(ck.duration),\n description: l(ck.description),\n domain: ck.domain,\n })),\n };\n }),\n })),\n };\n}\n\n/** Format the model date for display, e.g. \"13 novembre 2026\" / \"November 13, 2026\". */\nexport function formatPolicyDate(model: Pick<PolicyModel, 'date' | 'lang'>): string | undefined {\n if (!model.date) return undefined;\n const d = new Date(`${model.date}T00:00:00Z`);\n if (Number.isNaN(d.getTime())) return model.date;\n try {\n return new Intl.DateTimeFormat(model.lang, { dateStyle: 'long', timeZone: 'UTC' }).format(d);\n } catch {\n return model.date;\n }\n}\n\nexport interface AutoClearRule {\n /** Cookie name, or a regular expression source when `pattern` is true. */\n name: string;\n pattern: boolean;\n domain?: string;\n service: string;\n}\n\n/**\n * Cookies to delete when a category is withdrawn, per category id.\n * Read-only (necessary) categories are omitted: they cannot be withdrawn.\n */\nexport function autoClearRules(config: EasyCookieConfig): Record<string, AutoClearRule[]> {\n const out: Record<string, AutoClearRule[]> = {};\n for (const [id, cat] of Object.entries(config.categories)) if (!cat.readOnly) out[id] = [];\n for (const [service, s] of Object.entries(config.services ?? {})) {\n const list = out[s.category];\n if (!list) continue;\n for (const ck of s.cookies ?? [])\n list.push({ name: ck.name, pattern: !!ck.pattern, ...(ck.domain ? { domain: ck.domain } : {}), service });\n }\n return out;\n}\n\n/** True when `cookieName` matches the rule. */\nexport function matchesRule(rule: Pick<AutoClearRule, 'name' | 'pattern'>, cookieName: string): boolean {\n if (!rule.pattern) return rule.name === cookieName;\n try {\n return new RegExp(rule.name).test(cookieName);\n } catch {\n return false;\n }\n}\n","import { formatPolicyDate, type PolicyCategory, type PolicyModel, type PolicyService } from './model';\n\nexport interface RenderPolicyOptions {\n /** Heading level of category titles (2 → `<h2>`). Default 2. */\n headingLevel?: 2 | 3 | 4 | 5 | 6;\n /** Prefix for section ids (`#ec-policy-measurement`). Default `ec-policy-`. */\n idPrefix?: string;\n /** Show the \"last updated / revision\" line. Default true. */\n meta?: boolean;\n /** Omit categories without services. Default false. */\n hideEmpty?: boolean;\n}\n\nconst ESC: Record<string, string> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' };\nexport function escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (c) => ESC[c]);\n}\n\nfunction safeUrl(url: string): string | undefined {\n return /^(https?:|mailto:|\\/|#)/i.test(url.trim()) ? url : undefined;\n}\n\nexport interface PolicyRow {\n service: PolicyService;\n cookie?: PolicyService['cookies'][number];\n /** First row of this service (for grouping). */\n first: boolean;\n}\n\n/** Flatten a category to table rows: one per cookie, or one per service without cookies. */\nexport function policyRows(cat: PolicyCategory): PolicyRow[] {\n const rows: PolicyRow[] = [];\n for (const service of cat.services) {\n if (service.cookies.length === 0) rows.push({ service, first: true });\n else service.cookies.forEach((cookie, i) => rows.push({ service, cookie, first: i === 0 }));\n }\n return rows;\n}\n\nfunction visible(model: PolicyModel, opts: RenderPolicyOptions): PolicyCategory[] {\n return opts.hideEmpty ? model.categories.filter((c) => c.services.length > 0) : model.categories;\n}\n\n/**\n * Semantic HTML for the cookie section of a policy: one `<section>` and one\n * `<table>` per category. Class names only (`ec-policy*`), no inline styles.\n */\nexport function renderPolicyHtml(model: PolicyModel, opts: RenderPolicyOptions = {}): string {\n const h = `h${opts.headingLevel ?? 2}`;\n const prefix = opts.idPrefix ?? 'ec-policy-';\n const L = model.labels;\n const e = escapeHtml;\n const out: string[] = [`<div class=\"ec-policy\" lang=\"${e(model.lang)}\">`];\n\n if (opts.meta !== false) {\n const parts: string[] = [];\n const date = formatPolicyDate(model);\n if (date) parts.push(`${e(L.updated)}: <time datetime=\"${e(model.date!)}\">${e(date)}</time>`);\n parts.push(`${e(L.revision)}: <span class=\"ec-policy__revision\">${e(model.revision)}</span>`);\n out.push(`<p class=\"ec-policy__meta\">${parts.join(' · ')}</p>`);\n }\n\n for (const cat of visible(model, opts)) {\n const id = `${prefix}${cat.id}`;\n out.push(\n `<section class=\"ec-policy__category\" id=\"${e(id)}\" data-category=\"${e(cat.id)}\" aria-labelledby=\"${e(id)}-title\">`,\n );\n out.push(`<${h} class=\"ec-policy__title\" id=\"${e(id)}-title\">${e(cat.title)}</${h}>`);\n if (cat.readOnly) out.push(`<p class=\"ec-policy__badge\">${e(L.alwaysOn)}</p>`);\n if (cat.description) out.push(`<p class=\"ec-policy__description\">${e(cat.description)}</p>`);\n\n const rows = policyRows(cat);\n if (rows.length === 0) {\n out.push(`<p class=\"ec-policy__empty\">${e(L.noCookies)}</p>`);\n } else {\n out.push('<div class=\"ec-policy__table-wrap\">');\n out.push('<table class=\"ec-policy__table\">');\n out.push(`<caption class=\"ec-policy__caption\">${e(L.caption.replace('{category}', cat.title))}</caption>`);\n out.push(\n '<thead><tr>' +\n [L.cookie, L.service, L.provider, L.duration, L.purpose].map((t) => `<th scope=\"col\">${e(t)}</th>`).join('') +\n '</tr></thead>',\n );\n out.push('<tbody>');\n for (const { service, cookie } of rows) {\n const name = cookie\n ? `<code class=\"ec-policy__cookie\"${cookie.pattern ? ` title=\"${e(cookie.name)}\"` : ''}>${e(cookie.displayName)}</code>` +\n (cookie.domain ? ` <span class=\"ec-policy__domain\">${e(cookie.domain)}</span>` : '')\n : `<span class=\"ec-policy__none\">${e(L.noCookie)}</span>`;\n const url = service.privacyUrl ? safeUrl(service.privacyUrl) : undefined;\n const provider = url\n ? `<a class=\"ec-policy__link\" href=\"${e(url)}\" rel=\"noopener noreferrer\" target=\"_blank\">${e(service.provider || L.privacyPolicy)}</a>`\n : e(service.provider);\n const purpose = cookie?.description || service.purpose;\n out.push(\n `<tr data-service=\"${e(service.id)}\">` +\n `<th scope=\"row\">${name}</th>` +\n `<td class=\"ec-policy__service\">${e(service.label)}</td>` +\n `<td class=\"ec-policy__provider\">${provider}</td>` +\n `<td class=\"ec-policy__duration\">${e(cookie?.duration ?? '')}</td>` +\n `<td class=\"ec-policy__purpose\">${e(purpose)}</td>` +\n '</tr>',\n );\n }\n out.push('</tbody></table></div>');\n }\n out.push('</section>');\n }\n out.push('</div>');\n return out.join('\\n');\n}\n\nfunction mdCell(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/\\|/g, '\\\\|').replace(/\\r?\\n/g, ' ');\n}\n\n/** GitHub-flavoured Markdown version of the policy (one table per category). */\nexport function renderPolicyMarkdown(model: PolicyModel, opts: Pick<RenderPolicyOptions, 'headingLevel' | 'meta' | 'hideEmpty'> = {}): string {\n const hashes = '#'.repeat(opts.headingLevel ?? 2);\n const L = model.labels;\n const out: string[] = [];\n if (opts.meta !== false) {\n const date = formatPolicyDate(model);\n out.push([date ? `${L.updated}: ${date}` : '', `${L.revision}: ${model.revision}`].filter(Boolean).join(' · '), '');\n }\n for (const cat of visible(model, opts)) {\n out.push(`${hashes} ${cat.title}`, '');\n if (cat.readOnly) out.push(`*${L.alwaysOn}*`, '');\n if (cat.description) out.push(cat.description, '');\n const rows = policyRows(cat);\n if (rows.length === 0) {\n out.push(L.noCookies, '');\n continue;\n }\n out.push(`| ${[L.cookie, L.service, L.provider, L.duration, L.purpose].map(mdCell).join(' | ')} |`);\n out.push('| --- | --- | --- | --- | --- |');\n for (const { service, cookie } of rows) {\n const name = cookie ? `\\`${cookie.displayName.replace(/`/g, '')}\\`` : L.noCookie;\n const provider = service.privacyUrl\n ? `[${mdCell(service.provider || L.privacyPolicy).replace(/[[\\]]/g, '\\\\$&')}](${service.privacyUrl.replace(/[()\\s]/g, encodeURIComponent)})`\n : mdCell(service.provider);\n out.push(\n `| ${name} | ${mdCell(service.label)} | ${provider} | ${mdCell(cookie?.duration ?? '')} | ${mdCell(cookie?.description || service.purpose)} |`,\n );\n }\n out.push('');\n }\n return out.join('\\n').trimEnd() + '\\n';\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/legal-client.ts
21
+ var legal_client_exports = {};
22
+ __export(legal_client_exports, {
23
+ LEGAL_KINDS: () => LEGAL_KINDS,
24
+ fetchLegalDocument: () => fetchLegalDocument,
25
+ isLegalKind: () => isLegalKind,
26
+ legalDocumentUrl: () => legalDocumentUrl
27
+ });
28
+ module.exports = __toCommonJS(legal_client_exports);
29
+ var LEGAL_KINDS = ["cookie", "privacy", "terms"];
30
+ var isLegalKind = (v) => typeof v === "string" && LEGAL_KINDS.includes(v);
31
+ function legalDocumentUrl(opts) {
32
+ const base = opts.endpoint.replace(/\/+$/, "");
33
+ const q = opts.lang ? `?lang=${encodeURIComponent(opts.lang)}` : "";
34
+ return `${base}/v1/sites/${encodeURIComponent(opts.siteKey)}/legal/${encodeURIComponent(opts.kind)}.json${q}`;
35
+ }
36
+ async function fetchLegalDocument(opts) {
37
+ const f = opts.fetch ?? fetch;
38
+ const res = await f(legalDocumentUrl(opts), { credentials: "omit", ...opts.init });
39
+ if (res.status === 404) return null;
40
+ if (!res.ok) throw new Error(`[easy-cookie] could not load the ${opts.kind} document (${res.status})`);
41
+ return await res.json();
42
+ }
43
+ // Annotate the CommonJS export names for ESM import in node:
44
+ 0 && (module.exports = {
45
+ LEGAL_KINDS,
46
+ fetchLegalDocument,
47
+ isLegalKind,
48
+ legalDocumentUrl
49
+ });
50
+ //# sourceMappingURL=client.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/legal-client.ts"],"sourcesContent":["/**\n * Legal document types and the fetch client. Kept free of the Markdown\n * renderer so browser bundles (React component, Next.js) stay small.\n */\n\n/** The three legal documents every site has. */\nexport type LegalKind = 'cookie' | 'privacy' | 'terms';\nexport const LEGAL_KINDS: readonly LegalKind[] = ['cookie', 'privacy', 'terms'];\nexport const isLegalKind = (v: unknown): v is LegalKind => typeof v === 'string' && (LEGAL_KINDS as readonly string[]).includes(v);\n\nexport interface LegalTranslation {\n title: string;\n /** Markdown, rendered with raw HTML disabled. */\n body: string;\n}\n\n/** Content of a legal document: one entry per language it is written in. */\nexport interface LegalDocContent {\n translations: Record<string, LegalTranslation>;\n}\n\n/** A published document as served by `GET /v1/sites/:publicKey/legal/:kind.json`. */\nexport interface PublishedLegalDocument {\n kind: LegalKind;\n /** Language actually shown (after fallback). */\n lang: string;\n /** Every language the document is written in. */\n languages: string[];\n title: string;\n /** Rendered, safe HTML wrapped in `<div class=\"ec-legal\" lang=\"…\">`. */\n html: string;\n version: string;\n publishedAt: string;\n}\n\nexport interface FetchLegalDocumentOptions {\n /** Base URL of the consent server. */\n endpoint: string;\n /** Public site key (`pk_…`). */\n siteKey: string;\n kind: LegalKind;\n lang?: string;\n /** Extra fetch options (e.g. Next's `next: { revalidate }`). */\n init?: RequestInit & Record<string, unknown>;\n /** Custom fetch implementation. */\n fetch?: typeof fetch;\n}\n\n/** URL of the published document JSON on the consent server. */\nexport function legalDocumentUrl(opts: Pick<FetchLegalDocumentOptions, 'endpoint' | 'siteKey' | 'kind' | 'lang'>): string {\n const base = opts.endpoint.replace(/\\/+$/, '');\n const q = opts.lang ? `?lang=${encodeURIComponent(opts.lang)}` : '';\n return `${base}/v1/sites/${encodeURIComponent(opts.siteKey)}/legal/${encodeURIComponent(opts.kind)}.json${q}`;\n}\n\n/**\n * Fetch a published legal document from the consent server.\n * Resolves to null when nothing is published yet (404); throws on other errors.\n */\nexport async function fetchLegalDocument(opts: FetchLegalDocumentOptions): Promise<PublishedLegalDocument | null> {\n const f = opts.fetch ?? fetch;\n const res = await f(legalDocumentUrl(opts), { credentials: 'omit', ...opts.init });\n if (res.status === 404) return null;\n if (!res.ok) throw new Error(`[easy-cookie] could not load the ${opts.kind} document (${res.status})`);\n return (await res.json()) as PublishedLegalDocument;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOO,IAAM,cAAoC,CAAC,UAAU,WAAW,OAAO;AACvE,IAAM,cAAc,CAAC,MAA+B,OAAO,MAAM,YAAa,YAAkC,SAAS,CAAC;AAyC1H,SAAS,iBAAiB,MAAyF;AACxH,QAAM,OAAO,KAAK,SAAS,QAAQ,QAAQ,EAAE;AAC7C,QAAM,IAAI,KAAK,OAAO,SAAS,mBAAmB,KAAK,IAAI,CAAC,KAAK;AACjE,SAAO,GAAG,IAAI,aAAa,mBAAmB,KAAK,OAAO,CAAC,UAAU,mBAAmB,KAAK,IAAI,CAAC,QAAQ,CAAC;AAC7G;AAMA,eAAsB,mBAAmB,MAAyE;AAChH,QAAM,IAAI,KAAK,SAAS;AACxB,QAAM,MAAM,MAAM,EAAE,iBAAiB,IAAI,GAAG,EAAE,aAAa,QAAQ,GAAG,KAAK,KAAK,CAAC;AACjF,MAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,oCAAoC,KAAK,IAAI,cAAc,IAAI,MAAM,GAAG;AACrG,SAAQ,MAAM,IAAI,KAAK;AACzB;","names":[]}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Legal document types and the fetch client. Kept free of the Markdown
3
+ * renderer so browser bundles (React component, Next.js) stay small.
4
+ */
5
+ /** The three legal documents every site has. */
6
+ type LegalKind = 'cookie' | 'privacy' | 'terms';
7
+ declare const LEGAL_KINDS: readonly LegalKind[];
8
+ declare const isLegalKind: (v: unknown) => v is LegalKind;
9
+ interface LegalTranslation {
10
+ title: string;
11
+ /** Markdown, rendered with raw HTML disabled. */
12
+ body: string;
13
+ }
14
+ /** Content of a legal document: one entry per language it is written in. */
15
+ interface LegalDocContent {
16
+ translations: Record<string, LegalTranslation>;
17
+ }
18
+ /** A published document as served by `GET /v1/sites/:publicKey/legal/:kind.json`. */
19
+ interface PublishedLegalDocument {
20
+ kind: LegalKind;
21
+ /** Language actually shown (after fallback). */
22
+ lang: string;
23
+ /** Every language the document is written in. */
24
+ languages: string[];
25
+ title: string;
26
+ /** Rendered, safe HTML wrapped in `<div class="ec-legal" lang="…">`. */
27
+ html: string;
28
+ version: string;
29
+ publishedAt: string;
30
+ }
31
+ interface FetchLegalDocumentOptions {
32
+ /** Base URL of the consent server. */
33
+ endpoint: string;
34
+ /** Public site key (`pk_…`). */
35
+ siteKey: string;
36
+ kind: LegalKind;
37
+ lang?: string;
38
+ /** Extra fetch options (e.g. Next's `next: { revalidate }`). */
39
+ init?: RequestInit & Record<string, unknown>;
40
+ /** Custom fetch implementation. */
41
+ fetch?: typeof fetch;
42
+ }
43
+ /** URL of the published document JSON on the consent server. */
44
+ declare function legalDocumentUrl(opts: Pick<FetchLegalDocumentOptions, 'endpoint' | 'siteKey' | 'kind' | 'lang'>): string;
45
+ /**
46
+ * Fetch a published legal document from the consent server.
47
+ * Resolves to null when nothing is published yet (404); throws on other errors.
48
+ */
49
+ declare function fetchLegalDocument(opts: FetchLegalDocumentOptions): Promise<PublishedLegalDocument | null>;
50
+
51
+ export { type FetchLegalDocumentOptions, LEGAL_KINDS, type LegalDocContent, type LegalKind, type LegalTranslation, type PublishedLegalDocument, fetchLegalDocument, isLegalKind, legalDocumentUrl };