@getquick/content 0.4.1 → 0.5.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 +39 -3
- package/dist/client-ip-cnqPLdDa.js +203 -0
- package/dist/consent-astro.d.ts +2 -36
- package/dist/consent-astro.js +1 -93
- package/dist/consent-html.d.ts +36 -0
- package/dist/consent-html.js +94 -0
- package/dist/consent-server.d.ts +7 -0
- package/dist/consent-server.js +3 -2
- package/dist/consent.js +1 -1
- package/dist/{site-language-BCGjkpWY.js → design-fonts-B0ts54vY.js} +3764 -1
- package/dist/forms-server.d.ts +7 -0
- package/dist/forms-server.js +4 -3
- package/dist/index.d.ts +2 -17
- package/dist/index.js +2 -2
- package/dist/server.d.ts +1 -1
- package/dist/server.js +88 -3556
- package/dist/{version-Co2JJcK_.js → version-CmdZGJvZ.js} +1 -1
- package/dist/{wordpress-BvkTDMA0.d.ts → wordpress-OWJxnbAF.d.ts} +113 -2
- package/package.json +5 -1
- package/dist/request-guards-BUPr_vA4.js +0 -46
package/README.md
CHANGED
|
@@ -288,14 +288,16 @@ updates. Full P6 release evidence remains issue #11.
|
|
|
288
288
|
CONSENT-1/LEGAL-1 ship as isolated entries: `@getquick/content/consent`
|
|
289
289
|
(neutral core), `/consent/client` (browser runtime), `/consent/server` (the
|
|
290
290
|
`/gq/consent` record and read endpoints and purge), `/consent/migrations`
|
|
291
|
-
(the separate consent database), `/consent/
|
|
292
|
-
footer as HTML) and `/consent/testing` (acceptance suite). They import no
|
|
291
|
+
(the separate consent database), `/consent/html` (default UI and legal
|
|
292
|
+
footer as HTML), `/consent/astro` (compatible re-export) and `/consent/testing` (acceptance suite). They import no
|
|
293
293
|
content runtime, framework or WordPress client. Passing
|
|
294
294
|
`consent: { services: ["youtube"], cmsSettings: true }` to
|
|
295
295
|
`createContentRuntime` rewrites third-party embeds into consent placeholders
|
|
296
296
|
at delivery and stores gq-config's Legal Contract and GA4 settings in each
|
|
297
297
|
language's chrome; `runtime.consentConfiguration(chrome)` returns the page's
|
|
298
|
-
consent configuration. See the repository's `docs/consent.md
|
|
298
|
+
consent configuration. See the repository's `docs/consent.md` export index and the
|
|
299
|
+
platform-owned [consent HTML reference](https://github.com/Quick-Release/gq-platform/blob/main/docs/runtime/gq-content/consent-html.md)
|
|
300
|
+
([platform PR #273](https://github.com/Quick-Release/gq-platform/pull/273)).
|
|
299
301
|
|
|
300
302
|
## Design presentation and composition
|
|
301
303
|
|
|
@@ -307,6 +309,40 @@ per response, using the delivered `theme` from entry/home/chrome. Resolve defaul
|
|
|
307
309
|
with `resolveDesignTheme` for older stored publications. A changed theme requires
|
|
308
310
|
regenerating both stylesheets, including its media thresholds.
|
|
309
311
|
|
|
312
|
+
### Fonts and text styles
|
|
313
|
+
|
|
314
|
+
GQ Design 0.10.1 (contract 2.1.0) delivers the Site's self-hosted font families
|
|
315
|
+
(faces on the Site's own or media host, each with a metric-matched local
|
|
316
|
+
fallback), the `sans`/`serif`/`mono` slot assignments and the Heading and
|
|
317
|
+
Paragraph text styles. The design presets carry them as optional `fonts` and
|
|
318
|
+
`textStyles`: read with the other presets, stored with them, refreshed by the
|
|
319
|
+
`design` settings event (a global styles save, a font slot assignment, or a
|
|
320
|
+
Font Library family or face saved or deleted) and compared by reconciliation.
|
|
321
|
+
A CMS whose GQ Design predates them rejects the fields; the presets are read
|
|
322
|
+
again without them, with a warning, and pages keep the theme's system stacks.
|
|
323
|
+
|
|
324
|
+
Emit them once per response, in the head, after `designThemeStyleSheet(theme)`:
|
|
325
|
+
|
|
326
|
+
```astro
|
|
327
|
+
<style is:inline set:html={designThemeStyleSheet(theme)} />
|
|
328
|
+
<Fragment set:html={designFontsHead(await runtime.designPresets())} />
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
`designFontsHead(presets, { root? })` returns, in order: at most two
|
|
332
|
+
`<link rel="preload" as="font" type="font/woff2" href="…" crossorigin>` for the
|
|
333
|
+
faces GQ Design marks (the text styles' primary Latin files); a
|
|
334
|
+
`<style data-gqd-fonts>` with the `@font-face` rules (`font-display` as
|
|
335
|
+
delivered, `swap` from GQ Design), the fallback faces and the assigned slots'
|
|
336
|
+
`--gqd-font-*` stacks; and a `<style data-gqd-text-styles>` with the text
|
|
337
|
+
styles under `root` (default `.gqd-content`, the theme's root). The slot stacks
|
|
338
|
+
are set on `:root` and again on the root, because the theme stylesheet declares
|
|
339
|
+
the system stacks there. It is empty without fonts or text styles, or with no
|
|
340
|
+
families. Font URLs only come from the presets: faces with a relative URL or a
|
|
341
|
+
file on a Google host are dropped, so a visitor's browser never contacts
|
|
342
|
+
Google. Media hosts need a public CORS rule for cross-origin font files.
|
|
343
|
+
`designFontStyleSheet`, `designFontPreloads` (link descriptors for another
|
|
344
|
+
serializer) and `designTextStyleSheet` return the parts.
|
|
345
|
+
|
|
310
346
|
`presentationHtml(name, shell, presentation, { attributes, model? })` decorates an
|
|
311
347
|
existing root shell. Pass the complete block attributes for native Group layout
|
|
312
348
|
context. `presentationVariables` takes the same attributes context. Both delegate
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
//#region frontend/src/request-guards.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether a browser posted from the Site itself. A cross-site post carries
|
|
4
|
+
* its Origin (and Sec-Fetch-Site); `origins` are the Site's public origins
|
|
5
|
+
* when the Worker sees another (a proxy). The request's own origin is always
|
|
6
|
+
* accepted.
|
|
7
|
+
*/
|
|
8
|
+
function isSameOrigin(request, origins) {
|
|
9
|
+
const origin = request.headers.get("Origin");
|
|
10
|
+
const site = request.headers.get("Sec-Fetch-Site");
|
|
11
|
+
if (site && site !== "same-origin") return false;
|
|
12
|
+
if (!origin) return false;
|
|
13
|
+
if (origin === new URL(request.url).origin || origins.has(origin)) return true;
|
|
14
|
+
try {
|
|
15
|
+
return new URL(origin).host === request.headers.get("Host");
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** The body as text, or null once it exceeds `limit` bytes (declared or read). */
|
|
21
|
+
async function readBoundedBody(request, limit) {
|
|
22
|
+
if (Number(request.headers.get("Content-Length") ?? "0") > limit) return null;
|
|
23
|
+
if (!request.body) return "";
|
|
24
|
+
const reader = request.body.getReader();
|
|
25
|
+
const chunks = [];
|
|
26
|
+
let size = 0;
|
|
27
|
+
for (;;) {
|
|
28
|
+
const { done, value } = await reader.read();
|
|
29
|
+
if (done) break;
|
|
30
|
+
size += value.byteLength;
|
|
31
|
+
if (size > limit) {
|
|
32
|
+
await reader.cancel();
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
chunks.push(value);
|
|
36
|
+
}
|
|
37
|
+
const bytes = new Uint8Array(size);
|
|
38
|
+
let offset = 0;
|
|
39
|
+
for (const chunk of chunks) {
|
|
40
|
+
bytes.set(chunk, offset);
|
|
41
|
+
offset += chunk.byteLength;
|
|
42
|
+
}
|
|
43
|
+
return new TextDecoder().decode(bytes);
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region frontend/src/client-ip.ts
|
|
47
|
+
function parseIpv4(text) {
|
|
48
|
+
const parts = text.split(".");
|
|
49
|
+
if (parts.length !== 4) return null;
|
|
50
|
+
const bytes = /* @__PURE__ */ new Uint8Array(4);
|
|
51
|
+
for (const [index, part] of parts.entries()) {
|
|
52
|
+
if (!/^(0|[1-9]\d{0,2})$/.test(part) || Number(part) > 255) return null;
|
|
53
|
+
bytes[index] = Number(part);
|
|
54
|
+
}
|
|
55
|
+
return bytes;
|
|
56
|
+
}
|
|
57
|
+
function parseIpv6(text) {
|
|
58
|
+
let head = text;
|
|
59
|
+
let tail = [];
|
|
60
|
+
const lastColon = text.lastIndexOf(":");
|
|
61
|
+
if (text.includes(".", lastColon)) {
|
|
62
|
+
const v4 = parseIpv4(text.slice(lastColon + 1));
|
|
63
|
+
if (!v4) return null;
|
|
64
|
+
head = text.slice(0, lastColon + 1);
|
|
65
|
+
if (head.endsWith(":") && !head.endsWith("::")) head = head.slice(0, -1);
|
|
66
|
+
tail = [v4[0] << 8 | v4[1], v4[2] << 8 | v4[3]];
|
|
67
|
+
}
|
|
68
|
+
const halves = head.split("::");
|
|
69
|
+
if (halves.length > 2) return null;
|
|
70
|
+
const groups = (part) => part === "" ? [] : part.split(":");
|
|
71
|
+
const left = groups(halves[0]);
|
|
72
|
+
const right = halves.length === 2 ? groups(halves[1]) : [];
|
|
73
|
+
const words = [];
|
|
74
|
+
for (const group of [...left, ...right]) {
|
|
75
|
+
if (!/^[0-9a-f]{1,4}$/i.test(group)) return null;
|
|
76
|
+
words.push(parseInt(group, 16));
|
|
77
|
+
}
|
|
78
|
+
const given = words.length + tail.length;
|
|
79
|
+
if (halves.length === 2 ? given > 7 : given !== 8) return null;
|
|
80
|
+
const all = [
|
|
81
|
+
...words.slice(0, left.length),
|
|
82
|
+
...Array(8 - given).fill(0),
|
|
83
|
+
...words.slice(left.length),
|
|
84
|
+
...tail
|
|
85
|
+
];
|
|
86
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
87
|
+
all.forEach((word, index) => {
|
|
88
|
+
bytes[index * 2] = word >> 8;
|
|
89
|
+
bytes[index * 2 + 1] = word & 255;
|
|
90
|
+
});
|
|
91
|
+
return bytes;
|
|
92
|
+
}
|
|
93
|
+
/** An IPv4 or IPv6 address; an IPv4-mapped IPv6 address reads as its IPv4. */
|
|
94
|
+
function parseAddress(value) {
|
|
95
|
+
const text = value.trim();
|
|
96
|
+
if (!text || text.length > 64) return null;
|
|
97
|
+
if (!text.includes(":")) {
|
|
98
|
+
const bytes = parseIpv4(text);
|
|
99
|
+
return bytes ? {
|
|
100
|
+
version: 4,
|
|
101
|
+
bytes
|
|
102
|
+
} : null;
|
|
103
|
+
}
|
|
104
|
+
const bytes = parseIpv6(text.replace(/%[0-9a-z._~-]+$/i, ""));
|
|
105
|
+
if (!bytes) return null;
|
|
106
|
+
return bytes.slice(0, 10).every((byte) => byte === 0) && bytes[10] === 255 && bytes[11] === 255 ? {
|
|
107
|
+
version: 4,
|
|
108
|
+
bytes: bytes.slice(12)
|
|
109
|
+
} : {
|
|
110
|
+
version: 6,
|
|
111
|
+
bytes
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** An X-Forwarded-For entry: an address, optionally `[v6]:port` or `v4:port`. */
|
|
115
|
+
function parseForwarded(value) {
|
|
116
|
+
const text = value.trim();
|
|
117
|
+
const bracketed = /^\[([^\]]+)\](?::\d{1,5})?$/.exec(text);
|
|
118
|
+
if (bracketed) return parseAddress(bracketed[1]);
|
|
119
|
+
const withPort = /^(\d{1,3}(?:\.\d{1,3}){3}):\d{1,5}$/.exec(text);
|
|
120
|
+
return parseAddress(withPort ? withPort[1] : text);
|
|
121
|
+
}
|
|
122
|
+
function format(address) {
|
|
123
|
+
if (address.version === 4) return address.bytes.join(".");
|
|
124
|
+
const words = Array.from({ length: 8 }, (_, index) => (address.bytes[index * 2] << 8 | address.bytes[index * 2 + 1]).toString(16));
|
|
125
|
+
let start = -1;
|
|
126
|
+
let length = 0;
|
|
127
|
+
for (let index = 0; index < 8;) {
|
|
128
|
+
if (words[index] !== "0") {
|
|
129
|
+
index += 1;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
let end = index;
|
|
133
|
+
while (end < 8 && words[end] === "0") end += 1;
|
|
134
|
+
if (end - index > length && end - index > 1) [start, length] = [index, end - index];
|
|
135
|
+
index = end;
|
|
136
|
+
}
|
|
137
|
+
if (start < 0) return words.join(":");
|
|
138
|
+
return `${words.slice(0, start).join(":")}::${words.slice(start + length).join(":")}`;
|
|
139
|
+
}
|
|
140
|
+
function parseRange(value) {
|
|
141
|
+
const [text, prefixText, extra] = value.trim().split("/");
|
|
142
|
+
const address = extra === void 0 ? parseAddress(text ?? "") : null;
|
|
143
|
+
if (!address) throw new Error(`trustedProxies: "${value}" isn't an IP address or CIDR`);
|
|
144
|
+
const bits = address.bytes.length * 8;
|
|
145
|
+
const prefix = prefixText === void 0 ? bits : Number(prefixText);
|
|
146
|
+
if (prefixText !== void 0 && !/^\d{1,3}$/.test(prefixText) || prefix > bits) throw new Error(`trustedProxies: "${value}" has an invalid prefix length`);
|
|
147
|
+
if (prefix === 0) throw new Error(`trustedProxies: "${value}" would trust every address`);
|
|
148
|
+
return {
|
|
149
|
+
...address,
|
|
150
|
+
prefix
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function contains(range, address) {
|
|
154
|
+
if (range.version !== address.version) return false;
|
|
155
|
+
const whole = range.prefix >> 3;
|
|
156
|
+
for (let index = 0; index < whole; index += 1) if (range.bytes[index] !== address.bytes[index]) return false;
|
|
157
|
+
const rest = range.prefix & 7;
|
|
158
|
+
if (!rest) return true;
|
|
159
|
+
const mask = 255 << 8 - rest & 255;
|
|
160
|
+
return (range.bytes[whole] & mask) === (address.bytes[whole] & mask);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The client address of a request, from `CF-Connecting-IP` and, only when
|
|
164
|
+
* that is one of `trustedProxies` (addresses or CIDRs, IPv4 or IPv6), the
|
|
165
|
+
* proxy's forwarding headers: the right-most `X-Forwarded-For` entry that
|
|
166
|
+
* isn't itself a trusted proxy, else `X-Real-IP`. Null when there is no
|
|
167
|
+
* usable `CF-Connecting-IP`. Throws on an invalid `trustedProxies` entry.
|
|
168
|
+
*/
|
|
169
|
+
function clientAddressResolver(trustedProxies = []) {
|
|
170
|
+
const ranges = trustedProxies.map(parseRange);
|
|
171
|
+
const trusted = (address) => ranges.some((range) => contains(range, address));
|
|
172
|
+
return (request) => {
|
|
173
|
+
const peer = parseAddress(request.headers.get("CF-Connecting-IP") ?? "");
|
|
174
|
+
if (!peer) return null;
|
|
175
|
+
if (!trusted(peer)) return format(peer);
|
|
176
|
+
const forwarded = (request.headers.get("X-Forwarded-For") ?? "").split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
177
|
+
for (let index = forwarded.length - 1; index >= 0; index -= 1) {
|
|
178
|
+
const address = parseForwarded(forwarded[index]);
|
|
179
|
+
if (!address) return format(peer);
|
|
180
|
+
if (!trusted(address)) return format(address);
|
|
181
|
+
}
|
|
182
|
+
const real = parseAddress(request.headers.get("X-Real-IP") ?? "");
|
|
183
|
+
return real && !trusted(real) ? format(real) : format(peer);
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* A rate-limit key for a client address: an IPv4 address as itself, an IPv6
|
|
188
|
+
* address as its /64 (`2001:db8:1:2::/64`), since one subscriber holds a
|
|
189
|
+
* whole /64. `unknown` without an address.
|
|
190
|
+
*/
|
|
191
|
+
function rateLimitKey(address) {
|
|
192
|
+
const parsed = address === null ? null : parseAddress(address);
|
|
193
|
+
if (!parsed) return "unknown";
|
|
194
|
+
if (parsed.version === 4) return format(parsed);
|
|
195
|
+
const network = /* @__PURE__ */ new Uint8Array(16);
|
|
196
|
+
network.set(parsed.bytes.slice(0, 8));
|
|
197
|
+
return `${format({
|
|
198
|
+
version: 6,
|
|
199
|
+
bytes: network
|
|
200
|
+
})}/64`;
|
|
201
|
+
}
|
|
202
|
+
//#endregion
|
|
203
|
+
export { readBoundedBody as i, rateLimitKey as n, isSameOrigin as r, clientAddressResolver as t };
|
package/dist/consent-astro.d.ts
CHANGED
|
@@ -1,36 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
//#region frontend/src/consent/astro.d.ts
|
|
4
|
-
export interface ConsentPageInput {
|
|
5
|
-
configuration: ConsentConfiguration;
|
|
6
|
-
/** The page's `lang` (BCP 47): the UI follows it (CN-C3). */
|
|
7
|
-
language: string;
|
|
8
|
-
/** The page language's Legal Contract, from the chrome (LEGAL-1); null when not delivered yet. */
|
|
9
|
-
legal?: LegalLanguage | null;
|
|
10
|
-
/** The request's Cookie header: the banner is rendered open only without a decision in force. */
|
|
11
|
-
cookie?: string | null;
|
|
12
|
-
now?: number;
|
|
13
|
-
/** Include the default stylesheet (Sites restyle through its custom properties). */
|
|
14
|
-
styles?: boolean;
|
|
15
|
-
}
|
|
16
|
-
export interface ConsentPage {
|
|
17
|
-
/** For `<head>`: the configuration the client reads, and the default styles. */
|
|
18
|
-
head: string;
|
|
19
|
-
/** For the start of `<body>`: the banner (first in focus order) and the preferences dialog. */
|
|
20
|
-
body: string;
|
|
21
|
-
/** For the footer: the legal links and the persistent "Cookie settings" control. */
|
|
22
|
-
footer: string;
|
|
23
|
-
/** Whether the banner is rendered open. */
|
|
24
|
-
bannerOpen: boolean;
|
|
25
|
-
}
|
|
26
|
-
/** The legal links and "Cookie settings" control for a footer (LG-R5, CN-R6). */
|
|
27
|
-
export declare function legalFooter(legal: LegalLanguage | null | undefined, language: string): string;
|
|
28
|
-
/** Everything the default UI renders on one page. */
|
|
29
|
-
export declare function consentPage(input: ConsentPageInput): ConsentPage;
|
|
30
|
-
/**
|
|
31
|
-
* The default stylesheet. Sites restyle it through these custom properties or
|
|
32
|
-
* their own CSS (CN-C4); the two first-layer choices share one class so they
|
|
33
|
-
* keep equal prominence.
|
|
34
|
-
*/
|
|
35
|
-
export declare const CONSENT_STYLES: string;
|
|
36
|
-
//#endregion
|
|
1
|
+
import { CONSENT_STYLES, ConsentPage, ConsentPageInput, consentPage, legalFooter } from "./consent-html.js";
|
|
2
|
+
export { CONSENT_STYLES, type ConsentPage, type ConsentPageInput, consentPage, legalFooter };
|
package/dist/consent-astro.js
CHANGED
|
@@ -1,94 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as CONSENT_COPY, c as legalRevisions, r as escapeHtml } from "./embeds-CJJemAmE.js";
|
|
3
|
-
import { t as CONSENT_UI_VERSION } from "./version-Co2JJcK_.js";
|
|
4
|
-
import { t as CONSENT_CONFIG_ID } from "./client-config-B3LNSOxl.js";
|
|
5
|
-
import { t as CONSENT_PATH } from "./paths-Cmzzof3v.js";
|
|
6
|
-
//#region frontend/src/consent/astro.ts
|
|
7
|
-
function storageText(service, language) {
|
|
8
|
-
const copy = CONSENT_COPY[language];
|
|
9
|
-
return service.storage.map((item) => `${item.name}${item.match === "prefix" ? "*" : ""} (${item.kind}, ${item.duration[language]}${item.party === "third" ? `; ${copy.thirdPartyStorage}` : ""})`).join("; ");
|
|
10
|
-
}
|
|
11
|
-
function serviceList(services, language) {
|
|
12
|
-
if (!services.length) return "";
|
|
13
|
-
const copy = CONSENT_COPY[language];
|
|
14
|
-
return `<ul class="gq-consent-services">${services.map((service) => {
|
|
15
|
-
const storage = storageText(service, language);
|
|
16
|
-
return `<li class="gq-consent-service"><strong>${escapeHtml(service.name)}</strong><dl><dt>${escapeHtml(copy.provider)}</dt><dd>${escapeHtml(service.provider)}</dd><dt>${escapeHtml(copy.purpose)}</dt><dd>${escapeHtml(service.purpose[language])}</dd><dt>${escapeHtml(copy.storage)}</dt><dd>${escapeHtml(storage || "—")}</dd><dt>${escapeHtml(copy.retention)}</dt><dd>${escapeHtml(service.retention[language])}</dd></dl></li>`;
|
|
17
|
-
}).join("")}</ul>`;
|
|
18
|
-
}
|
|
19
|
-
function link(href, text, attributes = "") {
|
|
20
|
-
return `<a href="${escapeHtml(href)}"${attributes}>${escapeHtml(text)}</a>`;
|
|
21
|
-
}
|
|
22
|
-
/** The legal links and "Cookie settings" control for a footer (LG-R5, CN-R6). */
|
|
23
|
-
function legalFooter(legal, language) {
|
|
24
|
-
const lang = copyLanguage(language);
|
|
25
|
-
const copy = CONSENT_COPY[lang];
|
|
26
|
-
const items = (legal?.roles ?? []).map((role) => `<li>${link(role.url, role.title ?? copy.legal[role.role] ?? role.role, ` data-gq-legal-role="${escapeHtml(role.role)}"`)}</li>`);
|
|
27
|
-
items.push(`<li><button type="button" class="gq-consent-settings" data-gq-consent-open aria-haspopup="dialog" aria-controls="gq-consent-preferences">${escapeHtml(copy.cookieSettings)}</button></li>`);
|
|
28
|
-
return `<nav class="gq-legal" aria-label="${escapeHtml(copy.legalNavigation)}"><ul>${items.join("")}</ul></nav>`;
|
|
29
|
-
}
|
|
30
|
-
/** Everything the default UI renders on one page. */
|
|
31
|
-
function consentPage(input) {
|
|
32
|
-
const { configuration } = input;
|
|
33
|
-
const lang = copyLanguage(input.language);
|
|
34
|
-
const copy = CONSENT_COPY[lang];
|
|
35
|
-
const state = readConsentCookie(input.cookie);
|
|
36
|
-
const now = input.now ?? Date.now();
|
|
37
|
-
const bannerOpen = promptReason(configuration, state, now) !== null;
|
|
38
|
-
const inForce = grantedCategories(configuration, state, now);
|
|
39
|
-
const cookiePolicy = input.legal?.roles.find((role) => role.role === "cookie_policy");
|
|
40
|
-
const config = {
|
|
41
|
-
configuration,
|
|
42
|
-
language: input.language,
|
|
43
|
-
uiVersion: CONSENT_UI_VERSION,
|
|
44
|
-
legalRevisions: legalRevisions(input.legal),
|
|
45
|
-
endpoint: CONSENT_PATH
|
|
46
|
-
};
|
|
47
|
-
const head = `<script type="application/json" id="${CONSENT_CONFIG_ID}">${JSON.stringify(config).replaceAll("<", "\\u003c")}<\/script>` + (input.styles === false ? "" : `<style data-gq-consent-styles>${CONSENT_STYLES}</style>`);
|
|
48
|
-
const services = configuration.services.map((id) => SERVICE_CATALOG[id]);
|
|
49
|
-
const inCategory = (category) => services.filter((service) => service.category === category);
|
|
50
|
-
const optional = configuration.categories.length > 0;
|
|
51
|
-
const banner = !optional ? "" : `<section id="gq-consent-banner" class="gq-consent-banner" role="dialog" aria-modal="false" aria-labelledby="gq-consent-banner-title" aria-describedby="gq-consent-banner-text" lang="${escapeHtml(input.language)}"${bannerOpen ? "" : " hidden"}><h2 id="gq-consent-banner-title" class="gq-consent-banner__title">${escapeHtml(copy.bannerTitle)}</h2><p id="gq-consent-banner-text" class="gq-consent-banner__text">${escapeHtml(copy.bannerText)}` + (cookiePolicy ? ` ${link(cookiePolicy.url, copy.cookiePolicy, " class=\"gq-consent-banner__policy\"")}` : "") + `</p><div class="gq-consent-banner__actions"><button type="button" class="gq-consent-button gq-consent-button--choice" data-gq-consent-reject-all>${escapeHtml(copy.rejectAll)}</button><button type="button" class="gq-consent-button gq-consent-button--choice" data-gq-consent-accept-all>${escapeHtml(copy.acceptAll)}</button><button type="button" class="gq-consent-button gq-consent-button--secondary" data-gq-consent-customise aria-haspopup="dialog" aria-controls="gq-consent-preferences">${escapeHtml(copy.customise)}</button></div></section>`;
|
|
52
|
-
const necessary = copy.categories.necessary;
|
|
53
|
-
const categories = OPTIONAL_CATEGORIES.filter((category) => configuration.categories.includes(category)).map((category) => {
|
|
54
|
-
const text = copy.categories[category];
|
|
55
|
-
const id = `gq-consent-category-${category}`;
|
|
56
|
-
return `<fieldset class="gq-consent-category"><legend>${escapeHtml(text.name)}</legend><label for="${id}"><input type="checkbox" id="${id}" name="${category}" value="${category}" data-gq-consent-category${inForce.includes(category) ? " checked" : ""}> ${escapeHtml(text.name)}</label><p>${escapeHtml(text.description)}</p>${serviceList(inCategory(category), lang)}</fieldset>`;
|
|
57
|
-
});
|
|
58
|
-
return {
|
|
59
|
-
head,
|
|
60
|
-
body: banner + (`<dialog id="gq-consent-preferences" class="gq-consent-preferences" aria-labelledby="gq-consent-preferences-title" lang="${escapeHtml(input.language)}"><form method="dialog"><h2 id="gq-consent-preferences-title">${escapeHtml(copy.preferencesTitle)}</h2><p>${escapeHtml(copy.preferencesText)}</p>` + (cookiePolicy ? `<p>${link(cookiePolicy.url, copy.cookiePolicy)}</p>` : "") + `<fieldset class="gq-consent-category"><legend>${escapeHtml(necessary.name)}</legend><p>${escapeHtml(copy.alwaysOn)}. ${escapeHtml(necessary.description)}</p>${serviceList(inCategory("necessary"), lang)}</fieldset>` + categories.join("") + (optional ? `<p class="gq-consent-id">${escapeHtml(copy.consentId)}: <code data-gq-consent-id data-gq-consent-none="${escapeHtml(copy.noDecision)}">${escapeHtml(state?.consentId ?? copy.noDecision)}</code></p>` : "") + `<div class="gq-consent-preferences__actions">` + (optional ? `<button type="button" class="gq-consent-button gq-consent-button--choice" data-gq-consent-reject-all>${escapeHtml(copy.rejectAll)}</button><button type="button" class="gq-consent-button gq-consent-button--choice" data-gq-consent-accept-all>${escapeHtml(copy.acceptAll)}</button><button type="button" class="gq-consent-button gq-consent-button--secondary" data-gq-consent-save>${escapeHtml(copy.save)}</button>` : "") + `<button type="button" class="gq-consent-button gq-consent-button--secondary" data-gq-consent-close>${escapeHtml(copy.close)}</button></div></form></dialog>`),
|
|
61
|
-
footer: legalFooter(input.legal, input.language),
|
|
62
|
-
bannerOpen
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* The default stylesheet. Sites restyle it through these custom properties or
|
|
67
|
-
* their own CSS (CN-C4); the two first-layer choices share one class so they
|
|
68
|
-
* keep equal prominence.
|
|
69
|
-
*/
|
|
70
|
-
const CONSENT_STYLES = `
|
|
71
|
-
.gq-consent-banner{position:fixed;inset:auto 1rem 1rem 1rem;z-index:2147483000;max-width:44rem;margin:0 auto;padding:1.25rem;background:var(--gq-consent-background,#fff);color:var(--gq-consent-color,#111);border:1px solid var(--gq-consent-border,#888);border-radius:var(--gq-consent-radius,.5rem);box-shadow:0 .5rem 2rem rgba(0,0,0,.2);font:inherit}
|
|
72
|
-
.gq-consent-banner[hidden]{display:none}
|
|
73
|
-
.gq-consent-banner__title,.gq-consent-preferences h2{margin:0 0 .5rem;font-size:1.125rem}
|
|
74
|
-
.gq-consent-banner__actions,.gq-consent-preferences__actions,.gq-consent-embed__actions{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:1rem}
|
|
75
|
-
.gq-consent-button{font:inherit;cursor:pointer;padding:.6rem 1rem;border-radius:var(--gq-consent-radius,.5rem);border:2px solid var(--gq-consent-accent,#111)}
|
|
76
|
-
.gq-consent-button--choice{flex:1 1 10rem;background:var(--gq-consent-accent,#111);color:var(--gq-consent-accent-contrast,#fff)}
|
|
77
|
-
.gq-consent-button--secondary{background:transparent;color:inherit}
|
|
78
|
-
.gq-consent-button:focus-visible,.gq-consent-settings:focus-visible{outline:3px solid var(--gq-consent-focus,#1a73e8);outline-offset:2px}
|
|
79
|
-
.gq-consent-preferences{max-width:40rem;width:calc(100% - 2rem);max-height:calc(100vh - 2rem);overflow:auto;padding:1.25rem;border:1px solid var(--gq-consent-border,#888);border-radius:var(--gq-consent-radius,.5rem);background:var(--gq-consent-background,#fff);color:var(--gq-consent-color,#111)}
|
|
80
|
-
.gq-consent-category{border:1px solid var(--gq-consent-border,#ccc);border-radius:.375rem;margin:.75rem 0;padding:.75rem}
|
|
81
|
-
.gq-consent-services{margin:.5rem 0 0;padding-left:1rem;font-size:.875rem}
|
|
82
|
-
.gq-consent-services dl{display:grid;grid-template-columns:max-content 1fr;gap:.125rem .5rem;margin:.25rem 0}
|
|
83
|
-
.gq-consent-services dd{margin:0}
|
|
84
|
-
.gq-consent-embed{position:relative;display:flex;align-items:center;justify-content:center;width:100%;min-height:12rem;background:var(--gq-consent-embed-background,#eee);color:var(--gq-consent-color,#111)}
|
|
85
|
-
.gq-consent-embed[data-gq-loaded]{display:block;min-height:0;background:none}
|
|
86
|
-
.gq-consent-embed[data-gq-loaded]>iframe{width:100%;height:100%;border:0}
|
|
87
|
-
.gq-consent-embed--decorative{position:absolute;inset:0;min-height:0;background:none}
|
|
88
|
-
.gq-consent-embed__notice{max-width:32rem;padding:1rem;text-align:center}
|
|
89
|
-
.gq-consent-blocked{padding:1rem;background:var(--gq-consent-embed-background,#eee)}
|
|
90
|
-
.gq-legal ul{display:flex;flex-wrap:wrap;gap:.5rem 1rem;list-style:none;margin:0;padding:0}
|
|
91
|
-
.gq-consent-settings{font:inherit;color:inherit;background:none;border:0;padding:0;text-decoration:underline;cursor:pointer}
|
|
92
|
-
`.trim();
|
|
93
|
-
//#endregion
|
|
1
|
+
import { CONSENT_STYLES, consentPage, legalFooter } from "./consent-html.js";
|
|
94
2
|
export { CONSENT_STYLES, consentPage, legalFooter };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { r as ConsentConfiguration } from "./configuration-C_JGygQI.js";
|
|
2
|
+
import { r as LegalLanguage } from "./legal-C5Xiq59W.js";
|
|
3
|
+
//#region frontend/src/consent/html.d.ts
|
|
4
|
+
export interface ConsentPageInput {
|
|
5
|
+
configuration: ConsentConfiguration;
|
|
6
|
+
/** The page's `lang` (BCP 47): the UI follows it (CN-C3). */
|
|
7
|
+
language: string;
|
|
8
|
+
/** The page language's Legal Contract, from the chrome (LEGAL-1); null when not delivered yet. */
|
|
9
|
+
legal?: LegalLanguage | null;
|
|
10
|
+
/** The request's Cookie header: the banner is rendered open only without a decision in force. */
|
|
11
|
+
cookie?: string | null;
|
|
12
|
+
now?: number;
|
|
13
|
+
/** Include the default stylesheet (Sites restyle through its custom properties). */
|
|
14
|
+
styles?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface ConsentPage {
|
|
17
|
+
/** For `<head>`: the configuration the client reads, and the default styles. */
|
|
18
|
+
head: string;
|
|
19
|
+
/** For the start of `<body>`: the banner (first in focus order) and the preferences dialog. */
|
|
20
|
+
body: string;
|
|
21
|
+
/** For the footer: the legal links and the persistent "Cookie settings" control. */
|
|
22
|
+
footer: string;
|
|
23
|
+
/** Whether the banner is rendered open. */
|
|
24
|
+
bannerOpen: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** The legal links and "Cookie settings" control for a footer (LG-R5, CN-R6). */
|
|
27
|
+
export declare function legalFooter(legal: LegalLanguage | null | undefined, language: string): string;
|
|
28
|
+
/** Everything the default UI renders on one page. */
|
|
29
|
+
export declare function consentPage(input: ConsentPageInput): ConsentPage;
|
|
30
|
+
/**
|
|
31
|
+
* The default stylesheet. Sites restyle it through these custom properties or
|
|
32
|
+
* their own CSS (CN-C4); the two first-layer choices share one class so they
|
|
33
|
+
* keep equal prominence.
|
|
34
|
+
*/
|
|
35
|
+
export declare const CONSENT_STYLES: string;
|
|
36
|
+
//#endregion
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { S as SERVICE_CATALOG, T as copyLanguage, c as grantedCategories, d as promptReason, f as readConsentCookie, x as OPTIONAL_CATEGORIES } from "./state-BYWc5TUB.js";
|
|
2
|
+
import { a as CONSENT_COPY, c as legalRevisions, r as escapeHtml } from "./embeds-CJJemAmE.js";
|
|
3
|
+
import { t as CONSENT_UI_VERSION } from "./version-CmdZGJvZ.js";
|
|
4
|
+
import { t as CONSENT_CONFIG_ID } from "./client-config-B3LNSOxl.js";
|
|
5
|
+
import { t as CONSENT_PATH } from "./paths-Cmzzof3v.js";
|
|
6
|
+
//#region frontend/src/consent/html.ts
|
|
7
|
+
function storageText(service, language) {
|
|
8
|
+
const copy = CONSENT_COPY[language];
|
|
9
|
+
return service.storage.map((item) => `${item.name}${item.match === "prefix" ? "*" : ""} (${item.kind}, ${item.duration[language]}${item.party === "third" ? `; ${copy.thirdPartyStorage}` : ""})`).join("; ");
|
|
10
|
+
}
|
|
11
|
+
function serviceList(services, language) {
|
|
12
|
+
if (!services.length) return "";
|
|
13
|
+
const copy = CONSENT_COPY[language];
|
|
14
|
+
return `<ul class="gq-consent-services">${services.map((service) => {
|
|
15
|
+
const storage = storageText(service, language);
|
|
16
|
+
return `<li class="gq-consent-service"><strong>${escapeHtml(service.name)}</strong><dl><dt>${escapeHtml(copy.provider)}</dt><dd>${escapeHtml(service.provider)}</dd><dt>${escapeHtml(copy.purpose)}</dt><dd>${escapeHtml(service.purpose[language])}</dd><dt>${escapeHtml(copy.storage)}</dt><dd>${escapeHtml(storage || "—")}</dd><dt>${escapeHtml(copy.retention)}</dt><dd>${escapeHtml(service.retention[language])}</dd></dl></li>`;
|
|
17
|
+
}).join("")}</ul>`;
|
|
18
|
+
}
|
|
19
|
+
function link(href, text, attributes = "") {
|
|
20
|
+
return `<a href="${escapeHtml(href)}"${attributes}>${escapeHtml(text)}</a>`;
|
|
21
|
+
}
|
|
22
|
+
/** The legal links and "Cookie settings" control for a footer (LG-R5, CN-R6). */
|
|
23
|
+
function legalFooter(legal, language) {
|
|
24
|
+
const lang = copyLanguage(language);
|
|
25
|
+
const copy = CONSENT_COPY[lang];
|
|
26
|
+
const items = (legal?.roles ?? []).map((role) => `<li>${link(role.url, role.title ?? copy.legal[role.role] ?? role.role, ` data-gq-legal-role="${escapeHtml(role.role)}"`)}</li>`);
|
|
27
|
+
items.push(`<li><button type="button" class="gq-consent-settings" data-gq-consent-open aria-haspopup="dialog" aria-controls="gq-consent-preferences">${escapeHtml(copy.cookieSettings)}</button></li>`);
|
|
28
|
+
return `<nav class="gq-legal" aria-label="${escapeHtml(copy.legalNavigation)}"><ul>${items.join("")}</ul></nav>`;
|
|
29
|
+
}
|
|
30
|
+
/** Everything the default UI renders on one page. */
|
|
31
|
+
function consentPage(input) {
|
|
32
|
+
const { configuration } = input;
|
|
33
|
+
const lang = copyLanguage(input.language);
|
|
34
|
+
const copy = CONSENT_COPY[lang];
|
|
35
|
+
const state = readConsentCookie(input.cookie);
|
|
36
|
+
const now = input.now ?? Date.now();
|
|
37
|
+
const bannerOpen = promptReason(configuration, state, now) !== null;
|
|
38
|
+
const inForce = grantedCategories(configuration, state, now);
|
|
39
|
+
const cookiePolicy = input.legal?.roles.find((role) => role.role === "cookie_policy");
|
|
40
|
+
const config = {
|
|
41
|
+
configuration,
|
|
42
|
+
language: input.language,
|
|
43
|
+
uiVersion: CONSENT_UI_VERSION,
|
|
44
|
+
legalRevisions: legalRevisions(input.legal),
|
|
45
|
+
endpoint: CONSENT_PATH
|
|
46
|
+
};
|
|
47
|
+
const head = `<script type="application/json" id="${CONSENT_CONFIG_ID}">${JSON.stringify(config).replaceAll("<", "\\u003c")}<\/script>` + (input.styles === false ? "" : `<style data-gq-consent-styles>${CONSENT_STYLES}</style>`);
|
|
48
|
+
const services = configuration.services.map((id) => SERVICE_CATALOG[id]);
|
|
49
|
+
const inCategory = (category) => services.filter((service) => service.category === category);
|
|
50
|
+
const optional = configuration.categories.length > 0;
|
|
51
|
+
const banner = !optional ? "" : `<section id="gq-consent-banner" class="gq-consent-banner" role="dialog" aria-modal="false" aria-labelledby="gq-consent-banner-title" aria-describedby="gq-consent-banner-text" lang="${escapeHtml(input.language)}"${bannerOpen ? "" : " hidden"}><h2 id="gq-consent-banner-title" class="gq-consent-banner__title">${escapeHtml(copy.bannerTitle)}</h2><p id="gq-consent-banner-text" class="gq-consent-banner__text">${escapeHtml(copy.bannerText)}` + (cookiePolicy ? ` ${link(cookiePolicy.url, copy.cookiePolicy, " class=\"gq-consent-banner__policy\"")}` : "") + `</p><div class="gq-consent-banner__actions"><button type="button" class="gq-consent-button gq-consent-button--choice" data-gq-consent-reject-all>${escapeHtml(copy.rejectAll)}</button><button type="button" class="gq-consent-button gq-consent-button--choice" data-gq-consent-accept-all>${escapeHtml(copy.acceptAll)}</button><button type="button" class="gq-consent-button gq-consent-button--secondary" data-gq-consent-customise aria-haspopup="dialog" aria-controls="gq-consent-preferences">${escapeHtml(copy.customise)}</button></div></section>`;
|
|
52
|
+
const necessary = copy.categories.necessary;
|
|
53
|
+
const categories = OPTIONAL_CATEGORIES.filter((category) => configuration.categories.includes(category)).map((category) => {
|
|
54
|
+
const text = copy.categories[category];
|
|
55
|
+
const id = `gq-consent-category-${category}`;
|
|
56
|
+
return `<fieldset class="gq-consent-category"><legend>${escapeHtml(text.name)}</legend><label for="${id}"><input type="checkbox" id="${id}" name="${category}" value="${category}" data-gq-consent-category${inForce.includes(category) ? " checked" : ""}> ${escapeHtml(text.name)}</label><p>${escapeHtml(text.description)}</p>${serviceList(inCategory(category), lang)}</fieldset>`;
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
head,
|
|
60
|
+
body: banner + (`<dialog id="gq-consent-preferences" class="gq-consent-preferences" aria-labelledby="gq-consent-preferences-title" lang="${escapeHtml(input.language)}"><form method="dialog"><h2 id="gq-consent-preferences-title">${escapeHtml(copy.preferencesTitle)}</h2><p>${escapeHtml(copy.preferencesText)}</p>` + (cookiePolicy ? `<p>${link(cookiePolicy.url, copy.cookiePolicy)}</p>` : "") + `<fieldset class="gq-consent-category"><legend>${escapeHtml(necessary.name)}</legend><p>${escapeHtml(copy.alwaysOn)}. ${escapeHtml(necessary.description)}</p>${serviceList(inCategory("necessary"), lang)}</fieldset>` + categories.join("") + (optional ? `<p class="gq-consent-id">${escapeHtml(copy.consentId)}: <code data-gq-consent-id data-gq-consent-none="${escapeHtml(copy.noDecision)}">${escapeHtml(state?.consentId ?? copy.noDecision)}</code></p>` : "") + `<div class="gq-consent-preferences__actions">` + (optional ? `<button type="button" class="gq-consent-button gq-consent-button--choice" data-gq-consent-reject-all>${escapeHtml(copy.rejectAll)}</button><button type="button" class="gq-consent-button gq-consent-button--choice" data-gq-consent-accept-all>${escapeHtml(copy.acceptAll)}</button><button type="button" class="gq-consent-button gq-consent-button--secondary" data-gq-consent-save>${escapeHtml(copy.save)}</button>` : "") + `<button type="button" class="gq-consent-button gq-consent-button--secondary" data-gq-consent-close>${escapeHtml(copy.close)}</button></div></form></dialog>`),
|
|
61
|
+
footer: legalFooter(input.legal, input.language),
|
|
62
|
+
bannerOpen
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The default stylesheet. Sites restyle it through these custom properties or
|
|
67
|
+
* their own CSS (CN-C4); the two first-layer choices share one class so they
|
|
68
|
+
* keep equal prominence.
|
|
69
|
+
*/
|
|
70
|
+
const CONSENT_STYLES = `
|
|
71
|
+
.gq-consent-banner{position:fixed;inset:auto 1rem 1rem 1rem;z-index:2147483000;max-width:44rem;margin:0 auto;padding:1.25rem;background:var(--gq-consent-background,#fff);color:var(--gq-consent-color,#111);border:1px solid var(--gq-consent-border,#888);border-radius:var(--gq-consent-radius,.5rem);box-shadow:0 .5rem 2rem rgba(0,0,0,.2);font:inherit}
|
|
72
|
+
.gq-consent-banner[hidden]{display:none}
|
|
73
|
+
.gq-consent-banner__title,.gq-consent-preferences h2{margin:0 0 .5rem;font-size:1.125rem}
|
|
74
|
+
.gq-consent-banner__actions,.gq-consent-preferences__actions,.gq-consent-embed__actions{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:1rem}
|
|
75
|
+
.gq-consent-button{font:inherit;cursor:pointer;padding:.6rem 1rem;border-radius:var(--gq-consent-radius,.5rem);border:2px solid var(--gq-consent-accent,#111)}
|
|
76
|
+
.gq-consent-button--choice{flex:1 1 10rem;background:var(--gq-consent-accent,#111);color:var(--gq-consent-accent-contrast,#fff)}
|
|
77
|
+
.gq-consent-button--secondary{background:transparent;color:inherit}
|
|
78
|
+
.gq-consent-button:focus-visible,.gq-consent-settings:focus-visible{outline:3px solid var(--gq-consent-focus,#1a73e8);outline-offset:2px}
|
|
79
|
+
.gq-consent-preferences{max-width:40rem;width:calc(100% - 2rem);max-height:calc(100vh - 2rem);overflow:auto;padding:1.25rem;border:1px solid var(--gq-consent-border,#888);border-radius:var(--gq-consent-radius,.5rem);background:var(--gq-consent-background,#fff);color:var(--gq-consent-color,#111)}
|
|
80
|
+
.gq-consent-category{border:1px solid var(--gq-consent-border,#ccc);border-radius:.375rem;margin:.75rem 0;padding:.75rem}
|
|
81
|
+
.gq-consent-services{margin:.5rem 0 0;padding-left:1rem;font-size:.875rem}
|
|
82
|
+
.gq-consent-services dl{display:grid;grid-template-columns:max-content 1fr;gap:.125rem .5rem;margin:.25rem 0}
|
|
83
|
+
.gq-consent-services dd{margin:0}
|
|
84
|
+
.gq-consent-embed{position:relative;display:flex;align-items:center;justify-content:center;width:100%;min-height:12rem;background:var(--gq-consent-embed-background,#eee);color:var(--gq-consent-color,#111)}
|
|
85
|
+
.gq-consent-embed[data-gq-loaded]{display:block;min-height:0;background:none}
|
|
86
|
+
.gq-consent-embed[data-gq-loaded]>iframe{width:100%;height:100%;border:0}
|
|
87
|
+
.gq-consent-embed--decorative{position:absolute;inset:0;min-height:0;background:none}
|
|
88
|
+
.gq-consent-embed__notice{max-width:32rem;padding:1rem;text-align:center}
|
|
89
|
+
.gq-consent-blocked{padding:1rem;background:var(--gq-consent-embed-background,#eee)}
|
|
90
|
+
.gq-legal ul{display:flex;flex-wrap:wrap;gap:.5rem 1rem;list-style:none;margin:0;padding:0}
|
|
91
|
+
.gq-consent-settings{font:inherit;color:inherit;background:none;border:0;padding:0;text-decoration:underline;cursor:pointer}
|
|
92
|
+
`.trim();
|
|
93
|
+
//#endregion
|
|
94
|
+
export { CONSENT_STYLES, consentPage, legalFooter };
|
package/dist/consent-server.d.ts
CHANGED
|
@@ -45,6 +45,13 @@ export interface ConsentServerConfig {
|
|
|
45
45
|
* request's own origin is always accepted.
|
|
46
46
|
*/
|
|
47
47
|
siteOrigins?: readonly string[];
|
|
48
|
+
/**
|
|
49
|
+
* The reverse proxies the Site runs in front of the Worker, as IPv4/IPv6
|
|
50
|
+
* addresses or CIDRs. Only a request whose `CF-Connecting-IP` is one of
|
|
51
|
+
* them has its client IP taken from the proxy's `X-Forwarded-For` /
|
|
52
|
+
* `X-Real-IP` (docs/client-ip.md).
|
|
53
|
+
*/
|
|
54
|
+
trustedProxies?: readonly string[];
|
|
48
55
|
/** Retention after a decision stops being in force; at most (and by default) 5 years (CN-R10). */
|
|
49
56
|
retentionDays?: number;
|
|
50
57
|
now?: () => number;
|
package/dist/consent-server.js
CHANGED
|
@@ -2,7 +2,7 @@ import { t as bearerMatches } from "./bearer-DMOCoSci.js";
|
|
|
2
2
|
import { i as DECISION_VALIDITY_MS, l as isConsentId, t as CONSENT_ACTIONS, x as OPTIONAL_CATEGORIES } from "./state-BYWc5TUB.js";
|
|
3
3
|
import { a as parseConsentRecordRequest, t as MAX_RECORD_BYTES } from "./records-Dumrz759.js";
|
|
4
4
|
import { t as CONSENT_PATH } from "./paths-Cmzzof3v.js";
|
|
5
|
-
import {
|
|
5
|
+
import { i as readBoundedBody, n as rateLimitKey, r as isSameOrigin, t as clientAddressResolver } from "./client-ip-cnqPLdDa.js";
|
|
6
6
|
//#region frontend/src/consent/server.ts
|
|
7
7
|
const MAX_RETENTION_DAYS = 1826;
|
|
8
8
|
const DAY_MS = 864e5;
|
|
@@ -73,6 +73,7 @@ function createConsentServer(config) {
|
|
|
73
73
|
if (!Number.isInteger(retentionDays) || retentionDays < 1 || retentionDays > 1826) throw new Error(`retentionDays must be a whole number of days from 1 to ${MAX_RETENTION_DAYS}`);
|
|
74
74
|
const now = config.now ?? Date.now;
|
|
75
75
|
const origins = new Set((config.siteOrigins ?? []).map((origin) => new URL(origin).origin));
|
|
76
|
+
const clientAddress = clientAddressResolver(config.trustedProxies);
|
|
76
77
|
/** CN-R9: same-origin only. A browser's cross-site post carries its Origin. */
|
|
77
78
|
const sameOrigin = (request) => isSameOrigin(request, origins);
|
|
78
79
|
const readBody = (request) => readBoundedBody(request, MAX_RECORD_BYTES);
|
|
@@ -90,7 +91,7 @@ function createConsentServer(config) {
|
|
|
90
91
|
const bindings = await config.bindings();
|
|
91
92
|
const limiter = bindings.CONSENT_RATE_LIMITER;
|
|
92
93
|
if (limiter) {
|
|
93
|
-
const key = request
|
|
94
|
+
const key = rateLimitKey(clientAddress(request));
|
|
94
95
|
const { success } = await limiter.limit({ key });
|
|
95
96
|
if (!success) return json(429, { error: "Too many consent records; try again later." });
|
|
96
97
|
}
|
package/dist/consent.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { C as SERVICE_IDS, D as hostMatches, E as embedService, O as hostService, S as SERVICE_CATALOG, T as copyLanguage, _ as consentConfiguration, a as consentCookie, b as COPY_LANGUAGES, c as grantedCategories, d as promptReason, f as readConsentCookie, g as MEASUREMENT_ID, h as CONSENT_COPY_REVISION, i as DECISION_VALIDITY_MS, k as isServiceId, l as isConsentId, m as serviceGranted, n as CONSENT_COOKIE, o as decide, p as serializeConsentState, r as CONSENT_COOKIE_MAX_AGE_SECONDS, s as denyAll, t as CONSENT_ACTIONS, u as parseConsentState, v as needsBanner, w as catalogService, x as OPTIONAL_CATEGORIES, y as CONSENT_CATEGORIES } from "./state-BYWc5TUB.js";
|
|
2
2
|
import { a as parseConsentRecordRequest, i as isRecordToken, n as consentRecordRequest, r as isLegalRoleName, t as MAX_RECORD_BYTES } from "./records-Dumrz759.js";
|
|
3
3
|
import { a as CONSENT_COPY, c as legalRevisions, i as rewriteEmbeds, l as parseLegalLanguage, n as embedPlaceholder, o as LEGAL_ROLES, r as escapeHtml, s as REQUIRED_LEGAL_ROLES, t as blockedPlaceholder } from "./embeds-CJJemAmE.js";
|
|
4
|
-
import { t as CONSENT_UI_VERSION } from "./version-
|
|
4
|
+
import { t as CONSENT_UI_VERSION } from "./version-CmdZGJvZ.js";
|
|
5
5
|
export { CONSENT_ACTIONS, CONSENT_CATEGORIES, CONSENT_COOKIE, CONSENT_COOKIE_MAX_AGE_SECONDS, CONSENT_COPY, CONSENT_COPY_REVISION, CONSENT_UI_VERSION, COPY_LANGUAGES, DECISION_VALIDITY_MS, LEGAL_ROLES, MAX_RECORD_BYTES, MEASUREMENT_ID, OPTIONAL_CATEGORIES, REQUIRED_LEGAL_ROLES, SERVICE_CATALOG, SERVICE_IDS, blockedPlaceholder, catalogService, consentConfiguration, consentCookie, consentRecordRequest, copyLanguage, decide, denyAll, embedPlaceholder, embedService, escapeHtml, grantedCategories, hostMatches, hostService, isConsentId, isLegalRoleName, isRecordToken, isServiceId, legalRevisions, needsBanner, parseConsentRecordRequest, parseConsentState, parseLegalLanguage, promptReason, readConsentCookie, rewriteEmbeds, serializeConsentState, serviceGranted };
|