@ecomconsult/consentkit 0.4.0 → 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 CHANGED
@@ -179,12 +179,16 @@ Pass any subset to `init()`. Nested objects merge with the defaults.
179
179
  |---|---|---|---|
180
180
  | `policyVersion` | `string \| number` | `"1"` | Bump to invalidate stored consent and re-show the banner |
181
181
  | `language` | `string` | `"auto"` | `"auto"` reads `navigator.language`. Falls back `pt-BR` → `pt` → `en` |
182
- | `layout.type` | `"bar" \| "modal" \| "box"` | `"bar"` | `box` is a compact ~360px card |
182
+ | `layout.type` | `"bar" \| "modal" \| "box"` | `"bar"` | `box` is a corner card, up to 540px wide |
183
183
  | `layout.position` | `string` | per type | `bar`: `bottom` (default) / `top`. `box`: `bottom-left` (default) / `bottom-right`. `modal` is always centred. A position that does not belong to the chosen type falls back to that type's default; the type itself is unaffected |
184
184
  | `theme.accent` | `string` | `"#2B50D8"` | Exposed as `--ck-accent` |
185
- | `theme.radius` | `string` | `"10px"` | Exposed as `--ck-radius` |
185
+ | `theme.font` | `"inherit" \| "system"` | `"inherit"` | v0.5.0. `inherit` takes the host page's font family; `system` restores the pre-0.5.0 system stack. Font *sizes* are fixed either way |
186
+ | `theme.radius` | `{ card, button }` | `{ card: 16, button: 8 }` | v0.5.0. px, clamped 0–32. A bare string or number is the pre-0.5.0 form and still sets the card radius |
187
+ | `theme.buttons` | `{ accept, reject, settings }` | see below | v0.5.0. Per-button appearance. **Contrast is enforced automatically** — see [Button appearance](#button-appearance) |
186
188
  | `theme.mode` | `"auto" \| "light" \| "dark"` | `"auto"` | `auto` follows `prefers-color-scheme` |
187
- | `theme.dark` | `{ bg, ink, accent }` | built-in | Overrides the dark palette |
189
+ | `theme.dark` | `{ bg, ink, accent, onAccent }` | built-in | Overrides the dark palette |
190
+ | `texts.policyUrl` | `string` | — | v0.5.0. Cookie policy address. `http(s)` only; anything else is ignored |
191
+ | `texts.detailsAction` | `"policy" \| "settings" \| "hide"` | see notes | v0.5.0. What «Learn more» does. Defaults to `policy` when `policyUrl` is set, `settings` when it is not. `policy` without a usable URL falls back to `settings` rather than rendering a dead link |
188
192
  | `categories.*.enabled` | `boolean` | `true` | Per category: `functional`, `analytics`, `marketing`. Hides the toggle when `false` |
189
193
  | `consentTtlDays` | `number` | `365` | Lifetime of the stored decision |
190
194
  | `integrations.gcm` | `boolean` | `true` | Google Consent Mode v2 signals |
@@ -200,6 +204,98 @@ Pass any subset to `init()`. Nested objects merge with the defaults.
200
204
  { name: '_ga', category: 'analytics', vendor: 'Google', purpose: 'Visit statistics', expiry: '2 years' }
201
205
  ```
202
206
 
207
+ ### Button appearance
208
+
209
+ Each of the three banner buttons can be styled independently:
210
+
211
+ ```js
212
+ theme: {
213
+ accent: '#2B50D8',
214
+ font: 'inherit',
215
+ radius: { card: 16, button: 8 },
216
+ buttons: {
217
+ accept: { variant: 'filled' }, // accent fill
218
+ reject: { variant: 'filled' }, // always matches accept
219
+ settings: { variant: 'outline', borderWidth: 1 } // accent border
220
+ }
221
+ }
222
+ ```
223
+
224
+ Each entry takes `variant` (`"filled"` or `"outline"`), and optionally `bg`,
225
+ `fg`, `border` and `borderWidth` (`1` or `2`). Omitted colours come from
226
+ `theme.accent`.
227
+
228
+ **Accept and reject are always equal.** They render at the same size and weight,
229
+ and they always share one variant — if the two disagree in the config,
230
+ `accept.variant` is used for both. A reject button that looks weaker than
231
+ accept is a dark pattern, and consent collected through one is not freely
232
+ given, so the config simply cannot express it. `settings` is independent and
233
+ may itself be filled.
234
+
235
+ **Contrast is enforced automatically and cannot be switched off.** A colour
236
+ combination that would be unreadable is corrected before it reaches the
237
+ stylesheet:
238
+
239
+ - button text keeps the colour you set only when it clears **4.5:1** against
240
+ the fill it sits on; otherwise it becomes white or `#161616`, whichever
241
+ contrasts more;
242
+ - an `outline` border is darkened (light card) or lightened (dark card) in
243
+ small steps until it clears **3:1** against the card, so it stays
244
+ recognisably your colour rather than jumping to black or white;
245
+ - that resolved border colour is then the outline button's text colour,
246
+ subject to the same 4.5:1 rule.
247
+
248
+ The card is `#ffffff` in light mode and `#1c1c1e` in dark. A colour the
249
+ arithmetic cannot read — a CSS colour name, an `rgb()` string — is left exactly
250
+ as you wrote it rather than being silently replaced.
251
+
252
+ The same arithmetic is exposed as pure functions on `ConsentKit._contrast`
253
+ (`relativeLuminance`, `contrastRatio`, `ensureContrast`, `stepToContrast`,
254
+ `resolveButtonStyles`, `resolveRadius`, `resolveFont`, `resolveDetails`,
255
+ `buildThemeCss`) so a theme editor can show the same numbers the banner paints
256
+ instead of reimplementing them. It is present whenever `src/ck-ui.js` is loaded,
257
+ and it is safe to call in Node — nothing in it touches the DOM. The debug
258
+ panel's **Appearance** section reads it directly and reports each button's
259
+ resolved colours, its contrast ratio, and whether the value was adjusted.
260
+
261
+ ### The «Learn more» link
262
+
263
+ `texts.detailsAction` decides what the link at the end of the banner copy does:
264
+
265
+ | Value | Renders |
266
+ |---|---|
267
+ | `"policy"` | A link to `texts.policyUrl`, opened with `target="_blank" rel="noopener"` |
268
+ | `"settings"` | A button that opens the preferences panel |
269
+ | `"hide"` | Nothing at all |
270
+
271
+ The default follows `policyUrl`: `policy` when one is set, `settings` when it
272
+ is not — so supplying only a URL does the obvious thing.
273
+
274
+ > Before 0.5.0 this control was rendered as `<a href="#">` with no handler at
275
+ > all: clicking it jumped to the top of the page and nothing else. Any site
276
+ > running 0.4.x or earlier has a dead «Learn more» link.
277
+
278
+ ### Infrastructure
279
+
280
+ Some third-party hosts are not a consent decision at all: they are where a site
281
+ builder or hosting platform serves the site's **own** markup, styles and
282
+ scripts from. A Tilda page loads its layout from `tildacdn.com`, a Wix page
283
+ loads its from `parastorage.com`, and a page using Google Fonts loads its
284
+ typefaces from `fonts.gstatic.com`. ConsentKit ships these as a separate class
285
+ of host — readable as `ConsentKit._infra()`, tested per URL or hostname with
286
+ `ConsentKit._isInfra(url)` — covering the CDNs of Tilda, Wix, Shopify,
287
+ Squarespace and Webflow, the general asset CDNs (`cdn.jsdelivr.net`,
288
+ `unpkg.com`, `cdnjs.cloudflare.com`, `code.jquery.com`, `ajax.googleapis.com`),
289
+ Google Fonts and hCaptcha. Strict mode never intercepts them, because blocking a
290
+ builder's own CDN breaks the page without protecting anyone; the hosted service
291
+ also leaves them out of scan reports, since there is nothing for a site owner to
292
+ decide. Membership is a claim that a host delivers the site's own assets, not
293
+ that it is harmless in general — anything that *measures* keeps a real consent
294
+ category instead, which is why `static.cloudflareinsights.com` (Cloudflare Web
295
+ Analytics) is classified as `analytics` and blocked before consent even though
296
+ the rest of Cloudflare's CDN is infrastructure. Both lists are matched by suffix
297
+ and returned as copies, so reading them cannot widen what strict mode allows.
298
+
203
299
  ## API
204
300
 
205
301
  All methods are safe to call at any time and never throw.
@@ -360,11 +456,10 @@ Four things are never intercepted:
360
456
  the answer is unclear it says same-site, because wrongly blocking a
361
457
  first-party asset breaks the site.
362
458
  2. **`blocking.allow`** — your own list, matched by suffix.
363
- 3. **The built-in allowlist**, readable as `ConsentKit._baseAllow`: asset CDNs
364
- (`cdn.jsdelivr.net`, `unpkg.com`, `cdnjs.cloudflare.com`, `code.jquery.com`,
365
- `fonts.googleapis.com`, `fonts.gstatic.com`) and things a page is unusable
366
- without (`js.stripe.com`, `pay.google.com`, `checkout.creem.io`,
367
- `hcaptcha.com`, and reCAPTCHA — scoped to `www.google.com/recaptcha` and
459
+ 3. **The built-in allowlist**, readable as `ConsentKit._baseAllow`. Two parts:
460
+ **infrastructure** (`ConsentKit._infra()`, see below) and things a page is
461
+ unusable without (`js.stripe.com`, `pay.google.com`, `checkout.creem.io`,
462
+ and reCAPTCHA — scoped to `www.google.com/recaptcha` and
368
463
  `www.gstatic.com/recaptcha`, not to those hosts at large).
369
464
  4. **Known `necessary` / `functional` hosts** already granted, which keep their
370
465
  real category rather than being swept up as marketing.
@@ -481,23 +576,26 @@ external requests. Rebuild them with `tools/build-inline.mjs` (see
481
576
  [`tools/README.md`](tools/README.md)); each block's header records the exact
482
577
  command that produced it.
483
578
 
484
- ConsentKit 0.4.0, rebuilt 2026-09-05, uncompressed — gzip on the server cuts
579
+ ConsentKit 0.5.0, rebuilt 2026-09-05, uncompressed — gzip on the server cuts
485
580
  this roughly three- to fourfold. Every block includes the branding extension
486
581
  and the attribution line; `--no-branding` drops both the code and the config
487
582
  and takes **~24 KB** back off:
488
583
 
489
584
  | Block | Languages | Bytes | gzip | `--no-branding` |
490
585
  |---|---|---|---|---|
491
- | `ready/en-bar.txt` | en | 134,969 | 40,519 | 110,907 |
492
- | `ready/ru-bar.txt` | ru, ro, en | 136,590 | 41,315 | 112,508 |
493
- | `ready/ru-box.txt` | ru, ro, en | 136,605 | 41,320 | 112,523 |
494
- | `ready/ru-box-right.txt` | ru, ro, en | 136,614 | 41,326 | 112,526 |
495
- | `ready/ru-modal.txt` | ru, ro, en | 136,598 | 41,318 | 112,514 |
496
- | `ready/eu-bar.txt` | 34 languages | 185,244 | 59,665 | 161,182 |
497
-
498
- 0.4.0 adds roughly 16.1 KB over 0.3.6: iframe interception, strict mode
499
- (same-site detection, the allowlists, the public-suffix table) and the
500
- extensible tracker database.
586
+ | `ready/en-bar.txt` | en | 160,388 | 48,950 | 136,326 |
587
+ | `ready/ru-bar.txt` | ru, ro, en | 162,009 | 49,711 | 137,927 |
588
+ | `ready/ru-box.txt` | ru, ro, en | 162,024 | 49,718 | 137,942 |
589
+ | `ready/ru-box-right.txt` | ru, ro, en | 162,033 | 49,718 | 137,945 |
590
+ | `ready/ru-modal.txt` | ru, ro, en | 162,017 | 49,713 | 137,933 |
591
+ | `ready/eu-bar.txt` | 34 languages | 210,663 | 68,094 | 186,601 |
592
+
593
+ 0.5.0 adds roughly 18.5 KB over 0.4.1 (about 6.1 KB gzipped): the contrast
594
+ engine, the per-button token resolution, the new stylesheet rules and the
595
+ comments explaining the rules the arithmetic implements. 0.4.1 had added
596
+ roughly 6.3 KB over 0.4.0 (the infrastructure list), and 0.4.0 roughly 16.1 KB
597
+ over 0.3.6: iframe interception, strict mode (same-site detection, the
598
+ allowlists, the public-suffix table) and the extensible tracker database.
501
599
 
502
600
  Size is driven almost entirely by the bundled languages: `en` and `ru` are
503
601
  built into the UI and cost nothing extra, while layout, position, theme and
package/npm/index.d.ts CHANGED
@@ -59,17 +59,87 @@ export interface CkDarkTheme {
59
59
  accent?: string;
60
60
  }
61
61
 
62
+ /** v0.5.0 (SPEC V1.6 §1). Corner radii in px, clamped to 0–32. */
63
+ export interface CkRadiusConfig {
64
+ /** Banner, panel and table corners. Default `16`. */
65
+ card?: number;
66
+ /** Button corners. Default `8`. */
67
+ button?: number;
68
+ }
69
+
70
+ /**
71
+ * v0.5.0 (SPEC V1.6 §1). One button's appearance.
72
+ *
73
+ * Contrast is enforced automatically and cannot be switched off: `fg` is kept
74
+ * only when it clears 4.5:1 against the fill it sits on, otherwise it becomes
75
+ * white or `#161616`, whichever wins. An `outline` `border` is darkened or
76
+ * lightened in steps until it clears 3:1 against the card, and that resolved
77
+ * border colour is then the text colour, subject to the same 4.5:1 rule.
78
+ * `ConsentKit._contrast` exposes the arithmetic.
79
+ */
80
+ export interface CkButtonStyle {
81
+ variant?: 'filled' | 'outline';
82
+ /** Fill. `filled` defaults to the accent; `outline` is transparent. */
83
+ bg?: string;
84
+ /** Text. Computed from the contrast rules when omitted. */
85
+ fg?: string;
86
+ /** Border. Defaults to the accent. */
87
+ border?: string;
88
+ /** `1` (default) or `2`. */
89
+ borderWidth?: 1 | 2;
90
+ }
91
+
92
+ /**
93
+ * v0.5.0 (SPEC V1.6 §1). Per-button appearance.
94
+ *
95
+ * `accept` and `reject` are held to the equal-buttons invariant: they always
96
+ * render at the same size and weight, and they always share one variant. If
97
+ * the two disagree, `accept.variant` is used for both.
98
+ */
99
+ export interface CkButtonsConfig {
100
+ /** Default `{ variant: 'filled' }` with the accent as the fill. */
101
+ accept?: CkButtonStyle;
102
+ /** Default `{ variant: 'filled' }`; always follows `accept.variant`. */
103
+ reject?: CkButtonStyle;
104
+ /** Default `{ variant: 'outline' }` with the accent as the border. */
105
+ settings?: CkButtonStyle;
106
+ }
107
+
62
108
  export interface CkThemeConfig {
63
109
  /** Accent colour, exposed as `--ck-accent`. Default `'#2B50D8'`. */
64
110
  accent?: string;
65
- /** Corner radius, exposed as `--ck-radius`. Default `'10px'`. */
66
- radius?: string;
111
+ /**
112
+ * v0.5.0. `{ card, button }` in px, 0–32. Default `{ card: 16, button: 8 }`.
113
+ *
114
+ * A bare string or number is the pre-0.5.0 form and still works: it sets the
115
+ * card radius and leaves the button radius at its default.
116
+ */
117
+ radius?: CkRadiusConfig | string | number;
118
+ /**
119
+ * v0.5.0. `'inherit'` (default) takes the host page's font family — sizes
120
+ * stay fixed either way. `'system'` restores the pre-0.5.0 system stack.
121
+ */
122
+ font?: 'inherit' | 'system';
123
+ /** v0.5.0. Per-button appearance; contrast is enforced automatically. */
124
+ buttons?: CkButtonsConfig;
67
125
  /** v0.2. Default `'auto'` — follows `prefers-color-scheme`. */
68
126
  mode?: 'auto' | 'light' | 'dark';
69
127
  /** v0.2. Overrides the built-in dark palette. */
70
128
  dark?: CkDarkTheme;
71
129
  }
72
130
 
131
+ /** v0.5.0 (SPEC V1.6 §2). Copy and the «Learn more» control. */
132
+ export interface CkTextsConfig {
133
+ /** Cookie policy address. `http(s)` only; anything else is ignored. */
134
+ policyUrl?: string;
135
+ /**
136
+ * What «Learn more» does. Defaults to `'policy'` when `policyUrl` is set and
137
+ * `'settings'` when it is not. `'policy'` without a usable URL falls back to
138
+ * `'settings'` rather than rendering a dead link. `'hide'` renders nothing.
139
+ */
140
+ detailsAction?: 'policy' | 'settings' | 'hide';
141
+ }
142
+
73
143
  /** Whether a category is offered in the preferences panel at all. */
74
144
  export interface CkCategoryConfig {
75
145
  enabled?: boolean;
@@ -119,6 +189,8 @@ export interface CkConfig {
119
189
  language?: string;
120
190
  layout?: CkLayoutConfig;
121
191
  theme?: CkThemeConfig;
192
+ /** v0.5.0. */
193
+ texts?: CkTextsConfig;
122
194
  categories?: CkCategoriesConfig;
123
195
  /** Lifetime of the stored decision, in days. Default `365`. */
124
196
  consentTtlDays?: number;
@@ -180,14 +252,100 @@ export interface ConsentKitApi {
180
252
  /**
181
253
  * v0.4.0 (§2). The built-in strict-mode allowlist, as hosts plus a few
182
254
  * `host/path` entries (reCAPTCHA). A copy: mutating it changes nothing.
255
+ *
256
+ * Since v0.4.1 this includes the whole of `_infra()`.
183
257
  */
184
258
  readonly _baseAllow: string[];
185
259
 
260
+ /**
261
+ * v0.4.1 (§8). The infrastructure list: CDNs and static hosts of site
262
+ * builders and hosting platforms, general asset CDNs, fonts and captcha.
263
+ *
264
+ * A CLASS of host, not a consent category — these serve the site's own
265
+ * assets, so there is no consent decision to make about them. Strict mode
266
+ * never intercepts them, and the SaaS scanner keeps them out of the report.
267
+ * A copy: mutating it changes nothing.
268
+ */
269
+ _infra(): string[];
270
+
271
+ /**
272
+ * v0.4.1 (§8). Whether a URL — or a bare hostname — is infrastructure.
273
+ */
274
+ _isInfra(url: string): boolean;
275
+
186
276
  /** The category the engine would assign to a URL, or `null` if unknown. */
187
277
  _categoryForUrl(url: string): CkCategory | null;
188
278
 
189
279
  /** What is currently held back, host+path only — never a query string. */
190
280
  _blocked(): CkBlockedEntry[];
281
+
282
+ /**
283
+ * v0.5.0 (SPEC V1.6 §1). The contrast and theme arithmetic the banner
284
+ * actually paints with, as pure functions — so a theme editor quotes the
285
+ * same numbers rather than reimplementing them and drifting.
286
+ *
287
+ * Present only when `src/ck-ui.js` is loaded (the core alone does not draw).
288
+ * Colour arguments are hex; anything unmeasurable (a colour name, an `rgb()`
289
+ * string) yields `null` from the ratio functions and is returned untouched,
290
+ * with `adjusted: false`, by the rest.
291
+ */
292
+ readonly _contrast?: CkContrastApi;
293
+ }
294
+
295
+ /** One resolved button, as `ConsentKit._contrast.resolveButtonStyles()` returns it. */
296
+ export interface CkResolvedButton {
297
+ variant: 'filled' | 'outline';
298
+ bg: string;
299
+ fg: string;
300
+ border: string;
301
+ borderWidth: 1 | 2;
302
+ /** Text contrast against `against`. `null` when it could not be measured. */
303
+ ratio: number | null;
304
+ /** `true` when the contrast rules overrode what the config asked for. */
305
+ adjusted: boolean;
306
+ /** Border contrast against the card (`outline` only). */
307
+ borderRatio?: number | null;
308
+ borderAdjusted?: boolean;
309
+ /** What `ratio` was measured against: the button's own fill, or the card. */
310
+ against: string;
311
+ }
312
+
313
+ export interface CkResolvedButtons {
314
+ mode: 'light' | 'dark';
315
+ cardBg: string;
316
+ accent: string;
317
+ buttons: { accept: CkResolvedButton; reject: CkResolvedButton; settings: CkResolvedButton };
318
+ }
319
+
320
+ /** v0.5.0. `ConsentKit._contrast` — pure, DOM-free, safe to call in Node. */
321
+ export interface CkContrastApi {
322
+ /** WCAG 2.1 relative luminance, 0–1. `null` for an unmeasurable colour. */
323
+ relativeLuminance(hex: string): number | null;
324
+ /** WCAG 2.1 contrast ratio, 1–21. `null` when either side is unmeasurable. */
325
+ contrastRatio(a: string, b: string): number | null;
326
+ /** Keeps `fg` when it clears `min`, else white or `#161616` — whichever wins. */
327
+ ensureContrast(fg: string, bg: string, min?: number):
328
+ { color: string; adjusted: boolean; ratio: number | null };
329
+ /** Darkens or lightens `color` in steps until it clears `min` against `bg`. */
330
+ stepToContrast(color: string, bg: string, min?: number):
331
+ { color: string; adjusted: boolean; ratio: number | null };
332
+ /** The three buttons resolved for one palette, contrast already applied. */
333
+ resolveButtonStyles(theme: CkThemeConfig, mode: 'light' | 'dark',
334
+ palette?: Record<string, string>): CkResolvedButtons;
335
+ resolveRadius(theme: CkThemeConfig): { card: number; button: number };
336
+ resolveFont(theme: CkThemeConfig): string;
337
+ /** Takes the whole config, not just `texts`. */
338
+ resolveDetails(config: CkConfig):
339
+ { kind: 'policy' | 'settings' | 'hide'; href: string | null };
340
+ /** Takes the whole config. Returns the generated stylesheet and both palettes. */
341
+ buildThemeCss(config: CkConfig): {
342
+ css: string;
343
+ mode: 'auto' | 'light' | 'dark';
344
+ radius: { card: number; button: number };
345
+ font: string;
346
+ light: CkResolvedButtons;
347
+ dark: CkResolvedButtons;
348
+ };
191
349
  }
192
350
 
193
351
  /** One entry of `ConsentKit._blocked()`. */
@@ -28,7 +28,7 @@ export function undecidedState() {
28
28
  */
29
29
  export function createStub() {
30
30
  const stub = {
31
- version: '0.4.0',
31
+ version: '0.5.0',
32
32
  config: {},
33
33
  init: function () { return undecidedState(); },
34
34
  allowed: function (cat) { return cat === 'necessary'; },
@@ -44,6 +44,11 @@ export function createStub() {
44
44
  _extendHostDb: function () { return 0; },
45
45
  _baseAllow: [],
46
46
  _categoryForUrl: function () { return null; },
47
+ // v0.4.1 (§8): the infrastructure list. Empty here for the same reason
48
+ // _baseAllow is — a stub means the engine never attached, so it is not
49
+ // letting anything through and must not claim a list it does not enforce.
50
+ _infra: function () { return []; },
51
+ _isInfra: function () { return false; },
47
52
  _blocked: function () { return []; },
48
53
  _isStub: true
49
54
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecomconsult/consentkit",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "GDPR cookie consent core with blocking engine, Shadow DOM UI and Google Consent Mode v2. Zero dependencies, no build step.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/ck-core.js CHANGED
@@ -77,6 +77,10 @@
77
77
  'nr-data.net': 'analytics', // New Relic beacon
78
78
  'datadoghq.com': 'analytics', // RUM
79
79
  'datadoghq-browser-agent.com': 'analytics',
80
+ // Cloudflare Web Analytics. Subdomain only, and deliberately NOT in
81
+ // INFRA_DB: the rest of Cloudflare's edge is infrastructure, but this one
82
+ // beacon is a measurement product (§8 names it by hand for that reason).
83
+ 'static.cloudflareinsights.com': 'analytics',
80
84
 
81
85
  // --- marketing -----------------------------------------------------
82
86
  'connect.facebook.net': 'marketing',
@@ -215,7 +219,16 @@
215
219
  policyVersion: '1',
216
220
  language: 'auto',
217
221
  layout: { type: 'bar', position: 'bottom' },
218
- theme: { accent: '#2B50D8', radius: '10px' },
222
+ // 0.5.0: radius/font/buttons are resolved in ck-ui.js, not defaulted here.
223
+ // A concrete default in this object would be merged into every config and
224
+ // would then shadow the UI's own defaults — which is exactly what happened
225
+ // to `radius: '10px'` before 0.5.0: it pinned the pre-reference geometry on
226
+ // every site whether or not the owner had ever set it.
227
+ theme: { accent: '#2B50D8' },
228
+ // 0.5.0 (SPEC V1.6 §2). `detailsAction` is deliberately absent rather than
229
+ // set: its default depends on whether policyUrl is present, and a merged
230
+ // concrete value here would make that URL-sensitive default unreachable.
231
+ texts: {},
219
232
  categories: {
220
233
  functional: { enabled: true },
221
234
  analytics: { enabled: true },
@@ -231,22 +244,66 @@
231
244
  cookieTable: []
232
245
  };
233
246
 
234
- // Hosts strict mode never intercepts, even though they are third-party and
235
- // unknown to HOST_DB. Two kinds only: asset CDNs that serve the site's own
236
- // code, and things a page is broken or unusable without (payment, captcha).
237
- // Deliberately short everything else is the site owner's `blocking.allow`.
238
- // Matched with the same suffix semantics as HOST_DB; the recaptcha entries
239
- // are hosts because www.google.com/recaptcha and www.gstatic.com/recaptcha
240
- // cannot be expressed host-wise without also allowing the whole of google.com,
241
- // so they are handled by BASE_ALLOW_PATH below instead.
242
- var BASE_ALLOW = [
247
+ // Infrastructure (§8) NOT a consent category, a CLASS of host.
248
+ //
249
+ // These are the CDNs, static hosts and font services that site builders and
250
+ // hosting platforms serve a site's OWN markup, styles and scripts from. A
251
+ // Tilda page loads its own layout from tildacdn.com; a Wix page loads its own
252
+ // from parastorage.com. Reporting those to the site owner as «сторонние
253
+ // подключения без категории» is noise about something they cannot decide:
254
+ // there is no consent question to answer and no switch to flip. §8 exists to
255
+ // stop the audit spending the owner's attention on them.
256
+ //
257
+ // Two consequences, both of them the point:
258
+ // - strict mode NEVER intercepts these (a builder's dynamic modules break
259
+ // without their own CDN, and blocking them breaks the site, not a tracker);
260
+ // - the scanner flags them `infra: true`, keeps them out of the summary and
261
+ // never writes them to tracker_observations.
262
+ //
263
+ // Suffix-matched, exactly like HOST_DB. Membership here is not a claim that a
264
+ // host is harmless in general — it is a claim that it serves the site's own
265
+ // assets. Anything that MEASURES belongs in HOST_DB with a real category
266
+ // instead: static.cloudflareinsights.com is the worked example (§8 calls it
267
+ // out by name), and it sits above as `analytics` for that reason. Because
268
+ // categoryForUrl is consulted before the strict allowlist, a host in both
269
+ // places is classified, not waved through — so a bare `cloudflare.com` here
270
+ // would be a bug.
271
+ var INFRA_DB = [
272
+ // --- site builders and hosting platforms ---------------------------
273
+ 'tildacdn.com',
274
+ 'tildacdn.net',
275
+ 'tilda.ws',
276
+ 'static.wixstatic.com',
277
+ 'parastorage.com', // Wix static assets
278
+ 'cdn.shopify.com',
279
+ 'squarespace-cdn.com',
280
+ 'assets.website-files.com', // Webflow
281
+ // --- general asset CDNs --------------------------------------------
243
282
  'cdn.jsdelivr.net',
244
283
  'unpkg.com',
245
284
  'cdnjs.cloudflare.com',
246
285
  'code.jquery.com',
286
+ 'ajax.googleapis.com',
287
+ // --- fonts ----------------------------------------------------------
247
288
  'fonts.googleapis.com',
248
289
  'fonts.gstatic.com',
290
+ // --- captcha ---------------------------------------------------------
291
+ // A page whose form is gated behind a captcha is unusable without it. The
292
+ // Google-hosted halves are path-scoped in BASE_ALLOW_PATH below, since
293
+ // www.google.com and www.gstatic.com cannot be waved through wholesale.
249
294
  'hcaptcha.com',
295
+ // --- our own service -------------------------------------------------
296
+ // The consent tool must not report itself as an unnamed third party.
297
+ 'consent.ecomconsult.net'
298
+ ];
299
+
300
+ // Hosts strict mode never intercepts that are NOT infrastructure: things a
301
+ // page is broken or unusable without, but which are somebody's product
302
+ // rather than the site's own asset delivery. Kept separate from INFRA_DB so
303
+ // that «инфраструктура» in a report means what §8 says it means — a payment
304
+ // form is a third party the owner chose, and calling it infrastructure would
305
+ // hide a real decision. Both lists feed strict mode identically.
306
+ var BASE_ALLOW = [
250
307
  'js.stripe.com',
251
308
  'pay.google.com',
252
309
  'checkout.creem.io'
@@ -254,6 +311,12 @@
254
311
 
255
312
  // Path-scoped members of the base allowlist: allowed only on this exact path
256
313
  // prefix, because the host at large is not something to wave through.
314
+ //
315
+ // §8 lists these two under infrastructure, and they are — but they cannot
316
+ // live in INFRA_DB, which is a flat list of suffix-matched HOSTS. Allowing
317
+ // `www.google.com` as a host would wave through every Google property, and
318
+ // `_isInfra(host)` has no path to test. So they stay here: strict mode treats
319
+ // them exactly as it treats INFRA_DB, and `_infra()` reports hosts only.
257
320
  var BASE_ALLOW_PATH = [
258
321
  { host: 'www.google.com', path: '/recaptcha' },
259
322
  { host: 'www.gstatic.com', path: '/recaptcha' }
@@ -760,7 +823,32 @@
760
823
  return !!r && r === pr;
761
824
  }
762
825
 
826
+ // Is this host infrastructure (§8)? Suffix-matched, like everything else.
827
+ //
828
+ // `ConsentKitDebugUrl` is resolved HERE rather than being baked into
829
+ // INFRA_DB, because it is a runtime window global a site sets to point the
830
+ // debug panel at its own mirror. Reading it at call time is the only way it
831
+ // can be covered at all; a snapshot taken when this file was evaluated would
832
+ // miss every page that sets it after the core loads.
833
+ function isInfraHost(host) {
834
+ if (!host) { return false; }
835
+ for (var i = 0; i < INFRA_DB.length; i++) {
836
+ if (hostMatches(host, INFRA_DB[i])) { return true; }
837
+ }
838
+ try {
839
+ var dbg = global.ConsentKitDebugUrl;
840
+ if (typeof dbg === 'string' && dbg) {
841
+ var dh = urlParts(dbg).host;
842
+ if (dh && dh === host) { return true; }
843
+ }
844
+ } catch (e) { /* noop */ }
845
+ return false;
846
+ }
847
+
763
848
  function baseAllowed(host, url) {
849
+ // §8: infrastructure is never intercepted by strict mode. Checked first —
850
+ // it is the larger list and the commoner case on a builder-hosted site.
851
+ if (isInfraHost(host)) { return true; }
764
852
  for (var i = 0; i < BASE_ALLOW.length; i++) {
765
853
  if (hostMatches(host, BASE_ALLOW[i])) { return true; }
766
854
  }
@@ -1325,7 +1413,7 @@
1325
1413
  // Public API
1326
1414
  // ---------------------------------------------------------------------------
1327
1415
  var ConsentKit = {
1328
- version: '0.4.0',
1416
+ version: '0.5.0',
1329
1417
  config: config,
1330
1418
 
1331
1419
  init: function (userConfig) {
@@ -1400,7 +1488,11 @@
1400
1488
  },
1401
1489
 
1402
1490
  // The built-in strict-mode allowlist, exported for docs and tests so the
1403
- // list a site owner reads is the list the engine actually uses.
1491
+ // list a site owner reads is the list the engine actually uses. Since 0.4.1
1492
+ // that is INFRA_DB (§8) plus the payment/captcha set plus the path-scoped
1493
+ // entries — everything baseAllowed() consults, in one list, because what a
1494
+ // site owner needs to know is «what does strict mode let through», not how
1495
+ // the engine files it internally.
1404
1496
  //
1405
1497
  // A GETTER, not a plain array: a plain property is evaluated once, and the
1406
1498
  // single array it produced would be handed to every caller — one
@@ -1408,7 +1500,48 @@
1408
1500
  // widen what strict mode lets through for the rest of the page load.
1409
1501
  // Each read returns a fresh copy, so the list is readable and inert.
1410
1502
  get _baseAllow() {
1411
- return BASE_ALLOW.slice().concat(BASE_ALLOW_PATH.map(function (e) { return e.host + e.path; }));
1503
+ return INFRA_DB.slice()
1504
+ .concat(BASE_ALLOW)
1505
+ .concat(BASE_ALLOW_PATH.map(function (e) { return e.host + e.path; }));
1506
+ },
1507
+
1508
+ // The infrastructure list (§8): CDNs and static hosts of site builders and
1509
+ // hosting platforms, general asset CDNs, fonts and captcha. A CLASS of
1510
+ // host, not a consent category — nothing here is a decision the site owner
1511
+ // gets to make, which is exactly why the audit stops reporting them.
1512
+ //
1513
+ // Shared with the SaaS scanner, which loads this core and reads this list
1514
+ // to set `thirdParty[].infra`, keep infrastructure out of the summary and
1515
+ // out of tracker_observations. Hosts only: the two path-scoped recaptcha
1516
+ // entries are infrastructure too, but cannot be expressed as bare hosts —
1517
+ // see BASE_ALLOW_PATH. A fresh copy per read, for the same reason as
1518
+ // _baseAllow.
1519
+ _infra: function () {
1520
+ return INFRA_DB.slice();
1521
+ },
1522
+
1523
+ // Is this URL infrastructure? Takes a URL, like every other public
1524
+ // predicate here, so a caller never has to reimplement host extraction.
1525
+ //
1526
+ // A BARE HOST is accepted too ('tildacdn.com'), because the scanner works
1527
+ // in hosts and would otherwise have to glue a fake scheme on every call.
1528
+ // It needs its own branch: urlParts resolves a relative string against the
1529
+ // page, so a bare host arrives as a PATH on the first-party origin and
1530
+ // would answer false. The test is deliberately narrow — a string with no
1531
+ // scheme, no slash, no query and at least one dot is a hostname and cannot
1532
+ // be anything else.
1533
+ _isInfra: function (url) {
1534
+ try {
1535
+ if (!url || typeof url !== 'string') { return false; }
1536
+ var s = url.trim();
1537
+ if (!s) { return false; }
1538
+ if (/^[a-z0-9.\-]+\.[a-z0-9\-]+$/i.test(s)) {
1539
+ return isInfraHost(s.toLowerCase().replace(/^\.+|\.+$/g, ''));
1540
+ }
1541
+ var host = urlParts(s).host;
1542
+ if (!host) { return false; }
1543
+ return isInfraHost(host);
1544
+ } catch (e) { return false; }
1412
1545
  },
1413
1546
 
1414
1547
  // What is being held back until consent: everything the engine intercepted