@ecomconsult/consentkit 0.3.5 → 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
@@ -44,7 +44,7 @@ Four ways to add ConsentKit to a site, from simplest to most integrated.
44
44
  |---|---|---|---|
45
45
  | 1 | **Script tags** — copy `src/` to your server, three `<script>` tags in `<head>` | Any site you control | [Quickstart below](#quickstart--script-tags) |
46
46
  | 2 | **npm** — `npm install @ecomconsult/consentkit` | Bundled apps, React | [Quickstart below](#quickstart--npm) |
47
- | 3 | **WordPress plugin** — copy the plugin folder to `wp-content/plugins/` | WordPress / WooCommerce | [`plugins/wordpress/consentkit/`](plugins/wordpress/consentkit/) |
47
+ | 3 | **WordPress plugin** — copy the plugin folder to `wp-content/plugins/`; rewrites static tracker tags server-side | WordPress / WooCommerce | [`plugins/wordpress/consentkit/`](plugins/wordpress/consentkit/) |
48
48
  | 4 | **Google Tag Manager** — import the container, trigger tags on consent events | Sites already running GTM | [`integrations/gtm/README.md`](integrations/gtm/README.md) |
49
49
 
50
50
  ```sh
@@ -66,6 +66,7 @@ must come first and should not be deferred.
66
66
  ```html
67
67
  <script src="/consentkit/src/ck-core.js"></script>
68
68
  <script src="/consentkit/src/ck-locales.js"></script><!-- optional: extra languages -->
69
+ <script src="/consentkit/src/ck-ui-branding.js"></script><!-- optional: logo / attribution -->
69
70
  <script src="/consentkit/src/ck-ui.js"></script>
70
71
  <script>
71
72
  ConsentKit.init({
@@ -178,16 +179,23 @@ Pass any subset to `init()`. Nested objects merge with the defaults.
178
179
  |---|---|---|---|
179
180
  | `policyVersion` | `string \| number` | `"1"` | Bump to invalidate stored consent and re-show the banner |
180
181
  | `language` | `string` | `"auto"` | `"auto"` reads `navigator.language`. Falls back `pt-BR` → `pt` → `en` |
181
- | `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 |
182
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 |
183
184
  | `theme.accent` | `string` | `"#2B50D8"` | Exposed as `--ck-accent` |
184
- | `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) |
185
188
  | `theme.mode` | `"auto" \| "light" \| "dark"` | `"auto"` | `auto` follows `prefers-color-scheme` |
186
- | `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 |
187
192
  | `categories.*.enabled` | `boolean` | `true` | Per category: `functional`, `analytics`, `marketing`. Hides the toggle when `false` |
188
193
  | `consentTtlDays` | `number` | `365` | Lifetime of the stored decision |
189
194
  | `integrations.gcm` | `boolean` | `true` | Google Consent Mode v2 signals |
190
195
  | `integrations.gtmDataLayer` | `boolean` | `true` | Push consent events to `window.dataLayer` |
196
+ | `blocking.mode` | `"known" \| "strict"` | `"known"` | `strict` also holds back unknown third-party scripts and iframes — see [Strict mode](#strict-mode) |
197
+ | `blocking.allow` | `string[]` | `[]` | Hosts strict mode must never intercept. Matched by suffix, so `partner.com` also covers `cdn.partner.com` |
198
+ | `hostdb` | `Record<string, Category>` | — | Extra `host: category` pairs merged into the tracker database, applied before the initial scan. SaaS mode fills this from the service; `ConsentKit._extendHostDb()` does the same at any later point |
191
199
  | `cookieTable` | `CkCookieTableEntry[]` | `[]` | Declared cookies, listed per category in the panel |
192
200
 
193
201
  `cookieTable` entries:
@@ -196,6 +204,98 @@ Pass any subset to `init()`. Nested objects merge with the defaults.
196
204
  { name: '_ga', category: 'analytics', vendor: 'Google', purpose: 'Visit statistics', expiry: '2 years' }
197
205
  ```
198
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
+
199
299
  ## API
200
300
 
201
301
  All methods are safe to call at any time and never throw.
@@ -279,6 +379,11 @@ Iframes use `data-src`, which is applied once the category is allowed:
279
379
 
280
380
  `data-ck` accepts any category name: `functional`, `analytics`, `marketing`.
281
381
 
382
+ > On WordPress this markup is applied **automatically, server-side**, for every
383
+ > tracker in the built-in database — see "Server-side markup" below. Manual
384
+ > markup is still needed for trackers the database does not know (your own
385
+ > domain, an unlisted vendor) and for inline snippets.
386
+
282
387
  ### Automatic blocking
283
388
 
284
389
  Scripts injected at runtime are intercepted without any markup. ConsentKit
@@ -301,6 +406,129 @@ and DoubleClick.
301
406
  Because the patches install at parse time, `ck-core.js` must load before any
302
407
  tracker — put it first in `<head>` and do not add `defer`.
303
408
 
409
+ `<iframe src>` is covered by the same three patches, and a blocked frame keeps
410
+ its URL in `data-src` until its category is granted.
411
+
412
+ ### Extending the tracker database
413
+
414
+ The built-in host list is a snapshot, not an oracle. `_extendHostDb()` merges
415
+ extra `host: category` pairs into it at runtime:
416
+
417
+ ```js
418
+ ConsentKit._extendHostDb({
419
+ 'analytics.vendor.example': 'analytics',
420
+ 'pixel.partner.example': 'marketing'
421
+ });
422
+ ```
423
+
424
+ Matching is the same as for built-in entries — a bare domain also covers its
425
+ subdomains — and an override wins over the shipped classification for the same
426
+ host. Categories outside `necessary | functional | analytics | marketing` and
427
+ malformed hostnames are ignored; the call returns how many pairs were accepted.
428
+
429
+ It works both **before and after** `init()`. Calling it afterwards does not
430
+ re-examine anything already inserted (a script that has loaded cannot be
431
+ unloaded), but every later insertion is classified against the extended map.
432
+
433
+ In SaaS mode this is automatic: `ck-saas.js` applies `config.hostdb` from the
434
+ service *before* it calls `init()`, and again when a background revalidation
435
+ brings a changed table. At release time `node tools/sync-hostdb.mjs` bakes the
436
+ same public table into `src/ck-core.js`, so inline blocks, the npm package and
437
+ the WordPress plugin get it too.
438
+
439
+ ### Strict mode
440
+
441
+ By default ConsentKit blocks what it **recognises**. `blocking.mode: 'strict'`
442
+ inverts that for third parties: before consent, any `<script src>` or
443
+ `<iframe src>` pointing at a host that is not same-site is intercepted, whether
444
+ or not the tracker database has ever heard of it.
445
+
446
+ ```js
447
+ ConsentKit.init({
448
+ blocking: { mode: 'strict', allow: ['widgets.partner.example'] }
449
+ });
450
+ ```
451
+
452
+ Four things are never intercepted:
453
+
454
+ 1. **Same-site URLs** — the page's own host, its subdomains, and anything
455
+ sharing its registrable domain. The check is deliberately conservative: when
456
+ the answer is unclear it says same-site, because wrongly blocking a
457
+ first-party asset breaks the site.
458
+ 2. **`blocking.allow`** — your own list, matched by suffix.
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
463
+ `www.gstatic.com/recaptcha`, not to those hosts at large).
464
+ 4. **Known `necessary` / `functional` hosts** already granted, which keep their
465
+ real category rather than being swept up as marketing.
466
+
467
+ Anything else is filed under **`marketing`** — the strictest category — and
468
+ comes back only when the visitor accepts marketing.
469
+
470
+ **Read this before switching it on.** Strict mode will block third-party code
471
+ your site needs and that ConsentKit has no way to recognise as necessary: a
472
+ booking widget, a map, a review embed, a payment provider that is not on the
473
+ list. Turn it on, load the site with `?ck_debug=1`, and read the "Blocked until
474
+ consent" list in the panel — entries the engine held back only because of strict
475
+ mode are labelled `strict`. Everything there that the page genuinely needs
476
+ belongs in `blocking.allow`.
477
+
478
+ Two limits are worth stating plainly:
479
+
480
+ * **Dynamic insertions only**, exactly as for known trackers. A tag written
481
+ straight into the HTML starts its request before ConsentKit runs (see below).
482
+ The WordPress plugin's server-side rewrite currently marks up *known* trackers
483
+ only; extending it to strict mode is recorded as a follow-up in SPEC.md.
484
+ * **Strict starts when the config does.** The mode is read from `config`, so in
485
+ SaaS mode nothing is blocked strictly until the config has arrived. Blocking
486
+ of *known* trackers still begins at parse time, as always.
487
+
488
+ ### Static tags: what the browser cannot catch
489
+
490
+ Runtime injection is covered by the patches above. A tracker tag written
491
+ **directly into the HTML** is not: the parser starts that request before the
492
+ first line of `ck-core.js` runs. The gap was measured (debt Д9: request at
493
+ 14 ms, our script at 18 ms) and it is negative — no client-side technique
494
+ closes it. Such tags need either manual markup, or a server that rewrites them
495
+ before the page is sent.
496
+
497
+ ### Server-side markup (WordPress plugin)
498
+
499
+ The WordPress plugin does exactly that, and it is **on by default** since 0.3.5.
500
+ While the page is generated, it rewrites tracker tags in the finished HTML:
501
+
502
+ ```html
503
+ <!-- what the theme wrote -->
504
+ <script src="https://mc.yandex.ru/metrika/tag.js"></script>
505
+
506
+ <!-- what the browser receives -->
507
+ <script type="text/plain" data-ck="analytics"
508
+ data-ck-src="https://mc.yandex.ru/metrika/tag.js"></script>
509
+ ```
510
+
511
+ `<iframe src>` of a known host becomes `data-ck` + `data-src` with `src`
512
+ removed. The categories come from the same HOST_DB/PATH_DB as the browser
513
+ engine: `tools/export-hostdb.mjs` generates
514
+ `plugins/wordpress/consentkit/includes/hostdb.php` from `src/ck-core.js`, and
515
+ `test/hostdb.test.mjs` fails if the two drift.
516
+
517
+ What it skips: ConsentKit's own assets, tags carrying `data-ck-ignore`, tags
518
+ already marked up by hand, inline scripts (there is no URL to defer), the GTM
519
+ container, and anything inside comments, `<pre>` or `<textarea>`. On any error
520
+ the page is returned unchanged.
521
+
522
+ The `<pre>` / `<textarea>` skip keeps the *source text* byte-identical, which is
523
+ what a page documenting a tracker snippet needs. It does not keep such a tag
524
+ alive: the browser parses `<pre><script src=…>` as a real script element
525
+ whatever the server did, so the runtime engine may still intercept it. Caching plugins are compatible and get the
526
+ already-rewritten HTML, because the rewrite happens at the PHP level before the
527
+ page is cached.
528
+
529
+ Outside WordPress the same idea applies to any server-side template: emit the
530
+ `type="text/plain" data-ck` form directly, as in "Manual markup" above.
531
+
304
532
  ## Google Consent Mode v2
305
533
 
306
534
  With `integrations.gcm` (default), the core pushes `consent: default` with every
@@ -348,18 +576,26 @@ external requests. Rebuild them with `tools/build-inline.mjs` (see
348
576
  [`tools/README.md`](tools/README.md)); each block's header records the exact
349
577
  command that produced it.
350
578
 
351
- ConsentKit 0.3.5, rebuilt 2026-09-04, uncompressed — gzip on the server cuts
352
- this roughly three- to fourfold. Every block includes the attribution line;
353
- `--no-branding` takes ~200 bytes back off:
354
-
355
- | Block | Languages | Bytes |
356
- |---|---|---|
357
- | `ready/en-bar.txt` | en | 111,664 |
358
- | `ready/ru-bar.txt` | ru, ro, en | 113,285 |
359
- | `ready/ru-box.txt` | ru, ro, en | 113,300 |
360
- | `ready/ru-box-right.txt` | ru, ro, en | 113,309 |
361
- | `ready/ru-modal.txt` | ru, ro, en | 113,293 |
362
- | `ready/eu-bar.txt` | 34 languages | 161,939 |
579
+ ConsentKit 0.5.0, rebuilt 2026-09-05, uncompressed — gzip on the server cuts
580
+ this roughly three- to fourfold. Every block includes the branding extension
581
+ and the attribution line; `--no-branding` drops both the code and the config
582
+ and takes **~24 KB** back off:
583
+
584
+ | Block | Languages | Bytes | gzip | `--no-branding` |
585
+ |---|---|---|---|---|
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.
363
599
 
364
600
  Size is driven almost entirely by the bundled languages: `en` and `ru` are
365
601
  built into the UI and cost nothing extra, while layout, position, theme and
@@ -540,13 +776,19 @@ has been verified and what has not.
540
776
  - `dataLayer` event trace for consent restore, upgrade and withdrawal.
541
777
  - npm entry points and TypeScript types: syntax and import smoke tests in Node
542
778
  without a DOM.
543
- - PHP files of the WordPress plugin pass `php -l` on 7.4 and 8.5.
779
+ - PHP files of the WordPress plugin pass `php -l` on 7.4, 8.3 and 8.5.
780
+ - The server-side rewriting engine has its own suite of 61 cases
781
+ (`plugins/wordpress/consentkit/tests/rewrite.test.php`), green on PHP 7.4,
782
+ 8.3 and 8.5. It is a plain PHP CLI script, so it runs outside `npm test`.
544
783
 
545
784
  ### Not verified — read before production use
546
785
 
547
- - **The WordPress plugin has never run on a live WordPress install.** It passes
548
- linting and review, but no activation, settings round-trip, theme conflict or
549
- multisite behaviour has been observed in a real installation.
786
+ - **The WordPress plugin has been verified on a live install, but only one.**
787
+ It was run on WordPress 7.1 / PHP 8.3 in Docker (activation, settings
788
+ round-trip, shortcode, uninstall, server-side markup end to end). The declared
789
+ floor of WordPress 6.0 / PHP 7.4 has not been exercised live — the PHP files
790
+ pass `php -l` and the rewriting test suite on 7.4, 8.3 and 8.5 — and no theme
791
+ conflict or multisite behaviour has been observed.
550
792
  - **The GTM container has never been through a real import.** The JSON is valid
551
793
  and structurally modelled on the documented export format, but Tag Manager has
552
794
  not accepted it in practice; some field names (notably GA4 config
package/npm/core.mjs CHANGED
@@ -28,6 +28,9 @@ export const {
28
28
  rejectAll,
29
29
  withdraw,
30
30
  show,
31
- hide
31
+ hide,
32
+ // v0.4.0 (§1.3): merging service overrides into the tracker database is part
33
+ // of the public surface, so it must be reachable as a named import too.
34
+ _extendHostDb
32
35
  } = ConsentKit;
33
36
  export { ConsentKit };
package/npm/index.cjs CHANGED
@@ -69,7 +69,10 @@ var hasDom = typeof document !== 'undefined' && typeof window !== 'undefined';
69
69
  if (hasDom) {
70
70
  // 2. Locales — optional language packs.
71
71
  try { require('../src/ck-locales.js'); } catch (e) { /* optional */ }
72
- // 3. UItouches `document` at module scope, browser only.
72
+ // 3. Brandingoptional, and before the UI: it registers itself on
73
+ // ConsentKit._uiExtensions and must be there before the first mount().
74
+ try { require('../src/ck-ui-branding.js'); } catch (e) { /* optional */ }
75
+ // 4. UI — touches `document` at module scope, browser only.
73
76
  try { require('../src/ck-ui.js'); } catch (e) { /* best effort */ }
74
77
  }
75
78
 
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;
@@ -88,6 +158,20 @@ export interface CkIntegrationsConfig {
88
158
  gtmDataLayer?: boolean;
89
159
  }
90
160
 
161
+ /** v0.4.0 (§2). How much the blocking engine holds back before consent. */
162
+ export interface CkBlockingConfig {
163
+ /**
164
+ * `'known'` (default) blocks what the tracker database recognises.
165
+ * `'strict'` additionally intercepts EVERY third-party `<script src>` and
166
+ * `<iframe src>` that is not same-site, not in `allow`, not in the built-in
167
+ * allowlist (`ConsentKit._baseAllow`) and not an already-granted
168
+ * necessary/functional host. Interceptions are filed under `marketing`.
169
+ */
170
+ mode?: 'known' | 'strict';
171
+ /** Hosts strict mode must never intercept. Suffix match: `p.com` covers `cdn.p.com`. */
172
+ allow?: string[];
173
+ }
174
+
91
175
  /** One declared cookie, shown under its category in the preferences panel. */
92
176
  export interface CkCookieTableEntry {
93
177
  name: string;
@@ -105,10 +189,21 @@ export interface CkConfig {
105
189
  language?: string;
106
190
  layout?: CkLayoutConfig;
107
191
  theme?: CkThemeConfig;
192
+ /** v0.5.0. */
193
+ texts?: CkTextsConfig;
108
194
  categories?: CkCategoriesConfig;
109
195
  /** Lifetime of the stored decision, in days. Default `365`. */
110
196
  consentTtlDays?: number;
111
197
  integrations?: CkIntegrationsConfig;
198
+ /** v0.4.0. Default `{ mode: 'known', allow: [] }`. */
199
+ blocking?: CkBlockingConfig;
200
+ /**
201
+ * v0.4.0 (§1.3). Extra `host: category` pairs merged into the tracker
202
+ * database. `init()` applies them before its initial scan, so scripts
203
+ * already in the markup are classified against them. In SaaS mode the
204
+ * service supplies this; `_extendHostDb()` does the same at any later point.
205
+ */
206
+ hostdb?: Record<string, CkCategory>;
112
207
  cookieTable?: CkCookieTableEntry[];
113
208
  }
114
209
 
@@ -141,6 +236,136 @@ export interface ConsentKitApi {
141
236
  show(): void;
142
237
  /** Dispatches `ck:ui:close`. */
143
238
  hide(): void;
239
+
240
+ /**
241
+ * v0.4.0 (§1.3). Merges `{ host: category }` into the runtime tracker
242
+ * database and returns how many pairs were accepted. Matching follows the
243
+ * built-in table: a bare domain also covers its subdomains, and an override
244
+ * wins over the shipped classification for the same host.
245
+ *
246
+ * Safe before AND after `init()`. Afterwards, nothing already inserted is
247
+ * re-examined — a script that has loaded cannot be unloaded — but every
248
+ * later insertion is classified against the extended map.
249
+ */
250
+ _extendHostDb(map: Record<string, CkCategory>): number;
251
+
252
+ /**
253
+ * v0.4.0 (§2). The built-in strict-mode allowlist, as hosts plus a few
254
+ * `host/path` entries (reCAPTCHA). A copy: mutating it changes nothing.
255
+ *
256
+ * Since v0.4.1 this includes the whole of `_infra()`.
257
+ */
258
+ readonly _baseAllow: string[];
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
+
276
+ /** The category the engine would assign to a URL, or `null` if unknown. */
277
+ _categoryForUrl(url: string): CkCategory | null;
278
+
279
+ /** What is currently held back, host+path only — never a query string. */
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
+ };
349
+ }
350
+
351
+ /** One entry of `ConsentKit._blocked()`. */
352
+ export interface CkBlockedEntry {
353
+ host: string;
354
+ path: string;
355
+ /** `'script'`, `'iframe'`, … */
356
+ kind: string;
357
+ category: CkCategory | null;
358
+ /** `'engine'` — intercepted by the patches; `'markup'` — marked up by hand. */
359
+ origin: 'engine' | 'markup';
360
+ /** v0.4.0. True when strict mode held this back, i.e. the host is unknown. */
361
+ strict: boolean;
362
+ /**
363
+ * v0.4.0. `false` when the category was granted but the element still never
364
+ * loaded — typically a script created and given a `src` without ever being
365
+ * appended, which `applyConsentToDom()` cannot reach. Such entries stay in
366
+ * the report after consent precisely so they can be diagnosed.
367
+ */
368
+ revived: boolean;
144
369
  }
145
370
 
146
371
  declare const ConsentKit: ConsentKitApi;
@@ -156,6 +381,7 @@ export declare function rejectAll(): CkState;
156
381
  export declare function withdraw(): CkState;
157
382
  export declare function show(): void;
158
383
  export declare function hide(): void;
384
+ export declare function _extendHostDb(map: Record<string, CkCategory>): number;
159
385
 
160
386
  declare global {
161
387
  interface Window {