@getquick/content 0.0.0-stage → 0.2.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.
Files changed (39) hide show
  1. package/README.md +179 -2
  2. package/consent-migrations/0001_consent_records.sql +34 -0
  3. package/dist/bearer-DMOCoSci.js +16 -0
  4. package/dist/client-config-B3LNSOxl.js +4 -0
  5. package/dist/configuration-C_JGygQI.d.ts +259 -0
  6. package/dist/consent-astro.d.ts +36 -0
  7. package/dist/consent-astro.js +94 -0
  8. package/dist/consent-client.d.ts +37 -0
  9. package/dist/consent-client.js +321 -0
  10. package/dist/consent-migrations.d.ts +8 -0
  11. package/dist/consent-migrations.js +10 -0
  12. package/dist/consent-server.d.ts +77 -0
  13. package/dist/consent-server.js +292 -0
  14. package/dist/consent-testing.d.ts +96 -0
  15. package/dist/consent-testing.js +188 -0
  16. package/dist/consent.d.ts +114 -0
  17. package/dist/consent.js +5 -0
  18. package/dist/embeds-p9SyGNZI.js +344 -0
  19. package/dist/formatting-wOk2jz6J.js +276 -0
  20. package/dist/index.d.ts +75 -0
  21. package/dist/index.js +39 -0
  22. package/dist/legal-C5Xiq59W.d.ts +48 -0
  23. package/dist/migrations.d.ts +4 -0
  24. package/dist/migrations.js +6 -0
  25. package/dist/paths-Cmzzof3v.js +5 -0
  26. package/dist/records-CACA2O0Q.d.ts +56 -0
  27. package/dist/records-Dumrz759.js +74 -0
  28. package/dist/server.d.ts +2 -0
  29. package/dist/server.js +6465 -0
  30. package/dist/state-BYWc5TUB.js +559 -0
  31. package/dist/state-Dzd52F3c.d.ts +77 -0
  32. package/dist/version-DyyNhsuU.js +6 -0
  33. package/dist/wordpress-CUFb6YZZ.d.ts +384 -0
  34. package/migrations/0001_publications.sql +31 -0
  35. package/migrations/0002_entries.sql +14 -0
  36. package/migrations/0003_publication_events.sql +36 -0
  37. package/migrations/0004_withdrawals.sql +30 -0
  38. package/migrations/0005_reconciliation.sql +45 -0
  39. package/package.json +56 -3
@@ -0,0 +1,114 @@
1
+ import { C as hostMatches, S as embedService, T as isServiceId, _ as SERVICE_CATALOG, a as MEASUREMENT_ID, b as catalogService, c as CONSENT_CATEGORIES, d as ConsentService, f as ConsentStorage, g as OptionalCategory, h as OPTIONAL_CATEGORIES, i as ConsentConfigurationInput, l as COPY_LANGUAGES, m as CopyLanguage, n as CONSENT_COPY_REVISION, o as consentConfiguration, p as Copy, r as ConsentConfiguration, s as needsBanner, t as AnalyticsSettings, u as ConsentCategory, v as SERVICE_IDS, w as hostService, x as copyLanguage, y as ServiceId } from "./configuration-C_JGygQI.js";
2
+ import { a as REQUIRED_LEGAL_ROLES, i as LegalRole, n as LegalContract, o as legalRevisions, r as LegalLanguage, s as parseLegalLanguage, t as LEGAL_ROLES } from "./legal-C5Xiq59W.js";
3
+ import { _ as serializeConsentState, a as ConsentChoice, c as DECISION_VALIDITY_MS, d as denyAll, f as grantedCategories, g as readConsentCookie, h as promptReason, i as ConsentAction, l as consentCookie, m as parseConsentState, n as CONSENT_COOKIE, o as ConsentDecisions, p as isConsentId, r as CONSENT_COOKIE_MAX_AGE_SECONDS, s as ConsentState, t as CONSENT_ACTIONS, u as decide, v as serviceGranted } from "./state-Dzd52F3c.js";
4
+ import { a as isLegalRoleName, i as consentRecordRequest, n as ConsentRecordRequest, o as isRecordToken, r as MAX_RECORD_BYTES, s as parseConsentRecordRequest, t as ConsentRecord } from "./records-CACA2O0Q.js";
5
+ //#region frontend/src/consent/copy.d.ts
6
+ /**
7
+ * Runtime-owned copy (CN-C3, CN-C4): Sites restyle it but do not edit it.
8
+ * Drafted for CONSENT-1 and pending the legal review recorded in ADR 0008's
9
+ * open items; changing what it says about processing bumps
10
+ * CONSENT_COPY_REVISION.
11
+ */
12
+ export interface ConsentCopy {
13
+ bannerTitle: string;
14
+ bannerText: string;
15
+ acceptAll: string;
16
+ rejectAll: string;
17
+ customise: string;
18
+ save: string;
19
+ close: string;
20
+ preferencesTitle: string;
21
+ preferencesText: string;
22
+ cookieSettings: string;
23
+ cookiePolicy: string;
24
+ consentId: string;
25
+ noDecision: string;
26
+ provider: string;
27
+ purpose: string;
28
+ storage: string;
29
+ retention: string;
30
+ thirdPartyStorage: string;
31
+ alwaysOn: string;
32
+ categories: Record<ConsentCategory, {
33
+ name: string;
34
+ description: string;
35
+ }>;
36
+ placeholderText: (service: string, provider: string) => string;
37
+ loadOnce: string;
38
+ alwaysAllow: string;
39
+ blockedText: (host: string) => string;
40
+ legal: Record<string, string>;
41
+ legalNavigation: string;
42
+ }
43
+ export declare const CONSENT_COPY: Record<CopyLanguage, ConsentCopy>;
44
+ //#endregion
45
+ //#region frontend/src/consent/embeds.d.ts
46
+ /** Where rewriting runs: the Site's consent configuration and the page's language. */
47
+ export interface EmbedContext {
48
+ configuration: ConsentConfiguration;
49
+ /** The page's `lang` (BCP 47); placeholders follow it (CN-C3). */
50
+ language: string;
51
+ /**
52
+ * Hosts that are the Site itself (its Frontend and its own CMS/media origin).
53
+ * Relative URLs are always first party.
54
+ */
55
+ firstPartyHosts?: readonly string[];
56
+ }
57
+ /** An embed rewriting withheld for good: from an unknown host, an inactive service, or a script. */
58
+ export interface BlockedEmbed {
59
+ tag: "iframe" | "script" | "embed" | "object";
60
+ /** The host it would have contacted; `inline` for an inline script. */
61
+ host: string;
62
+ service: ServiceId | null;
63
+ reason: "unknown-host" | "inactive-service" | "script";
64
+ }
65
+ export interface RewrittenHtml {
66
+ html: string;
67
+ /** Embeds turned into service placeholders, loadable after consent. */
68
+ placeholders: Array<{
69
+ service: ServiceId;
70
+ host: string;
71
+ }>;
72
+ /** Embeds blocked for good (CN-R3), for diagnostics and readiness. */
73
+ blocked: BlockedEmbed[];
74
+ }
75
+ /** Text safe inside an attribute value or element content. */
76
+ export declare function escapeHtml(value: string): string;
77
+ export interface PlaceholderInput {
78
+ service: ServiceId;
79
+ /** The embed's URL; it is only an attribute value until consent. */
80
+ src: string;
81
+ /** The page's `lang`. */
82
+ language: string;
83
+ title?: string;
84
+ width?: string | number;
85
+ height?: string | number;
86
+ /** Further iframe attributes (allow, allowfullscreen, class…), applied when it loads. */
87
+ attributes?: Record<string, string>;
88
+ /** A decorative embed (a background video): no notice or buttons; loads only on a grant. */
89
+ decorative?: boolean;
90
+ }
91
+ /**
92
+ * A placeholder for a third-party embed (CN-R13): no live `src`, naming the
93
+ * provider and purpose and offering "Load once" and "Always allow external
94
+ * media". The client runtime replaces it with the iframe after consent.
95
+ * Without JavaScript it never loads. Site components use it for their own
96
+ * embeds (a contact map); rewriteEmbeds uses it for WordPress HTML.
97
+ */
98
+ export declare function embedPlaceholder(input: PlaceholderInput): string;
99
+ /** The generic placeholder for an embed that may never load (CN-R3). */
100
+ export declare function blockedPlaceholder(host: string, tag: BlockedEmbed["tag"], language: string): string;
101
+ /**
102
+ * Rewrites published HTML so its initial form contacts no third party
103
+ * (CN-R1, CN-R3, CN-R13): iframes of active catalog services become
104
+ * placeholders; other third-party iframes, embeds and objects, and every
105
+ * third-party or inline executable script, are blocked and reported.
106
+ * First-party and non-executable content is left byte for byte.
107
+ */
108
+ export declare function rewriteEmbeds(html: string, context: EmbedContext): RewrittenHtml;
109
+ //#endregion
110
+ //#region frontend/src/consent/version.d.ts
111
+ /** The UI/adapter version recorded with each decision (CN-R7). */
112
+ export declare const CONSENT_UI_VERSION: string;
113
+ //#endregion
114
+ export { AnalyticsSettings, CONSENT_ACTIONS, CONSENT_CATEGORIES, CONSENT_COOKIE, CONSENT_COOKIE_MAX_AGE_SECONDS, CONSENT_COPY_REVISION, COPY_LANGUAGES, ConsentAction, ConsentCategory, ConsentChoice, ConsentConfiguration, ConsentConfigurationInput, ConsentDecisions, ConsentRecord, ConsentRecordRequest, ConsentService, ConsentState, ConsentStorage, Copy, CopyLanguage, DECISION_VALIDITY_MS, LEGAL_ROLES, type LegalContract, type LegalLanguage, type LegalRole, MAX_RECORD_BYTES, MEASUREMENT_ID, OPTIONAL_CATEGORIES, OptionalCategory, REQUIRED_LEGAL_ROLES, SERVICE_CATALOG, SERVICE_IDS, ServiceId, catalogService, consentConfiguration, consentCookie, consentRecordRequest, copyLanguage, decide, denyAll, embedService, grantedCategories, hostMatches, hostService, isConsentId, isLegalRoleName, isRecordToken, isServiceId, legalRevisions, needsBanner, parseConsentRecordRequest, parseConsentState, parseLegalLanguage, promptReason, readConsentCookie, serializeConsentState, serviceGranted };
@@ -0,0 +1,5 @@
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
+ import { a as parseConsentRecordRequest, i as isRecordToken, n as consentRecordRequest, r as isLegalRoleName, t as MAX_RECORD_BYTES } from "./records-Dumrz759.js";
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-p9SyGNZI.js";
4
+ import { t as CONSENT_UI_VERSION } from "./version-DyyNhsuU.js";
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 };
@@ -0,0 +1,344 @@
1
+ import { D as hostMatches, E as embedService, O as hostService, S as SERVICE_CATALOG, T as copyLanguage } from "./state-BYWc5TUB.js";
2
+ import { i as isRecordToken, r as isLegalRoleName } from "./records-Dumrz759.js";
3
+ //#region frontend/src/consent/legal.ts
4
+ /**
5
+ * LEGAL-1's Legal Contract, as gq-config publishes it (fixed in the consent
6
+ * execution record): per language, each assigned role's public URL, title and
7
+ * revision, plus the controller's identity.
8
+ */
9
+ const LEGAL_ROLES = [
10
+ "privacy_policy",
11
+ "cookie_policy",
12
+ "terms_of_use",
13
+ "legal_notice",
14
+ "terms_of_sale",
15
+ "returns_policy",
16
+ "shipping_policy",
17
+ "complaints_book",
18
+ "dispute_resolution"
19
+ ];
20
+ /** The roles every Site needs; commerce and Portuguese-consumer roles come from gq-config's `missingRequired`. */
21
+ const REQUIRED_LEGAL_ROLES = ["privacy_policy", "cookie_policy"];
22
+ const text = (value) => typeof value === "string" && value.trim() ? value : null;
23
+ function safeUrl(value) {
24
+ if (typeof value !== "string") return null;
25
+ if (value.startsWith("/") && !value.startsWith("//")) return value;
26
+ try {
27
+ const url = new URL(value);
28
+ return url.protocol === "https:" || url.protocol === "http:" ? url.href : null;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ /**
34
+ * One language's Contract from untrusted input (GraphQL or the storefront
35
+ * payload): unknown roles, unsafe URLs and malformed entries are dropped.
36
+ */
37
+ function parseLegalLanguage(value) {
38
+ if (!value || typeof value !== "object") return null;
39
+ const raw = value;
40
+ const controller = raw.controller ?? {};
41
+ const roles = [];
42
+ for (const entry of Array.isArray(raw.roles) ? raw.roles : []) {
43
+ if (!entry || typeof entry !== "object") continue;
44
+ const role = entry;
45
+ const url = safeUrl(role.url);
46
+ if (typeof role.role !== "string" || !LEGAL_ROLES.includes(role.role)) continue;
47
+ if (!url || roles.some((each) => each.role === role.role)) continue;
48
+ roles.push({
49
+ role: role.role,
50
+ url,
51
+ title: text(role.title),
52
+ revision: text(role.revision),
53
+ isFallback: role.isFallback === true
54
+ });
55
+ }
56
+ roles.sort((a, b) => LEGAL_ROLES.indexOf(a.role) - LEGAL_ROLES.indexOf(b.role));
57
+ return {
58
+ controller: {
59
+ name: text(controller.name),
60
+ privacyEmail: text(controller.privacyEmail),
61
+ dpoContact: text(controller.dpoContact)
62
+ },
63
+ roles,
64
+ missingRequired: (Array.isArray(raw.missingRequired) ? raw.missingRequired : []).filter(isLegalRoleName)
65
+ };
66
+ }
67
+ /**
68
+ * The LEGAL-1 revisions in force on a page, recorded with each decision
69
+ * (CN-R7). A revision the record schema can't hold is left out rather than
70
+ * making every record invalid.
71
+ */
72
+ function legalRevisions(legal) {
73
+ return Object.fromEntries((legal?.roles ?? []).flatMap((role) => isRecordToken(role.revision) ? [[role.role, role.revision]] : []));
74
+ }
75
+ //#endregion
76
+ //#region frontend/src/consent/copy.ts
77
+ const CONSENT_COPY = {
78
+ en: {
79
+ bannerTitle: "Your privacy choices",
80
+ bannerText: "We use what is strictly necessary for this website to work. With your consent we also use the services listed in the settings, for the purposes shown there. You can accept or reject them all, choose by purpose, and change your mind at any time in Cookie settings.",
81
+ acceptAll: "Accept all",
82
+ rejectAll: "Reject all",
83
+ customise: "Choose by purpose",
84
+ save: "Save choices",
85
+ close: "Close",
86
+ preferencesTitle: "Cookie settings",
87
+ preferencesText: "Choose which purposes you allow. Nothing optional is used until you allow it, and you can withdraw at any time.",
88
+ cookieSettings: "Cookie settings",
89
+ cookiePolicy: "Cookie policy",
90
+ consentId: "Your consent ID",
91
+ noDecision: "No choice recorded yet",
92
+ provider: "Provider",
93
+ purpose: "Purpose",
94
+ storage: "Storage",
95
+ retention: "Retention",
96
+ thirdPartyStorage: "set by the provider; this website cannot remove it",
97
+ alwaysOn: "Always on",
98
+ categories: {
99
+ necessary: {
100
+ name: "Strictly necessary",
101
+ description: "Needed for the website to work and to remember your choices."
102
+ },
103
+ preferences: {
104
+ name: "Preferences",
105
+ description: "Remembers settings that make the website more convenient."
106
+ },
107
+ analytics: {
108
+ name: "Analytics",
109
+ description: "Helps us understand, in aggregate, how the website is used."
110
+ },
111
+ marketing: {
112
+ name: "Marketing",
113
+ description: "Measures and personalises advertising."
114
+ },
115
+ external_media: {
116
+ name: "External media",
117
+ description: "Loads videos, maps and other content from other providers."
118
+ }
119
+ },
120
+ placeholderText: (service, provider) => `This content is hosted by ${service} (${provider}). Loading it sends your IP address to that provider and may let it store data on your device.`,
121
+ loadOnce: "Load once",
122
+ alwaysAllow: "Always allow external media",
123
+ blockedText: (host) => `Content from ${host} is blocked: it isn't a service this website uses.`,
124
+ legal: {
125
+ privacy_policy: "Privacy policy",
126
+ cookie_policy: "Cookie policy",
127
+ terms_of_use: "Terms of use",
128
+ legal_notice: "Legal notice",
129
+ terms_of_sale: "Terms of sale",
130
+ returns_policy: "Returns policy",
131
+ shipping_policy: "Shipping policy",
132
+ complaints_book: "Complaints book",
133
+ dispute_resolution: "Dispute resolution"
134
+ },
135
+ legalNavigation: "Legal information"
136
+ },
137
+ "pt-PT": {
138
+ bannerTitle: "As suas escolhas de privacidade",
139
+ bannerText: "Usamos o estritamente necessário para que este website funcione. Com o seu consentimento, usamos também os serviços indicados nas definições, para as finalidades aí descritas. Pode aceitar ou rejeitar todos, escolher por finalidade e mudar de ideias a qualquer momento nas Definições de cookies.",
140
+ acceptAll: "Aceitar todos",
141
+ rejectAll: "Rejeitar todos",
142
+ customise: "Escolher por finalidade",
143
+ save: "Guardar escolhas",
144
+ close: "Fechar",
145
+ preferencesTitle: "Definições de cookies",
146
+ preferencesText: "Escolha as finalidades que autoriza. Nada opcional é usado até o autorizar, e pode retirar o consentimento a qualquer momento.",
147
+ cookieSettings: "Definições de cookies",
148
+ cookiePolicy: "Política de cookies",
149
+ consentId: "O seu ID de consentimento",
150
+ noDecision: "Ainda não há escolha registada",
151
+ provider: "Fornecedor",
152
+ purpose: "Finalidade",
153
+ storage: "Armazenamento",
154
+ retention: "Conservação",
155
+ thirdPartyStorage: "definido pelo fornecedor; este website não o pode remover",
156
+ alwaysOn: "Sempre ativo",
157
+ categories: {
158
+ necessary: {
159
+ name: "Estritamente necessários",
160
+ description: "Necessários para o funcionamento do website e para memorizar as suas escolhas."
161
+ },
162
+ preferences: {
163
+ name: "Preferências",
164
+ description: "Memorizam definições que tornam o website mais conveniente."
165
+ },
166
+ analytics: {
167
+ name: "Estatísticas",
168
+ description: "Ajudam-nos a perceber, de forma agregada, como o website é usado."
169
+ },
170
+ marketing: {
171
+ name: "Marketing",
172
+ description: "Medem e personalizam a publicidade."
173
+ },
174
+ external_media: {
175
+ name: "Conteúdos externos",
176
+ description: "Carregam vídeos, mapas e outros conteúdos de outros fornecedores."
177
+ }
178
+ },
179
+ placeholderText: (service, provider) => `Este conteúdo está alojado em ${service} (${provider}). Carregá-lo envia o seu endereço IP a esse fornecedor e pode permitir-lhe guardar dados no seu dispositivo.`,
180
+ loadOnce: "Carregar uma vez",
181
+ alwaysAllow: "Permitir sempre conteúdos externos",
182
+ blockedText: (host) => `O conteúdo de ${host} está bloqueado: não é um serviço usado por este website.`,
183
+ legal: {
184
+ privacy_policy: "Política de privacidade",
185
+ cookie_policy: "Política de cookies",
186
+ terms_of_use: "Termos de utilização",
187
+ legal_notice: "Informação legal",
188
+ terms_of_sale: "Termos e condições de venda",
189
+ returns_policy: "Política de devoluções",
190
+ shipping_policy: "Política de envios",
191
+ complaints_book: "Livro de reclamações",
192
+ dispute_resolution: "Resolução alternativa de litígios"
193
+ },
194
+ legalNavigation: "Informação legal"
195
+ }
196
+ };
197
+ //#endregion
198
+ //#region frontend/src/consent/embeds.ts
199
+ const ATTRIBUTE = /([^\s"'<>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
200
+ const ATTRIBUTES = `((?:[^>"']|"[^"]*"|'[^']*')*)`;
201
+ const ELEMENT = new RegExp(`<(iframe|object)\\b${ATTRIBUTES}>[\\s\\S]*?<\\/\\1\\s*>|<(script)\\b${ATTRIBUTES}>([\\s\\S]*?)<\\/script\\s*>|<(iframe|embed)\\b${ATTRIBUTES}\\/?>`, "gi");
202
+ const NON_EXECUTABLE = /^(?:application\/(?:ld\+)?json|text\/template|text\/x-template|importmap)$/i;
203
+ const RUNTIME_HOOKS = /\bdata-gq-(consent-[a-z-]*|embed|decorative|loaded|once)\b/gi;
204
+ function codePoint(value) {
205
+ return Number.isInteger(value) && value >= 0 && value <= 1114111 ? String.fromCodePoint(value) : "�";
206
+ }
207
+ function decodeEntities(value) {
208
+ return value.replace(/&#x([0-9a-f]{1,8});/gi, (_, hex) => codePoint(parseInt(hex, 16))).replace(/&#(\d{1,10});/g, (_, decimal) => codePoint(Number(decimal))).replaceAll("&quot;", "\"").replaceAll("&#039;", "'").replaceAll("&apos;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
209
+ }
210
+ /** Text safe inside an attribute value or element content. */
211
+ function escapeHtml(value) {
212
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#039;");
213
+ }
214
+ function attributesOf(source) {
215
+ const attributes = {};
216
+ for (const match of source.matchAll(ATTRIBUTE)) {
217
+ const name = match[1].toLowerCase();
218
+ if (Object.hasOwn(attributes, name)) continue;
219
+ attributes[name] = decodeEntities(match[2] ?? match[3] ?? match[4] ?? "");
220
+ }
221
+ return attributes;
222
+ }
223
+ /**
224
+ * What a reference contacts: the Site itself (relative, first-party host,
225
+ * about:blank), a third-party URL, or an opaque document (data:, javascript:,
226
+ * blob:…) whose own requests can't be known, treated as never loadable.
227
+ */
228
+ function classify(reference, firstPartyHosts) {
229
+ const value = reference.trim();
230
+ if (!value || /^about:blank$/i.test(value)) return { kind: "first" };
231
+ let url;
232
+ try {
233
+ url = new URL(value, "https://first-party.invalid/");
234
+ } catch {
235
+ return { kind: "opaque" };
236
+ }
237
+ if (url.protocol !== "https:" && url.protocol !== "http:") return { kind: "opaque" };
238
+ if (url.hostname === "first-party.invalid") return { kind: "first" };
239
+ if (firstPartyHosts.some((host) => hostMatches(url.hostname, host.toLowerCase()))) return { kind: "first" };
240
+ return {
241
+ kind: "third",
242
+ url
243
+ };
244
+ }
245
+ /**
246
+ * A placeholder for a third-party embed (CN-R13): no live `src`, naming the
247
+ * provider and purpose and offering "Load once" and "Always allow external
248
+ * media". The client runtime replaces it with the iframe after consent.
249
+ * Without JavaScript it never loads. Site components use it for their own
250
+ * embeds (a contact map); rewriteEmbeds uses it for WordPress HTML.
251
+ */
252
+ function embedPlaceholder(input) {
253
+ const service = SERVICE_CATALOG[input.service];
254
+ const copy = CONSENT_COPY[copyLanguage(input.language)];
255
+ const attrs = {
256
+ ...input.attributes,
257
+ src: input.src
258
+ };
259
+ if (input.title !== void 0) attrs.title = input.title;
260
+ if (input.width !== void 0) attrs.width = String(input.width);
261
+ if (input.height !== void 0) attrs.height = String(input.height);
262
+ const embed = escapeHtml(JSON.stringify({
263
+ tag: "iframe",
264
+ attrs
265
+ }));
266
+ const ratio = Number(attrs.width) > 0 && Number(attrs.height) > 0 ? ` style="aspect-ratio: ${Number(attrs.width)} / ${Number(attrs.height)}"` : "";
267
+ const data = `data-gq-consent-embed="${input.service}" data-gq-category="${service.category}" data-gq-embed="${embed}"`;
268
+ if (input.decorative) return `<div class="gq-consent-embed gq-consent-embed--decorative" ${data} data-gq-decorative aria-hidden="true"${ratio}></div>`;
269
+ return `<div class="gq-consent-embed" ${data} role="group" aria-label="${escapeHtml(attrs.title ? `${service.name}: ${attrs.title}` : service.name)}"${ratio}><div class="gq-consent-embed__notice"><p class="gq-consent-embed__text">${escapeHtml(copy.placeholderText(service.name, service.provider))}</p><p class="gq-consent-embed__purpose">${escapeHtml(service.purpose[copyLanguage(input.language)])}</p><div class="gq-consent-embed__actions"><button type="button" class="gq-consent-button" data-gq-consent-load-once>${escapeHtml(copy.loadOnce)}</button><button type="button" class="gq-consent-button" data-gq-consent-allow="${service.category}">${escapeHtml(copy.alwaysAllow)}</button></div></div></div>`;
270
+ }
271
+ /** The generic placeholder for an embed that may never load (CN-R3). */
272
+ function blockedPlaceholder(host, tag, language) {
273
+ const copy = CONSENT_COPY[copyLanguage(language)];
274
+ if (tag === "script") return `<span hidden data-gq-consent-blocked="${escapeHtml(host)}" data-gq-tag="script"></span>`;
275
+ return `<div class="gq-consent-blocked" data-gq-consent-blocked="${escapeHtml(host)}" data-gq-tag="${tag}" role="note"><p>${escapeHtml(copy.blockedText(host))}</p></div>`;
276
+ }
277
+ /**
278
+ * Rewrites published HTML so its initial form contacts no third party
279
+ * (CN-R1, CN-R3, CN-R13): iframes of active catalog services become
280
+ * placeholders; other third-party iframes, embeds and objects, and every
281
+ * third-party or inline executable script, are blocked and reported.
282
+ * First-party and non-executable content is left byte for byte.
283
+ */
284
+ function rewriteEmbeds(html, context) {
285
+ const firstParty = context.firstPartyHosts ?? [];
286
+ const placeholders = [];
287
+ const blocked = [];
288
+ const block = (tag, host, service, reason) => {
289
+ blocked.push({
290
+ tag,
291
+ host,
292
+ service,
293
+ reason
294
+ });
295
+ return blockedPlaceholder(host, tag, context.language);
296
+ };
297
+ return {
298
+ html: html.replace(RUNTIME_HOOKS, "data-gq-published-$1").replace(ELEMENT, (whole, pairedTag, pairedAttrs, scriptTag, scriptAttrs, scriptBody, voidTag, voidAttrs) => {
299
+ if (scriptTag) {
300
+ const attrs = attributesOf(scriptAttrs ?? "");
301
+ if (attrs.type && NON_EXECUTABLE.test(attrs.type.trim())) return whole;
302
+ if (attrs.src !== void 0) {
303
+ const target = classify(attrs.src, firstParty);
304
+ if (target.kind === "first") return whole;
305
+ return target.kind === "third" ? block("script", target.url.hostname, hostService(target.url.hostname), "script") : block("script", "inline", null, "script");
306
+ }
307
+ return scriptBody?.trim() ? block("script", "inline", null, "script") : whole;
308
+ }
309
+ const tag = (pairedTag ?? voidTag).toLowerCase();
310
+ const attrs = attributesOf((pairedAttrs ?? voidAttrs) || "");
311
+ const reference = tag === "object" ? attrs.data : attrs.src;
312
+ if (reference === void 0) {
313
+ if (tag === "iframe" && attrs.srcdoc === void 0) return whole;
314
+ return block(tag, "inline", null, "unknown-host");
315
+ }
316
+ const target = classify(reference, firstParty);
317
+ if (target.kind === "first") return whole;
318
+ if (target.kind === "opaque") return block(tag, "inline", null, "unknown-host");
319
+ const url = target.url;
320
+ const service = tag === "iframe" ? embedService(url) : null;
321
+ if (!service) return block(tag, url.hostname, hostService(url.hostname), "unknown-host");
322
+ if (!context.configuration.services.includes(service)) return block(tag, url.hostname, service, "inactive-service");
323
+ placeholders.push({
324
+ service,
325
+ host: url.hostname
326
+ });
327
+ const { src: _src, title, width, height, ...rest } = attrs;
328
+ return embedPlaceholder({
329
+ service,
330
+ src: url.href,
331
+ language: context.language,
332
+ ...title !== void 0 ? { title } : {},
333
+ ...width !== void 0 ? { width } : {},
334
+ ...height !== void 0 ? { height } : {},
335
+ attributes: rest,
336
+ decorative: /\bwp-block-getquick-design-video-hero__video\b/.test(attrs.class ?? "")
337
+ });
338
+ }),
339
+ placeholders,
340
+ blocked
341
+ };
342
+ }
343
+ //#endregion
344
+ export { CONSENT_COPY as a, legalRevisions as c, rewriteEmbeds as i, parseLegalLanguage as l, embedPlaceholder as n, LEGAL_ROLES as o, escapeHtml as r, REQUIRED_LEGAL_ROLES as s, blockedPlaceholder as t };