@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
package/README.md
CHANGED
|
@@ -1,3 +1,143 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @getquick/content
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The configured content runtime for WordPress HTML and WPGraphQL. This slice
|
|
4
|
+
provides pure helpers, the configured WordPress client, durable delivery and
|
|
5
|
+
installed SQL migrations, and event/refresh HTTP handlers. This is not a
|
|
6
|
+
completed or published 0.1.0 release pair.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { renderWordPressBlocks, htmlLang } from "@getquick/content";
|
|
10
|
+
import { createContentRuntime } from "@getquick/content/server";
|
|
11
|
+
|
|
12
|
+
const content = createContentRuntime({
|
|
13
|
+
site: "my Site identity",
|
|
14
|
+
graphqlEndpoint: "https://cms.example.test/wp/graphql",
|
|
15
|
+
cmsOrigin: "https://cms.example.test",
|
|
16
|
+
wordpress: {
|
|
17
|
+
locale: "pt_PT_ao90",
|
|
18
|
+
languages: [{ locale: "en_US", slug: "en" }],
|
|
19
|
+
},
|
|
20
|
+
isServer: () => true, // Supply the framework's lazy server-context check.
|
|
21
|
+
bindings: () => ({}), // Supply fresh request bindings, including optional credentials.
|
|
22
|
+
});
|
|
23
|
+
const home = await content.wordpress.getHomeContent(content.defaultLanguage);
|
|
24
|
+
const entry = await content.wordpress.getEntryByUri("/about/");
|
|
25
|
+
const chrome = await content.wordpress.getSiteChrome("https://www.example.test", {
|
|
26
|
+
language: content.routeLanguage("/en/about/"),
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The Site supplies all configuration: no manifest, framework environment or
|
|
31
|
+
Cloudflare discovery is imported. Construct per request where practical. A
|
|
32
|
+
long-lived instance calls its binding getter on every delivery operation and CMS request. Keep the
|
|
33
|
+
GraphQL read identity separate from automation identities. Partial credential
|
|
34
|
+
pairs fail before transport. With credentials, only the configured HTTPS CMS
|
|
35
|
+
origin at `/graphql` or `/wp/graphql`, without URL userinfo, is allowed. Reads use
|
|
36
|
+
manual redirects and an eight-second deadline; failed reads return unavailable,
|
|
37
|
+
confirmed absence returns missing, and successful reads return found.
|
|
38
|
+
|
|
39
|
+
The root entry contains pure helpers and public structural types. The server
|
|
40
|
+
entry exports `createContentRuntime`, `ContentConfig`, `ContentBindings`,
|
|
41
|
+
`ContentRuntime` and `WordPressClient`. Runtime properties contain languages,
|
|
42
|
+
routing functions and the WordPress methods; credentials/configuration are kept
|
|
43
|
+
in closures. Language configuration retains the source's normalization policy,
|
|
44
|
+
and routing returns the same language objects held in that instance's array.
|
|
45
|
+
|
|
46
|
+
The reusable client methods retain their argument order:
|
|
47
|
+
|
|
48
|
+
- `getHomeContent(language?)`
|
|
49
|
+
- `getEntryByUri(uri)`
|
|
50
|
+
- `getDesignPresets()`
|
|
51
|
+
- `getPublishedEntries()`
|
|
52
|
+
- `getPublishedRoutes()`
|
|
53
|
+
- `getSiteChrome(siteOrigin?, { location?, language? }?)`
|
|
54
|
+
|
|
55
|
+
From the repository root, use `vp install` and `vp run validate`. Validation
|
|
56
|
+
builds compiled ESM/declarations through Vite+ Pack, checks formatting/lint/types,
|
|
57
|
+
runs package-boundary suites, then packs and installs in a disposable consumer
|
|
58
|
+
without private registry configuration. `vp run test` rebuilds before tests;
|
|
59
|
+
`vp test frontend/tests/wordpress-access.test.ts` is useful after an explicit
|
|
60
|
+
`vp pack` during a focused implementation cycle. Build output is intentionally
|
|
61
|
+
untracked. The npm tarball allowlist contains only compiled output, the five original SQL migrations, the
|
|
62
|
+
separate consent database migrations and package metadata (npm also includes this README).
|
|
63
|
+
|
|
64
|
+
The public delivery methods are `publishedHome(siteOrigin?, language?)`,
|
|
65
|
+
`publishedEntry(path, siteOrigin?)` and `pageChrome(siteOrigin?)`. Supply a
|
|
66
|
+
`PUBLICATION_DB` structural binding to serve durable publications. Stored reads
|
|
67
|
+
need no event or refresh secret and retain last-known-good content without an
|
|
68
|
+
age limit. Cold entry lookups read the CMS once, preserving missing versus
|
|
69
|
+
unavailable, canonical routes, language isolation and conditional promotion.
|
|
70
|
+
Production without a store returns unavailable (chrome returns null). Only
|
|
71
|
+
`development: true` enables live reads without a store. A store failure never
|
|
72
|
+
activates that fallback. All public reads guard server context before bindings
|
|
73
|
+
or transport.
|
|
74
|
+
|
|
75
|
+
Infrastructure imports the Node-only migration entry separately:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { migrationsDirectory } from "@getquick/content/migrations";
|
|
79
|
+
// Alchemy beta.79: Cloudflare.D1.Database("Publications", {
|
|
80
|
+
// migrations: migrationsDirectory,
|
|
81
|
+
// });
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The exported string resolves to the five SQL files inside the installed package.
|
|
85
|
+
Keep this infrastructure entry out of browser and Worker imports. The root/server
|
|
86
|
+
graphs contain no Node migration module. `vp run alchemy` packs/installs with pnpm
|
|
87
|
+
and exercises the pinned Alchemy local D1/Worker lifecycle, including an update
|
|
88
|
+
with unchanged migration identities and publication data. This proof uses local
|
|
89
|
+
workerd, no remote resources or credentials. See the repository's issue #4 evidence.
|
|
90
|
+
|
|
91
|
+
The HTTP methods are `handleEvent(request, { siteOrigin? }?)` and
|
|
92
|
+
`handleRefresh(request, { siteOrigin? }?)`, both returning standard `Response`s.
|
|
93
|
+
Sites wire their POST routes directly to these methods; other methods return
|
|
94
|
+
405 with `Allow: POST`. Responses use `Cache-Control: no-store`.
|
|
95
|
+
|
|
96
|
+
Events require `PUBLICATION_EVENT_SECRET` and refresh requires
|
|
97
|
+
`FRONTEND_REFRESH_TOKEN`, each at least 32 characters, plus `PUBLICATION_DB`.
|
|
98
|
+
An absent or short credential disables its handler without disabling stored
|
|
99
|
+
public reads. Event HMAC verification and Site/schema checks precede any CMS
|
|
100
|
+
reads or store changes. Refresh uses bearer authorization; `{}` refreshes the
|
|
101
|
+
whole Site and `{"uris":["/about/"]}` targets 1–100 entry paths. Language homes
|
|
102
|
+
require a whole-Site refresh. A signed withdrawal hides every stored route of
|
|
103
|
+
that entry immediately, without reading the CMS. Refreshes cannot lift it;
|
|
104
|
+
only a later publication can.
|
|
105
|
+
|
|
106
|
+
Signed `reconcile` events compare canonical shared rows and the complete
|
|
107
|
+
published-entry list, repair differences through normal publication rules, and
|
|
108
|
+
rotate rereads of unchanged entries. Runs hold a D1 lease, reserve a bounded CMS
|
|
109
|
+
request budget, and persist outcomes and pending counts for continuation.
|
|
110
|
+
Responses retain the source protocol: 200 for `reconciled`, `behind` or `busy`;
|
|
111
|
+
503 for `failed`, preserving last-known-good content. Incomplete lists remove
|
|
112
|
+
nothing. Every configured language has separate home/chrome rows; design is
|
|
113
|
+
shared.
|
|
114
|
+
|
|
115
|
+
Signed `check` events return `{ status: "checked", site, reconciliation, store }`.
|
|
116
|
+
`store` reports shared row states, entry-state counts, active withdrawals and
|
|
117
|
+
failed events, plus other language row states under `languages`. It is null
|
|
118
|
+
when unavailable. `reconciliation` reports the last run, counts, failure and
|
|
119
|
+
last matched time, or null before any run. These are runtime evidence for the
|
|
120
|
+
Blueprint readiness gate; readiness orchestration, media checks and provisioning
|
|
121
|
+
remain Blueprint-owned. No storage or reconciliation helpers are public exports.
|
|
122
|
+
|
|
123
|
+
`vp run pair` exercises installed WordPress/package transport; `vp run alchemy`
|
|
124
|
+
retains the local installed-package/workerd migration and protocol proof.
|
|
125
|
+
`vp run reconciliation` additionally installs the pair and runs actual
|
|
126
|
+
WordPress/Polylang CLI and scheduler HTTP into the installed workerd consumer,
|
|
127
|
+
using GraphQL answers projected from real CMS state. It verifies convergence,
|
|
128
|
+
partial runs, outages/recovery and complete pending/failed records across Worker
|
|
129
|
+
updates. Full P6 release evidence remains issue #11.
|
|
130
|
+
|
|
131
|
+
## Consent and legal links
|
|
132
|
+
|
|
133
|
+
CONSENT-1/LEGAL-1 ship as isolated entries: `@getquick/content/consent`
|
|
134
|
+
(neutral core), `/consent/client` (browser runtime), `/consent/server` (the
|
|
135
|
+
`/gq/consent` record and read endpoints and purge), `/consent/migrations`
|
|
136
|
+
(the separate consent database), `/consent/astro` (default UI and legal
|
|
137
|
+
footer as HTML) and `/consent/testing` (acceptance suite). They import no
|
|
138
|
+
content runtime, framework or WordPress client. Passing
|
|
139
|
+
`consent: { services: ["youtube"], cmsSettings: true }` to
|
|
140
|
+
`createContentRuntime` rewrites third-party embeds into consent placeholders
|
|
141
|
+
at delivery and stores gq-config's Legal Contract and GA4 settings in each
|
|
142
|
+
language's chrome; `runtime.consentConfiguration(chrome)` returns the page's
|
|
143
|
+
consent configuration. See the repository's `docs/consent.md`.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-- The consent database (CONSENT-1 CN-R7/R8): a dedicated D1 database per Site
|
|
2
|
+
-- and stage, created with jurisdiction "eu", separate from the publication
|
|
3
|
+
-- database. One row per decision; rows are never updated. Rows are deleted
|
|
4
|
+
-- only by the retention purge (CN-R10). No IP address, user agent or account
|
|
5
|
+
-- link is stored.
|
|
6
|
+
CREATE TABLE consent_records (
|
|
7
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
8
|
+
consent_id TEXT NOT NULL,
|
|
9
|
+
site_id TEXT NOT NULL,
|
|
10
|
+
-- Server UTC time, ISO 8601 with milliseconds.
|
|
11
|
+
recorded_at TEXT NOT NULL,
|
|
12
|
+
-- The browser's decision time: with consent_id, a retried post's identity.
|
|
13
|
+
decided_at TEXT NOT NULL,
|
|
14
|
+
-- When the decision expires if nothing supersedes it (6 months).
|
|
15
|
+
expires_at TEXT NOT NULL,
|
|
16
|
+
config_version TEXT NOT NULL,
|
|
17
|
+
legal_revisions TEXT NOT NULL,
|
|
18
|
+
preferences INTEGER NOT NULL CHECK (preferences IN (0, 1)),
|
|
19
|
+
analytics INTEGER NOT NULL CHECK (analytics IN (0, 1)),
|
|
20
|
+
marketing INTEGER NOT NULL CHECK (marketing IN (0, 1)),
|
|
21
|
+
external_media INTEGER NOT NULL CHECK (external_media IN (0, 1)),
|
|
22
|
+
action TEXT NOT NULL CHECK (action IN ('accept_all', 'reject_all', 'custom', 'withdraw', 'renew')),
|
|
23
|
+
language TEXT NOT NULL,
|
|
24
|
+
ui_version TEXT NOT NULL,
|
|
25
|
+
UNIQUE (consent_id, decided_at)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE INDEX consent_records_by_time ON consent_records (recorded_at);
|
|
29
|
+
|
|
30
|
+
CREATE TRIGGER consent_records_append_only
|
|
31
|
+
BEFORE UPDATE ON consent_records
|
|
32
|
+
BEGIN
|
|
33
|
+
SELECT RAISE(ABORT, 'consent records are append-only');
|
|
34
|
+
END;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region frontend/src/bearer.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether a request carries `Authorization: Bearer <token>`. Both sides are
|
|
4
|
+
* hashed first, so the comparison's time depends on neither value nor length.
|
|
5
|
+
* Shared by the refresh and consent read endpoints; dependency-free.
|
|
6
|
+
*/
|
|
7
|
+
async function bearerMatches(request, token) {
|
|
8
|
+
const presented = /^Bearer (.+)$/.exec(request.headers.get("Authorization") ?? "")?.[1] ?? "";
|
|
9
|
+
const digest = async (value) => new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)));
|
|
10
|
+
const [expected, actual] = await Promise.all([digest(token), digest(presented)]);
|
|
11
|
+
let difference = presented ? 0 : 1;
|
|
12
|
+
for (let index = 0; index < expected.length; index += 1) difference |= expected[index] ^ actual[index];
|
|
13
|
+
return difference === 0;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { bearerMatches as t };
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
//#region frontend/src/consent/catalog.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* CONSENT-1's fixed purpose categories (CN-R2) and the Runtime's closed service
|
|
4
|
+
* catalog (CN-R3). Sites choose services from here; they cannot add categories
|
|
5
|
+
* or services. Adding a service is a Runtime release.
|
|
6
|
+
*/
|
|
7
|
+
declare const CONSENT_CATEGORIES: readonly ["necessary", "preferences", "analytics", "marketing", "external_media"];
|
|
8
|
+
type ConsentCategory = (typeof CONSENT_CATEGORIES)[number];
|
|
9
|
+
/** The categories a visitor decides on: every one but `necessary`, which is never a choice. */
|
|
10
|
+
declare const OPTIONAL_CATEGORIES: readonly ["preferences", "analytics", "marketing", "external_media"];
|
|
11
|
+
type OptionalCategory = (typeof OPTIONAL_CATEGORIES)[number];
|
|
12
|
+
/** The languages Runtime-owned copy ships in (CN-C3). */
|
|
13
|
+
declare const COPY_LANGUAGES: readonly ["en", "pt-PT"];
|
|
14
|
+
type CopyLanguage = (typeof COPY_LANGUAGES)[number];
|
|
15
|
+
type Copy = Record<CopyLanguage, string>;
|
|
16
|
+
/** The copy language for a page's `lang` (BCP 47): Portuguese pages get pt-PT, others en. */
|
|
17
|
+
declare function copyLanguage(lang: string | undefined): CopyLanguage;
|
|
18
|
+
interface ConsentStorage {
|
|
19
|
+
name: string;
|
|
20
|
+
kind: "cookie" | "localStorage" | "sessionStorage";
|
|
21
|
+
/** `prefix`: every item whose name starts with `name`. */
|
|
22
|
+
match?: "exact" | "prefix";
|
|
23
|
+
/** First-party storage the Runtime can remove on withdrawal; third-party it cannot. */
|
|
24
|
+
party: "first" | "third";
|
|
25
|
+
duration: Copy;
|
|
26
|
+
}
|
|
27
|
+
interface ConsentService {
|
|
28
|
+
id: string;
|
|
29
|
+
category: ConsentCategory;
|
|
30
|
+
/** The provider's legal name. */
|
|
31
|
+
provider: string;
|
|
32
|
+
name: string;
|
|
33
|
+
purpose: Copy;
|
|
34
|
+
storage: ConsentStorage[];
|
|
35
|
+
/** How long the provider keeps the data it collects. */
|
|
36
|
+
retention: Copy;
|
|
37
|
+
privacyPolicy: string;
|
|
38
|
+
/** Domains the service contacts; a host matches a domain or any subdomain of it. */
|
|
39
|
+
hosts: string[];
|
|
40
|
+
/** The embed URLs that identify this service in HTML (host, optional path prefix). */
|
|
41
|
+
embeds?: Array<{
|
|
42
|
+
host: string;
|
|
43
|
+
path?: string;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
declare const SERVICE_CATALOG: {
|
|
47
|
+
readonly consent: {
|
|
48
|
+
readonly id: "consent";
|
|
49
|
+
readonly category: "necessary";
|
|
50
|
+
readonly provider: "This website";
|
|
51
|
+
readonly name: "Consent choices";
|
|
52
|
+
readonly purpose: {
|
|
53
|
+
readonly en: "Remembers your consent choices and the consent ID that identifies them.";
|
|
54
|
+
readonly "pt-PT": "Memoriza as suas escolhas de consentimento e o ID que as identifica.";
|
|
55
|
+
};
|
|
56
|
+
readonly storage: [{
|
|
57
|
+
readonly name: "gq_consent";
|
|
58
|
+
readonly kind: "cookie";
|
|
59
|
+
readonly party: "first";
|
|
60
|
+
readonly duration: Copy;
|
|
61
|
+
}];
|
|
62
|
+
readonly retention: {
|
|
63
|
+
readonly en: "Records: 5 years after the choice stops being in force.";
|
|
64
|
+
readonly "pt-PT": "Registos: 5 anos após a escolha deixar de estar em vigor.";
|
|
65
|
+
};
|
|
66
|
+
readonly privacyPolicy: "";
|
|
67
|
+
readonly hosts: [];
|
|
68
|
+
};
|
|
69
|
+
readonly turnstile: {
|
|
70
|
+
readonly id: "turnstile";
|
|
71
|
+
readonly category: "necessary";
|
|
72
|
+
readonly provider: "Cloudflare, Inc.";
|
|
73
|
+
readonly name: "Cloudflare Turnstile";
|
|
74
|
+
readonly purpose: {
|
|
75
|
+
readonly en: "Protects sign-in, registration and checkout against abuse, only on those actions.";
|
|
76
|
+
readonly "pt-PT": "Protege o início de sessão, o registo e a finalização de compra contra abusos, apenas nessas ações.";
|
|
77
|
+
};
|
|
78
|
+
readonly storage: [];
|
|
79
|
+
readonly retention: {
|
|
80
|
+
readonly en: "Not stored by this website.";
|
|
81
|
+
readonly "pt-PT": "Não guardado por este website.";
|
|
82
|
+
};
|
|
83
|
+
readonly privacyPolicy: "https://www.cloudflare.com/privacypolicy/";
|
|
84
|
+
readonly hosts: ["challenges.cloudflare.com"];
|
|
85
|
+
};
|
|
86
|
+
readonly ga4: {
|
|
87
|
+
readonly id: "ga4";
|
|
88
|
+
readonly category: "analytics";
|
|
89
|
+
readonly provider: "Google Ireland Limited";
|
|
90
|
+
readonly name: "Google Analytics 4";
|
|
91
|
+
readonly purpose: {
|
|
92
|
+
readonly en: "Measures how visitors use the website, in aggregate (Consent Mode v2, basic mode).";
|
|
93
|
+
readonly "pt-PT": "Mede, de forma agregada, como os visitantes usam o website (Consent Mode v2, modo básico).";
|
|
94
|
+
};
|
|
95
|
+
readonly storage: [{
|
|
96
|
+
readonly name: "_ga";
|
|
97
|
+
readonly kind: "cookie";
|
|
98
|
+
readonly match: "prefix";
|
|
99
|
+
readonly party: "first";
|
|
100
|
+
readonly duration: {
|
|
101
|
+
readonly en: "Up to 2 years";
|
|
102
|
+
readonly "pt-PT": "Até 2 anos";
|
|
103
|
+
};
|
|
104
|
+
}];
|
|
105
|
+
readonly retention: {
|
|
106
|
+
readonly en: "14 months at Google (default data retention).";
|
|
107
|
+
readonly "pt-PT": "14 meses na Google (retenção de dados predefinida).";
|
|
108
|
+
};
|
|
109
|
+
readonly privacyPolicy: "https://policies.google.com/privacy";
|
|
110
|
+
readonly hosts: ["googletagmanager.com", "google-analytics.com", "analytics.google.com"];
|
|
111
|
+
};
|
|
112
|
+
readonly youtube: {
|
|
113
|
+
readonly id: "youtube";
|
|
114
|
+
readonly category: "external_media";
|
|
115
|
+
readonly provider: "Google Ireland Limited";
|
|
116
|
+
readonly name: "YouTube";
|
|
117
|
+
readonly purpose: {
|
|
118
|
+
readonly en: "Plays embedded YouTube videos. Loading one sends your IP address to YouTube.";
|
|
119
|
+
readonly "pt-PT": "Reproduz vídeos do YouTube incorporados. Carregar um envia o seu endereço IP ao YouTube.";
|
|
120
|
+
};
|
|
121
|
+
readonly storage: [{
|
|
122
|
+
readonly name: "VISITOR_INFO1_LIVE, YSC";
|
|
123
|
+
readonly kind: "cookie";
|
|
124
|
+
readonly party: "third";
|
|
125
|
+
readonly duration: {
|
|
126
|
+
readonly en: "Session to 6 months";
|
|
127
|
+
readonly "pt-PT": "Sessão a 6 meses";
|
|
128
|
+
};
|
|
129
|
+
}];
|
|
130
|
+
readonly retention: {
|
|
131
|
+
readonly en: "Set by Google; see its privacy policy.";
|
|
132
|
+
readonly "pt-PT": "Definida pela Google; consulte a sua política de privacidade.";
|
|
133
|
+
};
|
|
134
|
+
readonly privacyPolicy: "https://policies.google.com/privacy";
|
|
135
|
+
readonly hosts: ["youtube.com", "youtube-nocookie.com", "ytimg.com", "googlevideo.com", "ggpht.com"];
|
|
136
|
+
readonly embeds: [{
|
|
137
|
+
readonly host: "youtube.com";
|
|
138
|
+
}, {
|
|
139
|
+
readonly host: "youtube-nocookie.com";
|
|
140
|
+
}];
|
|
141
|
+
};
|
|
142
|
+
readonly vimeo: {
|
|
143
|
+
readonly id: "vimeo";
|
|
144
|
+
readonly category: "external_media";
|
|
145
|
+
readonly provider: "Vimeo.com, Inc.";
|
|
146
|
+
readonly name: "Vimeo";
|
|
147
|
+
readonly purpose: {
|
|
148
|
+
readonly en: "Plays embedded Vimeo videos. Loading one sends your IP address to Vimeo.";
|
|
149
|
+
readonly "pt-PT": "Reproduz vídeos do Vimeo incorporados. Carregar um envia o seu endereço IP ao Vimeo.";
|
|
150
|
+
};
|
|
151
|
+
readonly storage: [{
|
|
152
|
+
readonly name: "vuid";
|
|
153
|
+
readonly kind: "cookie";
|
|
154
|
+
readonly party: "third";
|
|
155
|
+
readonly duration: {
|
|
156
|
+
readonly en: "2 years";
|
|
157
|
+
readonly "pt-PT": "2 anos";
|
|
158
|
+
};
|
|
159
|
+
}];
|
|
160
|
+
readonly retention: {
|
|
161
|
+
readonly en: "Set by Vimeo; see its privacy policy.";
|
|
162
|
+
readonly "pt-PT": "Definida pela Vimeo; consulte a sua política de privacidade.";
|
|
163
|
+
};
|
|
164
|
+
readonly privacyPolicy: "https://vimeo.com/privacy";
|
|
165
|
+
readonly hosts: ["vimeo.com", "vimeocdn.com"];
|
|
166
|
+
readonly embeds: [{
|
|
167
|
+
readonly host: "player.vimeo.com";
|
|
168
|
+
}];
|
|
169
|
+
};
|
|
170
|
+
readonly google_maps: {
|
|
171
|
+
readonly id: "google_maps";
|
|
172
|
+
readonly category: "external_media";
|
|
173
|
+
readonly provider: "Google Ireland Limited";
|
|
174
|
+
readonly name: "Google Maps";
|
|
175
|
+
readonly purpose: {
|
|
176
|
+
readonly en: "Shows an embedded map. Loading it sends your IP address to Google.";
|
|
177
|
+
readonly "pt-PT": "Mostra um mapa incorporado. Carregá-lo envia o seu endereço IP à Google.";
|
|
178
|
+
};
|
|
179
|
+
readonly storage: [{
|
|
180
|
+
readonly name: "NID";
|
|
181
|
+
readonly kind: "cookie";
|
|
182
|
+
readonly party: "third";
|
|
183
|
+
readonly duration: {
|
|
184
|
+
readonly en: "6 months";
|
|
185
|
+
readonly "pt-PT": "6 meses";
|
|
186
|
+
};
|
|
187
|
+
}];
|
|
188
|
+
readonly retention: {
|
|
189
|
+
readonly en: "Set by Google; see its privacy policy.";
|
|
190
|
+
readonly "pt-PT": "Definida pela Google; consulte a sua política de privacidade.";
|
|
191
|
+
};
|
|
192
|
+
readonly privacyPolicy: "https://policies.google.com/privacy";
|
|
193
|
+
readonly hosts: ["google.com", "googleapis.com", "gstatic.com"];
|
|
194
|
+
readonly embeds: [{
|
|
195
|
+
readonly host: "www.google.com";
|
|
196
|
+
readonly path: "/maps";
|
|
197
|
+
}, {
|
|
198
|
+
readonly host: "google.com";
|
|
199
|
+
readonly path: "/maps";
|
|
200
|
+
}, {
|
|
201
|
+
readonly host: "maps.google.com";
|
|
202
|
+
}];
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
type ServiceId = keyof typeof SERVICE_CATALOG;
|
|
206
|
+
declare const SERVICE_IDS: ServiceId[];
|
|
207
|
+
declare function isServiceId(value: unknown): value is ServiceId;
|
|
208
|
+
declare function catalogService(id: ServiceId): ConsentService;
|
|
209
|
+
/** Whether a host is a domain or one of its subdomains. */
|
|
210
|
+
declare function hostMatches(host: string, domain: string): boolean;
|
|
211
|
+
/** The catalog service an embed URL belongs to, if any. */
|
|
212
|
+
declare function embedService(url: URL): ServiceId | null;
|
|
213
|
+
/** The catalog service whose hosts include a host, if any. */
|
|
214
|
+
declare function hostService(host: string): ServiceId | null;
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region frontend/src/consent/configuration.d.ts
|
|
217
|
+
/**
|
|
218
|
+
* The revision of the Runtime's banner copy that counts as material. Copy edits
|
|
219
|
+
* keep it (CN-R5); only a reviewed change of what the visitor is told about
|
|
220
|
+
* processing bumps it.
|
|
221
|
+
*/
|
|
222
|
+
declare const CONSENT_COPY_REVISION = 1;
|
|
223
|
+
/** gq-config's GA4 settings: the toggle and the measurement ID (GraphQL `googleAnalytics`). */
|
|
224
|
+
interface AnalyticsSettings {
|
|
225
|
+
enabled: boolean;
|
|
226
|
+
measurementId: string | null;
|
|
227
|
+
}
|
|
228
|
+
/** A GA4 measurement ID. Anything else leaves GA4 inactive. */
|
|
229
|
+
declare const MEASUREMENT_ID: RegExp;
|
|
230
|
+
interface ConsentConfigurationInput {
|
|
231
|
+
/** The catalog services the Site declares in code. */
|
|
232
|
+
services?: readonly string[];
|
|
233
|
+
/** gq-config's analytics settings: GA4 is active only when its toggle is on (ticket 08). */
|
|
234
|
+
analytics?: AnalyticsSettings | null;
|
|
235
|
+
/** The data controller's legal name (LEGAL-1); changing it is material. */
|
|
236
|
+
controller?: string | null;
|
|
237
|
+
}
|
|
238
|
+
interface ConsentConfiguration {
|
|
239
|
+
schemaVersion: 1;
|
|
240
|
+
/** Every active service, necessary ones included, sorted. */
|
|
241
|
+
services: ServiceId[];
|
|
242
|
+
/** The optional categories at least one active service is in, in category order. */
|
|
243
|
+
categories: OptionalCategory[];
|
|
244
|
+
/** GA4's measurement ID, when GA4 is active. */
|
|
245
|
+
measurementId: string | null;
|
|
246
|
+
/** A short hash of the controller's name, empty when unknown; a change between known ones is material. */
|
|
247
|
+
controller: string;
|
|
248
|
+
/** The consent configuration version: categories, services, controller and copy revision. */
|
|
249
|
+
version: string;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* A Site's consent configuration: the union of services it declares in code and
|
|
253
|
+
* those its settings switch on. Unknown services are refused: the set is closed.
|
|
254
|
+
*/
|
|
255
|
+
declare function consentConfiguration(input?: ConsentConfigurationInput): ConsentConfiguration;
|
|
256
|
+
/** CN-C1: a Site with no active service outside `necessary` shows no banner and records nothing. */
|
|
257
|
+
declare function needsBanner(configuration: ConsentConfiguration): boolean;
|
|
258
|
+
//#endregion
|
|
259
|
+
export { hostMatches as C, embedService as S, isServiceId as T, SERVICE_CATALOG as _, MEASUREMENT_ID as a, catalogService as b, CONSENT_CATEGORIES as c, ConsentService as d, ConsentStorage as f, OptionalCategory as g, OPTIONAL_CATEGORIES as h, ConsentConfigurationInput as i, COPY_LANGUAGES as l, CopyLanguage as m, CONSENT_COPY_REVISION as n, consentConfiguration as o, Copy as p, ConsentConfiguration as r, needsBanner as s, AnalyticsSettings as t, ConsentCategory as u, SERVICE_IDS as v, hostService as w, copyLanguage as x, ServiceId as y };
|
|
@@ -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/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
|
|
@@ -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-p9SyGNZI.js";
|
|
3
|
+
import { t as CONSENT_UI_VERSION } from "./version-Dn6VLZJO.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
|
|
94
|
+
export { CONSENT_STYLES, consentPage, legalFooter };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { r as ConsentConfiguration } from "./configuration-C_JGygQI.js";
|
|
2
|
+
import { a as ConsentChoice, i as ConsentAction, s as ConsentState } from "./state-Dzd52F3c.js";
|
|
3
|
+
//#region frontend/src/consent/client-config.d.ts
|
|
4
|
+
/** The JSON the server renders into `<script type="application/json" id="gq-consent-config">`. */
|
|
5
|
+
interface ConsentPageConfig {
|
|
6
|
+
configuration: ConsentConfiguration;
|
|
7
|
+
/** The page's `lang`, recorded as the banner language. */
|
|
8
|
+
language: string;
|
|
9
|
+
uiVersion: string;
|
|
10
|
+
legalRevisions: Record<string, string>;
|
|
11
|
+
endpoint: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const CONSENT_CONFIG_ID = "gq-consent-config";
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region frontend/src/consent/client.d.ts
|
|
16
|
+
export interface ConsentController {
|
|
17
|
+
readonly state: ConsentState | null;
|
|
18
|
+
readonly config: ConsentPageConfig;
|
|
19
|
+
choose(choice: ConsentChoice): void;
|
|
20
|
+
openPreferences(opener?: HTMLElement | null): void;
|
|
21
|
+
/** Re-applies grants to the current document (after a client-side navigation). */
|
|
22
|
+
refresh(): void;
|
|
23
|
+
}
|
|
24
|
+
declare global {
|
|
25
|
+
interface Window {
|
|
26
|
+
dataLayer?: unknown[];
|
|
27
|
+
gtag?: (...args: unknown[]) => void;
|
|
28
|
+
__gqConsent?: ConsentController;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Starts the runtime once per document. Safe to call again: the existing
|
|
33
|
+
* controller refreshes the page instead.
|
|
34
|
+
*/
|
|
35
|
+
export declare function startConsent(): ConsentController | null;
|
|
36
|
+
//#endregion
|
|
37
|
+
export type { ConsentAction, ConsentChoice, ConsentPageConfig, ConsentState };
|