@getquick/content 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -2
- package/consent-migrations/0001_consent_records.sql +34 -0
- package/dist/bearer-DMOCoSci.js +16 -0
- package/dist/client-config-B3LNSOxl.js +4 -0
- package/dist/configuration-C_JGygQI.d.ts +259 -0
- package/dist/consent-astro.d.ts +36 -0
- package/dist/consent-astro.js +94 -0
- package/dist/consent-client.d.ts +37 -0
- package/dist/consent-client.js +321 -0
- package/dist/consent-migrations.d.ts +8 -0
- package/dist/consent-migrations.js +10 -0
- package/dist/consent-server.d.ts +77 -0
- package/dist/consent-server.js +292 -0
- package/dist/consent-testing.d.ts +96 -0
- package/dist/consent-testing.js +188 -0
- package/dist/consent.d.ts +114 -0
- package/dist/consent.js +5 -0
- package/dist/embeds-p9SyGNZI.js +344 -0
- package/dist/formatting-wOk2jz6J.js +276 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +20 -0
- package/dist/legal-C5Xiq59W.d.ts +48 -0
- package/dist/migrations.d.ts +4 -0
- package/dist/migrations.js +6 -0
- package/dist/paths-Cmzzof3v.js +5 -0
- package/dist/records-CACA2O0Q.d.ts +56 -0
- package/dist/records-Dumrz759.js +74 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +6368 -0
- package/dist/state-BYWc5TUB.js +559 -0
- package/dist/state-Dzd52F3c.d.ts +77 -0
- package/dist/version-Dn6VLZJO.js +6 -0
- package/dist/wordpress-CPL40-vq.d.ts +318 -0
- package/migrations/0001_publications.sql +31 -0
- package/migrations/0002_entries.sql +14 -0
- package/migrations/0003_publication_events.sql +36 -0
- package/migrations/0004_withdrawals.sql +30 -0
- package/migrations/0005_reconciliation.sql +45 -0
- package/package.json +56 -3
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { E as embedService, S as SERVICE_CATALOG, a as consentCookie, c as grantedCategories, d as promptReason, f as readConsentCookie, k as isServiceId, m as serviceGranted, o as decide, x as OPTIONAL_CATEGORIES } from "./state-BYWc5TUB.js";
|
|
2
|
+
import { i as isRecordToken, n as consentRecordRequest, r as isLegalRoleName } from "./records-Dumrz759.js";
|
|
3
|
+
import { t as CONSENT_CONFIG_ID } from "./client-config-B3LNSOxl.js";
|
|
4
|
+
//#region frontend/src/consent/client.ts
|
|
5
|
+
const RETRY_DELAYS_MS = [
|
|
6
|
+
1e3,
|
|
7
|
+
3e3,
|
|
8
|
+
1e4,
|
|
9
|
+
3e4,
|
|
10
|
+
6e4
|
|
11
|
+
];
|
|
12
|
+
function readConfig() {
|
|
13
|
+
const element = document.getElementById(CONSENT_CONFIG_ID);
|
|
14
|
+
if (!element?.textContent) return null;
|
|
15
|
+
try {
|
|
16
|
+
const config = JSON.parse(element.textContent);
|
|
17
|
+
config.legalRevisions = Object.fromEntries(Object.entries(config.legalRevisions ?? {}).filter(([role, revision]) => isLegalRoleName(role) && isRecordToken(revision)));
|
|
18
|
+
return config;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Removes the first-party storage a withdrawn category's services wrote (CN-R6). */
|
|
24
|
+
function removeStorage(categories) {
|
|
25
|
+
const services = Object.values(SERVICE_CATALOG);
|
|
26
|
+
const host = location.hostname;
|
|
27
|
+
const domains = [
|
|
28
|
+
"",
|
|
29
|
+
host,
|
|
30
|
+
`.${host}`
|
|
31
|
+
];
|
|
32
|
+
const parts = host.split(".");
|
|
33
|
+
for (let i = 1; i < parts.length - 1; i++) domains.push(`.${parts.slice(i).join(".")}`);
|
|
34
|
+
for (const service of services) {
|
|
35
|
+
if (!categories.includes(service.category)) continue;
|
|
36
|
+
for (const storage of service.storage) {
|
|
37
|
+
if (storage.party !== "first") continue;
|
|
38
|
+
const matches = (name) => storage.match === "prefix" ? name.startsWith(storage.name) : name === storage.name;
|
|
39
|
+
if (storage.kind === "cookie") for (const cookie of document.cookie.split(";")) {
|
|
40
|
+
const name = cookie.split("=")[0].trim();
|
|
41
|
+
if (!name || !matches(name)) continue;
|
|
42
|
+
for (const domain of domains) document.cookie = `${name}=; Path=/; Max-Age=0${domain ? `; Domain=${domain}` : ""}`;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const area = storage.kind === "localStorage" ? localStorage : sessionStorage;
|
|
46
|
+
for (const key of Object.keys(area)) if (matches(key)) area.removeItem(key);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* GA4 under Consent Mode v2 basic mode (CN-R12): nothing loads before the
|
|
53
|
+
* analytics grant; the denied defaults are queued before the tag; every later
|
|
54
|
+
* decision sends an update. Withdrawal sends denied and disables the tag.
|
|
55
|
+
*/
|
|
56
|
+
function ga4(measurementId) {
|
|
57
|
+
let loaded = false;
|
|
58
|
+
function gtag(..._args) {
|
|
59
|
+
window.dataLayer = window.dataLayer ?? [];
|
|
60
|
+
window.dataLayer.push(arguments);
|
|
61
|
+
}
|
|
62
|
+
const signals = (analytics) => ({
|
|
63
|
+
analytics_storage: analytics ? "granted" : "denied",
|
|
64
|
+
ad_storage: "denied",
|
|
65
|
+
ad_user_data: "denied",
|
|
66
|
+
ad_personalization: "denied"
|
|
67
|
+
});
|
|
68
|
+
return function apply(granted) {
|
|
69
|
+
if (!loaded && !granted) return;
|
|
70
|
+
window[`ga-disable-${measurementId}`] = !granted;
|
|
71
|
+
if (loaded) {
|
|
72
|
+
gtag("consent", "update", signals(granted));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
loaded = true;
|
|
76
|
+
window.gtag = gtag;
|
|
77
|
+
gtag("consent", "default", signals(false));
|
|
78
|
+
gtag("consent", "update", signals(true));
|
|
79
|
+
gtag("js", /* @__PURE__ */ new Date());
|
|
80
|
+
gtag("config", measurementId);
|
|
81
|
+
const script = document.createElement("script");
|
|
82
|
+
script.async = true;
|
|
83
|
+
script.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`;
|
|
84
|
+
document.head.append(script);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const EMBED_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
88
|
+
"allow",
|
|
89
|
+
"allowfullscreen",
|
|
90
|
+
"aria-hidden",
|
|
91
|
+
"class",
|
|
92
|
+
"frameborder",
|
|
93
|
+
"height",
|
|
94
|
+
"loading",
|
|
95
|
+
"referrerpolicy",
|
|
96
|
+
"tabindex",
|
|
97
|
+
"title",
|
|
98
|
+
"width"
|
|
99
|
+
]);
|
|
100
|
+
/**
|
|
101
|
+
* Replaces a placeholder's content with its embed, keeping the placeholder to
|
|
102
|
+
* restore. Only an HTTPS URL of the catalog service the placeholder names
|
|
103
|
+
* loads, so markup can't smuggle another host or a script URL through it.
|
|
104
|
+
*/
|
|
105
|
+
function loadEmbed(placeholder, id) {
|
|
106
|
+
const data = placeholder.getAttribute("data-gq-embed");
|
|
107
|
+
if (!data || placeholder.hasAttribute("data-gq-loaded")) return;
|
|
108
|
+
let embed;
|
|
109
|
+
try {
|
|
110
|
+
embed = JSON.parse(data);
|
|
111
|
+
} catch {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (embed.tag !== "iframe" || !embed.attrs || typeof embed.attrs.src !== "string") return;
|
|
115
|
+
let src;
|
|
116
|
+
try {
|
|
117
|
+
src = new URL(embed.attrs.src);
|
|
118
|
+
} catch {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (src.protocol !== "https:" || embedService(src) !== id) return;
|
|
122
|
+
const iframe = document.createElement("iframe");
|
|
123
|
+
for (const [name, value] of Object.entries(embed.attrs)) if (EMBED_ATTRIBUTES.has(name.toLowerCase()) && typeof value === "string") iframe.setAttribute(name, value);
|
|
124
|
+
iframe.src = src.href;
|
|
125
|
+
const original = placeholder.cloneNode(true);
|
|
126
|
+
placeholder.replaceChildren(iframe);
|
|
127
|
+
placeholder.setAttribute("data-gq-loaded", "");
|
|
128
|
+
placeholder.removeAttribute("role");
|
|
129
|
+
placeholder.removeAttribute("aria-label");
|
|
130
|
+
originals.set(placeholder, original);
|
|
131
|
+
}
|
|
132
|
+
const originals = /* @__PURE__ */ new WeakMap();
|
|
133
|
+
function unloadEmbed(placeholder) {
|
|
134
|
+
const original = originals.get(placeholder);
|
|
135
|
+
if (!original) return;
|
|
136
|
+
placeholder.replaceWith(original);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Starts the runtime once per document. Safe to call again: the existing
|
|
140
|
+
* controller refreshes the page instead.
|
|
141
|
+
*/
|
|
142
|
+
function startConsent() {
|
|
143
|
+
if (window.__gqConsent) {
|
|
144
|
+
window.__gqConsent.refresh();
|
|
145
|
+
return window.__gqConsent;
|
|
146
|
+
}
|
|
147
|
+
const initial = readConfig();
|
|
148
|
+
if (!initial) return null;
|
|
149
|
+
let config = initial;
|
|
150
|
+
let state = readConsentCookie(document.cookie);
|
|
151
|
+
const analytics = config.configuration.measurementId ? ga4(config.configuration.measurementId) : null;
|
|
152
|
+
let retry;
|
|
153
|
+
let opener = null;
|
|
154
|
+
const secure = location.protocol === "https:";
|
|
155
|
+
const writeCookie = (next) => {
|
|
156
|
+
document.cookie = consentCookie(next, { secure });
|
|
157
|
+
};
|
|
158
|
+
const granted = (id) => serviceGranted(config.configuration, state, id);
|
|
159
|
+
function applyEmbeds() {
|
|
160
|
+
for (const placeholder of document.querySelectorAll("[data-gq-consent-embed]")) {
|
|
161
|
+
const id = placeholder.getAttribute("data-gq-consent-embed");
|
|
162
|
+
if (!isServiceId(id)) continue;
|
|
163
|
+
if (granted(id)) loadEmbed(placeholder, id);
|
|
164
|
+
else if (placeholder.hasAttribute("data-gq-loaded") && !placeholder.hasAttribute("data-gq-once")) unloadEmbed(placeholder);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function applyUi() {
|
|
168
|
+
const reason = promptReason(config.configuration, state);
|
|
169
|
+
const banner = document.getElementById("gq-consent-banner");
|
|
170
|
+
if (banner) banner.hidden = reason === null;
|
|
171
|
+
for (const element of document.querySelectorAll("[data-gq-consent-id]")) element.textContent = state?.consentId ?? element.getAttribute("data-gq-consent-none") ?? "";
|
|
172
|
+
const inForce = grantedCategories(config.configuration, state);
|
|
173
|
+
for (const input of document.querySelectorAll("input[data-gq-consent-category]")) input.checked = inForce.includes(input.value);
|
|
174
|
+
}
|
|
175
|
+
function apply() {
|
|
176
|
+
applyUi();
|
|
177
|
+
applyEmbeds();
|
|
178
|
+
analytics?.(granted("ga4"));
|
|
179
|
+
}
|
|
180
|
+
/** One decision, however often its cookie is read again. */
|
|
181
|
+
const sameDecision = (a, b) => Boolean(a && b && a.consentId === b.consentId && a.decidedAt === b.decidedAt);
|
|
182
|
+
async function send(attempt = 0) {
|
|
183
|
+
if (!state || state.recorded) return;
|
|
184
|
+
const decided = state;
|
|
185
|
+
const body = JSON.stringify(consentRecordRequest(decided, decided.action, {
|
|
186
|
+
language: config.language,
|
|
187
|
+
uiVersion: config.uiVersion,
|
|
188
|
+
legalRevisions: config.legalRevisions
|
|
189
|
+
}));
|
|
190
|
+
let retryable = true;
|
|
191
|
+
try {
|
|
192
|
+
const response = await fetch(config.endpoint, {
|
|
193
|
+
method: "POST",
|
|
194
|
+
headers: { "Content-Type": "application/json" },
|
|
195
|
+
body,
|
|
196
|
+
credentials: "same-origin"
|
|
197
|
+
});
|
|
198
|
+
if (response.ok) {
|
|
199
|
+
if (sameDecision(state, decided)) {
|
|
200
|
+
state = {
|
|
201
|
+
...state,
|
|
202
|
+
recorded: true
|
|
203
|
+
};
|
|
204
|
+
writeCookie(state);
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
retryable = response.status >= 500 || response.status === 429 || response.status === 408;
|
|
209
|
+
} catch {}
|
|
210
|
+
if (!retryable || !sameDecision(state, decided) || attempt >= RETRY_DELAYS_MS.length) return;
|
|
211
|
+
retry = setTimeout(() => {
|
|
212
|
+
retry = void 0;
|
|
213
|
+
send(attempt + 1);
|
|
214
|
+
}, RETRY_DELAYS_MS[attempt]);
|
|
215
|
+
}
|
|
216
|
+
function choose(choice) {
|
|
217
|
+
const before = grantedCategories(config.configuration, state);
|
|
218
|
+
state = decide(config.configuration, state, choice).state;
|
|
219
|
+
writeCookie(state);
|
|
220
|
+
const after = grantedCategories(config.configuration, state);
|
|
221
|
+
removeStorage(before.filter((category) => !after.includes(category)));
|
|
222
|
+
apply();
|
|
223
|
+
clearTimeout(retry);
|
|
224
|
+
retry = void 0;
|
|
225
|
+
send();
|
|
226
|
+
}
|
|
227
|
+
function preferences() {
|
|
228
|
+
return document.getElementById("gq-consent-preferences");
|
|
229
|
+
}
|
|
230
|
+
function openPreferences(from) {
|
|
231
|
+
const dialog = preferences();
|
|
232
|
+
if (!dialog) return;
|
|
233
|
+
opener = from ?? document.activeElement;
|
|
234
|
+
applyUi();
|
|
235
|
+
if (!dialog.open) dialog.showModal();
|
|
236
|
+
}
|
|
237
|
+
function closePreferences() {
|
|
238
|
+
const dialog = preferences();
|
|
239
|
+
if (dialog?.open) dialog.close();
|
|
240
|
+
}
|
|
241
|
+
function leaveBanner() {
|
|
242
|
+
if (document.getElementById("gq-consent-banner")?.contains(document.activeElement)) {
|
|
243
|
+
const main = document.querySelector("main") ?? document.body;
|
|
244
|
+
if (!main.hasAttribute("tabindex")) main.setAttribute("tabindex", "-1");
|
|
245
|
+
main.focus({ preventScroll: true });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
document.addEventListener("click", (event) => {
|
|
249
|
+
const target = event.target?.closest("[data-gq-consent-accept-all],[data-gq-consent-reject-all],[data-gq-consent-customise],[data-gq-consent-open],[data-gq-consent-save],[data-gq-consent-close],[data-gq-consent-load-once],[data-gq-consent-allow]");
|
|
250
|
+
if (!target) return;
|
|
251
|
+
const ui = target.closest("#gq-consent-banner, #gq-consent-preferences");
|
|
252
|
+
const placeholder = target.closest("[data-gq-consent-embed]");
|
|
253
|
+
if ((target.hasAttribute("data-gq-consent-accept-all") || target.hasAttribute("data-gq-consent-reject-all") || target.hasAttribute("data-gq-consent-save") || target.hasAttribute("data-gq-consent-close")) && !ui) return;
|
|
254
|
+
if ((target.hasAttribute("data-gq-consent-load-once") || target.hasAttribute("data-gq-consent-allow")) && !placeholder) return;
|
|
255
|
+
event.preventDefault();
|
|
256
|
+
if (target.hasAttribute("data-gq-consent-accept-all")) {
|
|
257
|
+
leaveBanner();
|
|
258
|
+
choose({ kind: "accept_all" });
|
|
259
|
+
closePreferences();
|
|
260
|
+
} else if (target.hasAttribute("data-gq-consent-reject-all")) {
|
|
261
|
+
leaveBanner();
|
|
262
|
+
choose({ kind: "reject_all" });
|
|
263
|
+
closePreferences();
|
|
264
|
+
} else if (target.hasAttribute("data-gq-consent-customise") || target.hasAttribute("data-gq-consent-open")) openPreferences(target);
|
|
265
|
+
else if (target.hasAttribute("data-gq-consent-save")) {
|
|
266
|
+
const decisions = {};
|
|
267
|
+
for (const input of document.querySelectorAll("#gq-consent-preferences input[data-gq-consent-category]")) decisions[input.value] = input.checked;
|
|
268
|
+
leaveBanner();
|
|
269
|
+
choose({
|
|
270
|
+
kind: "custom",
|
|
271
|
+
decisions
|
|
272
|
+
});
|
|
273
|
+
closePreferences();
|
|
274
|
+
} else if (target.hasAttribute("data-gq-consent-close")) closePreferences();
|
|
275
|
+
else if (target.hasAttribute("data-gq-consent-load-once")) {
|
|
276
|
+
const id = placeholder.getAttribute("data-gq-consent-embed");
|
|
277
|
+
if (isServiceId(id)) {
|
|
278
|
+
placeholder.setAttribute("data-gq-once", "");
|
|
279
|
+
loadEmbed(placeholder, id);
|
|
280
|
+
}
|
|
281
|
+
} else if (target.hasAttribute("data-gq-consent-allow")) {
|
|
282
|
+
const id = placeholder.getAttribute("data-gq-consent-embed");
|
|
283
|
+
if (!isServiceId(id)) return;
|
|
284
|
+
const category = SERVICE_CATALOG[id].category;
|
|
285
|
+
if (category === "necessary") return;
|
|
286
|
+
const decisions = Object.fromEntries(OPTIONAL_CATEGORIES.map((each) => [each, grantedCategories(config.configuration, state).includes(each)]));
|
|
287
|
+
decisions[category] = true;
|
|
288
|
+
choose({
|
|
289
|
+
kind: "custom",
|
|
290
|
+
decisions
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
document.addEventListener("close", (event) => {
|
|
295
|
+
if (event.target === preferences() && opener?.isConnected) opener.focus();
|
|
296
|
+
}, true);
|
|
297
|
+
const controller = {
|
|
298
|
+
get state() {
|
|
299
|
+
return state;
|
|
300
|
+
},
|
|
301
|
+
get config() {
|
|
302
|
+
return config;
|
|
303
|
+
},
|
|
304
|
+
choose,
|
|
305
|
+
openPreferences,
|
|
306
|
+
refresh() {
|
|
307
|
+
config = readConfig() ?? config;
|
|
308
|
+
const read = readConsentCookie(document.cookie);
|
|
309
|
+
if (!sameDecision(read, state)) state = read;
|
|
310
|
+
apply();
|
|
311
|
+
if (state && !state.recorded && retry === void 0) send();
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
window.__gqConsent = controller;
|
|
315
|
+
document.addEventListener("astro:page-load", () => controller.refresh());
|
|
316
|
+
apply();
|
|
317
|
+
send();
|
|
318
|
+
return controller;
|
|
319
|
+
}
|
|
320
|
+
//#endregion
|
|
321
|
+
export { CONSENT_CONFIG_ID, startConsent };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region frontend/src/consent/migrations.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The consent database's migrations, for infrastructure (Alchemy/Wrangler):
|
|
4
|
+
* a directory separate from the publication store's. Node-only; keep this
|
|
5
|
+
* entry out of browser and Worker imports.
|
|
6
|
+
*/
|
|
7
|
+
export declare const consentMigrationsDirectory: string;
|
|
8
|
+
//#endregion
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
//#region frontend/src/consent/migrations.ts
|
|
3
|
+
/**
|
|
4
|
+
* The consent database's migrations, for infrastructure (Alchemy/Wrangler):
|
|
5
|
+
* a directory separate from the publication store's. Node-only; keep this
|
|
6
|
+
* entry out of browser and Worker imports.
|
|
7
|
+
*/
|
|
8
|
+
const consentMigrationsDirectory = fileURLToPath(new URL("../consent-migrations/", import.meta.url));
|
|
9
|
+
//#endregion
|
|
10
|
+
export { consentMigrationsDirectory };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { n as ConsentRecordRequest, t as ConsentRecord } from "./records-CACA2O0Q.js";
|
|
2
|
+
//#region frontend/src/consent/paths.d.ts
|
|
3
|
+
/** The same-origin consent endpoint on the Site's Frontend Worker (fixed interface). */
|
|
4
|
+
export declare const CONSENT_PATH = "/gq/consent";
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region frontend/src/consent/server.d.ts
|
|
7
|
+
/** D1's surface the consent endpoints use (structural, as the publication store's). */
|
|
8
|
+
export interface ConsentStatement {
|
|
9
|
+
bind(...values: unknown[]): ConsentStatement;
|
|
10
|
+
first<T = Record<string, unknown>>(): Promise<T | null>;
|
|
11
|
+
all<T = Record<string, unknown>>(): Promise<{
|
|
12
|
+
results: T[];
|
|
13
|
+
}>;
|
|
14
|
+
run(): Promise<{
|
|
15
|
+
meta: {
|
|
16
|
+
changes: number;
|
|
17
|
+
};
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
export interface ConsentDatabase {
|
|
21
|
+
prepare(query: string): ConsentStatement;
|
|
22
|
+
}
|
|
23
|
+
/** A Workers Rate Limiting binding. */
|
|
24
|
+
export interface ConsentRateLimiter {
|
|
25
|
+
limit(options: {
|
|
26
|
+
key: string;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
success: boolean;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
export interface ConsentBindings {
|
|
32
|
+
/** The Site's consent database (D1, jurisdiction "eu"), never the publication store. */
|
|
33
|
+
CONSENT_DB?: ConsentDatabase;
|
|
34
|
+
/** The CMS-held bearer secret for the read endpoints (a Worker secret, ≥ 32 characters). */
|
|
35
|
+
CONSENT_READ_TOKEN?: string;
|
|
36
|
+
/** A Workers Rate Limiting binding for record writes, keyed per client IP per request. */
|
|
37
|
+
CONSENT_RATE_LIMITER?: ConsentRateLimiter;
|
|
38
|
+
}
|
|
39
|
+
export interface ConsentServerConfig {
|
|
40
|
+
/** The Site's identity, recorded with each decision. */
|
|
41
|
+
siteId: string;
|
|
42
|
+
bindings: () => ConsentBindings | Promise<ConsentBindings>;
|
|
43
|
+
/**
|
|
44
|
+
* The Site's public origin(s), when the Worker sees another (a proxy). The
|
|
45
|
+
* request's own origin is always accepted.
|
|
46
|
+
*/
|
|
47
|
+
siteOrigins?: readonly string[];
|
|
48
|
+
/** Retention after a decision stops being in force; at most (and by default) 5 years (CN-R10). */
|
|
49
|
+
retentionDays?: number;
|
|
50
|
+
now?: () => number;
|
|
51
|
+
}
|
|
52
|
+
export declare const MAX_RETENTION_DAYS = 1826;
|
|
53
|
+
/** The CSV export's columns, in order (documented; stable within the schema version). */
|
|
54
|
+
export declare const CONSENT_EXPORT_COLUMNS: readonly ["recorded_at", "consent_id", "site_id", "action", "config_version", "language", "ui_version", "preferences", "analytics", "marketing", "external_media", "legal_revisions"];
|
|
55
|
+
/**
|
|
56
|
+
* The consent endpoints. `handle` routes `/gq/consent` (record writes) and
|
|
57
|
+
* `/gq/consent/records/<id>`, `/gq/consent/export`, `/gq/consent/stats`
|
|
58
|
+
* (authenticated reads), and answers null for any other path. `purge` is the
|
|
59
|
+
* scheduled retention job.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createConsentServer(config: ConsentServerConfig): {
|
|
62
|
+
handle: (request: Request) => Promise<Response | null>;
|
|
63
|
+
handleRecord: (request: Request) => Promise<Response>;
|
|
64
|
+
handleRead: (request: Request) => Promise<Response>;
|
|
65
|
+
purge: () => Promise<{
|
|
66
|
+
deleted: number;
|
|
67
|
+
}>;
|
|
68
|
+
check: () => Promise<{
|
|
69
|
+
records: boolean;
|
|
70
|
+
reads: boolean;
|
|
71
|
+
rateLimit: boolean;
|
|
72
|
+
retentionDays: number;
|
|
73
|
+
}>;
|
|
74
|
+
};
|
|
75
|
+
export type ConsentServer = ReturnType<typeof createConsentServer>;
|
|
76
|
+
//#endregion
|
|
77
|
+
export type { ConsentRecord, ConsentRecordRequest };
|