@ecomconsult/consentkit 0.3.4 → 0.4.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({
@@ -188,6 +189,9 @@ Pass any subset to `init()`. Nested objects merge with the defaults.
188
189
  | `consentTtlDays` | `number` | `365` | Lifetime of the stored decision |
189
190
  | `integrations.gcm` | `boolean` | `true` | Google Consent Mode v2 signals |
190
191
  | `integrations.gtmDataLayer` | `boolean` | `true` | Push consent events to `window.dataLayer` |
192
+ | `blocking.mode` | `"known" \| "strict"` | `"known"` | `strict` also holds back unknown third-party scripts and iframes — see [Strict mode](#strict-mode) |
193
+ | `blocking.allow` | `string[]` | `[]` | Hosts strict mode must never intercept. Matched by suffix, so `partner.com` also covers `cdn.partner.com` |
194
+ | `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
195
  | `cookieTable` | `CkCookieTableEntry[]` | `[]` | Declared cookies, listed per category in the panel |
192
196
 
193
197
  `cookieTable` entries:
@@ -279,6 +283,11 @@ Iframes use `data-src`, which is applied once the category is allowed:
279
283
 
280
284
  `data-ck` accepts any category name: `functional`, `analytics`, `marketing`.
281
285
 
286
+ > On WordPress this markup is applied **automatically, server-side**, for every
287
+ > tracker in the built-in database — see "Server-side markup" below. Manual
288
+ > markup is still needed for trackers the database does not know (your own
289
+ > domain, an unlisted vendor) and for inline snippets.
290
+
282
291
  ### Automatic blocking
283
292
 
284
293
  Scripts injected at runtime are intercepted without any markup. ConsentKit
@@ -301,6 +310,130 @@ and DoubleClick.
301
310
  Because the patches install at parse time, `ck-core.js` must load before any
302
311
  tracker — put it first in `<head>` and do not add `defer`.
303
312
 
313
+ `<iframe src>` is covered by the same three patches, and a blocked frame keeps
314
+ its URL in `data-src` until its category is granted.
315
+
316
+ ### Extending the tracker database
317
+
318
+ The built-in host list is a snapshot, not an oracle. `_extendHostDb()` merges
319
+ extra `host: category` pairs into it at runtime:
320
+
321
+ ```js
322
+ ConsentKit._extendHostDb({
323
+ 'analytics.vendor.example': 'analytics',
324
+ 'pixel.partner.example': 'marketing'
325
+ });
326
+ ```
327
+
328
+ Matching is the same as for built-in entries — a bare domain also covers its
329
+ subdomains — and an override wins over the shipped classification for the same
330
+ host. Categories outside `necessary | functional | analytics | marketing` and
331
+ malformed hostnames are ignored; the call returns how many pairs were accepted.
332
+
333
+ It works both **before and after** `init()`. Calling it afterwards does not
334
+ re-examine anything already inserted (a script that has loaded cannot be
335
+ unloaded), but every later insertion is classified against the extended map.
336
+
337
+ In SaaS mode this is automatic: `ck-saas.js` applies `config.hostdb` from the
338
+ service *before* it calls `init()`, and again when a background revalidation
339
+ brings a changed table. At release time `node tools/sync-hostdb.mjs` bakes the
340
+ same public table into `src/ck-core.js`, so inline blocks, the npm package and
341
+ the WordPress plugin get it too.
342
+
343
+ ### Strict mode
344
+
345
+ By default ConsentKit blocks what it **recognises**. `blocking.mode: 'strict'`
346
+ inverts that for third parties: before consent, any `<script src>` or
347
+ `<iframe src>` pointing at a host that is not same-site is intercepted, whether
348
+ or not the tracker database has ever heard of it.
349
+
350
+ ```js
351
+ ConsentKit.init({
352
+ blocking: { mode: 'strict', allow: ['widgets.partner.example'] }
353
+ });
354
+ ```
355
+
356
+ Four things are never intercepted:
357
+
358
+ 1. **Same-site URLs** — the page's own host, its subdomains, and anything
359
+ sharing its registrable domain. The check is deliberately conservative: when
360
+ the answer is unclear it says same-site, because wrongly blocking a
361
+ first-party asset breaks the site.
362
+ 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
368
+ `www.gstatic.com/recaptcha`, not to those hosts at large).
369
+ 4. **Known `necessary` / `functional` hosts** already granted, which keep their
370
+ real category rather than being swept up as marketing.
371
+
372
+ Anything else is filed under **`marketing`** — the strictest category — and
373
+ comes back only when the visitor accepts marketing.
374
+
375
+ **Read this before switching it on.** Strict mode will block third-party code
376
+ your site needs and that ConsentKit has no way to recognise as necessary: a
377
+ booking widget, a map, a review embed, a payment provider that is not on the
378
+ list. Turn it on, load the site with `?ck_debug=1`, and read the "Blocked until
379
+ consent" list in the panel — entries the engine held back only because of strict
380
+ mode are labelled `strict`. Everything there that the page genuinely needs
381
+ belongs in `blocking.allow`.
382
+
383
+ Two limits are worth stating plainly:
384
+
385
+ * **Dynamic insertions only**, exactly as for known trackers. A tag written
386
+ straight into the HTML starts its request before ConsentKit runs (see below).
387
+ The WordPress plugin's server-side rewrite currently marks up *known* trackers
388
+ only; extending it to strict mode is recorded as a follow-up in SPEC.md.
389
+ * **Strict starts when the config does.** The mode is read from `config`, so in
390
+ SaaS mode nothing is blocked strictly until the config has arrived. Blocking
391
+ of *known* trackers still begins at parse time, as always.
392
+
393
+ ### Static tags: what the browser cannot catch
394
+
395
+ Runtime injection is covered by the patches above. A tracker tag written
396
+ **directly into the HTML** is not: the parser starts that request before the
397
+ first line of `ck-core.js` runs. The gap was measured (debt Д9: request at
398
+ 14 ms, our script at 18 ms) and it is negative — no client-side technique
399
+ closes it. Such tags need either manual markup, or a server that rewrites them
400
+ before the page is sent.
401
+
402
+ ### Server-side markup (WordPress plugin)
403
+
404
+ The WordPress plugin does exactly that, and it is **on by default** since 0.3.5.
405
+ While the page is generated, it rewrites tracker tags in the finished HTML:
406
+
407
+ ```html
408
+ <!-- what the theme wrote -->
409
+ <script src="https://mc.yandex.ru/metrika/tag.js"></script>
410
+
411
+ <!-- what the browser receives -->
412
+ <script type="text/plain" data-ck="analytics"
413
+ data-ck-src="https://mc.yandex.ru/metrika/tag.js"></script>
414
+ ```
415
+
416
+ `<iframe src>` of a known host becomes `data-ck` + `data-src` with `src`
417
+ removed. The categories come from the same HOST_DB/PATH_DB as the browser
418
+ engine: `tools/export-hostdb.mjs` generates
419
+ `plugins/wordpress/consentkit/includes/hostdb.php` from `src/ck-core.js`, and
420
+ `test/hostdb.test.mjs` fails if the two drift.
421
+
422
+ What it skips: ConsentKit's own assets, tags carrying `data-ck-ignore`, tags
423
+ already marked up by hand, inline scripts (there is no URL to defer), the GTM
424
+ container, and anything inside comments, `<pre>` or `<textarea>`. On any error
425
+ the page is returned unchanged.
426
+
427
+ The `<pre>` / `<textarea>` skip keeps the *source text* byte-identical, which is
428
+ what a page documenting a tracker snippet needs. It does not keep such a tag
429
+ alive: the browser parses `<pre><script src=…>` as a real script element
430
+ whatever the server did, so the runtime engine may still intercept it. Caching plugins are compatible and get the
431
+ already-rewritten HTML, because the rewrite happens at the PHP level before the
432
+ page is cached.
433
+
434
+ Outside WordPress the same idea applies to any server-side template: emit the
435
+ `type="text/plain" data-ck` form directly, as in "Manual markup" above.
436
+
304
437
  ## Google Consent Mode v2
305
438
 
306
439
  With `integrations.gcm` (default), the core pushes `consent: default` with every
@@ -348,23 +481,128 @@ external requests. Rebuild them with `tools/build-inline.mjs` (see
348
481
  [`tools/README.md`](tools/README.md)); each block's header records the exact
349
482
  command that produced it.
350
483
 
351
- ConsentKit 0.3.4, 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:
484
+ ConsentKit 0.4.0, rebuilt 2026-09-05, uncompressed — gzip on the server cuts
485
+ this roughly three- to fourfold. Every block includes the branding extension
486
+ and the attribution line; `--no-branding` drops both the code and the config
487
+ and takes **~24 KB** back off:
354
488
 
355
- | Block | Languages | Bytes |
356
- |---|---|---|
357
- | `ready/en-bar.txt` | en | 102,128 |
358
- | `ready/ru-bar.txt` | ru, en | 102,191 |
359
- | `ready/ru-box.txt` | ru, en | 102,206 |
360
- | `ready/ru-box-right.txt` | ru, en | 102,215 |
361
- | `ready/ru-modal.txt` | ru, en | 102,199 |
362
- | `ready/eu-bar.txt` | 34 languages | 152,403 |
489
+ | Block | Languages | Bytes | gzip | `--no-branding` |
490
+ |---|---|---|---|---|
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.
363
501
 
364
502
  Size is driven almost entirely by the bundled languages: `en` and `ru` are
365
503
  built into the UI and cost nothing extra, while layout, position, theme and
366
504
  accent change only a few bytes of config.
367
505
 
506
+ The debug panel is **not** in these numbers. Blocks carry a ~5.1 KB loader
507
+ (`src/ck-debug-loader.js`) which fetches the 33 KB panel only when someone opens
508
+ the page with `?ck_debug=1` — see [Debug mode](#debug-mode). An ordinary visitor
509
+ downloads the loader and nothing more.
510
+
511
+ ## Debug mode
512
+
513
+ A panel that shows what the client actually did on a live page — useful when a
514
+ site owner asks "is this thing working?" and screenshots of the banner do not
515
+ answer it.
516
+
517
+ **Open it** by adding `?ck_debug=1` to the page URL (`#ck_debug` works too):
518
+
519
+ ```
520
+ https://example.com/?ck_debug=1
521
+ ```
522
+
523
+ A dark, monospace panel appears bottom-right in its own Shadow DOM. The flag is
524
+ remembered in `localStorage` for that browser, so it survives navigation.
525
+
526
+ **It shows**, in order: the client version, config source (SaaS or inline),
527
+ siteId and policy version / ETag; the consent status, granted categories,
528
+ decision time and cookie lifetime; what the blocking engine is holding back
529
+ (`ConsentKit._blocked()`); which requests to known trackers actually left the
530
+ page, each marked *before* or *after* consent; the recent `ck_*` dataLayer
531
+ events and `gtag('consent', …)` calls; and three buttons — reset consent
532
+ (withdraw, clear the cookie, reload), open preferences, and copy a JSON report.
533
+
534
+ The request list carries the same caveat as the blocking engine itself:
535
+ requests that left **before** ck-core.js parsed — a plain `<script src>` written
536
+ into the HTML — show up there but could not have been blocked. Mark those tags
537
+ up manually.
538
+
539
+ **Turn it off** with `?ck_debug=0`, or the × in the panel's header. The loader
540
+ owns the stored flag, so `?ck_debug=0` clears it whether or not the panel is on
541
+ the page.
542
+
543
+ **Privacy.** Nothing is sent anywhere: the panel is local to that browser and
544
+ that page. It never renders cookie *values* (names only) and never shows URL
545
+ query strings (host and path only), so the copied report is safe to paste into
546
+ a support ticket.
547
+
548
+ ### How the panel gets onto the page
549
+
550
+ The panel is ~33 KB, and on any given page exactly one person will ever open
551
+ it. So `ready/*.txt` and the WordPress plugin ship **`src/ck-debug-loader.js`**
552
+ (~5.1 KB) instead, and the loader fetches the panel on demand. With no flag set
553
+ the loader creates no DOM, installs no observers and makes **no network
554
+ request** — it costs its own bytes and nothing else.
555
+
556
+ When the panel *is* activated, the loader resolves its URL in this order, first
557
+ match wins:
558
+
559
+ 1. **`window.ConsentKitDebugUrl`**, if you set it — a self-hosted copy, an
560
+ internal mirror, or a pinned build. Set it before the loader runs.
561
+ 2. **`<API_BASE>/client/ck-debug.js`**, when `ck-saas.js` is on the page with a
562
+ site id — `API_BASE` is that loader's `data-ck-api` (its `ConsentKit._saas.api`).
563
+ The panel then comes from the same origin as the config, so a locked-down CSP
564
+ needs no extra host. **A SaaS deployment is expected to serve the panel at
565
+ that path**; if yours does not, set `window.ConsentKitDebugUrl` instead.
566
+ 3. **jsDelivr**, pinned to the running core version:
567
+ `https://cdn.jsdelivr.net/npm/@ecomconsult/consentkit@<version>/src/ck-debug.js`.
568
+ The version comes from `ConsentKit.version`, so the panel can never be newer
569
+ or older than the client it is reporting on. This path is published because
570
+ `package.json` lists `src` in `files`.
571
+
572
+ The script is injected `async`, and neither jsDelivr nor a ConsentKit API host
573
+ is in the blocking engine's tracker list, so the panel loads even while the
574
+ visitor has yet to decide.
575
+
576
+ ### Loading the panel directly
577
+
578
+ If you host the files yourself and would rather have the panel inline, load it
579
+ after `ck-ui.js` (and after `ck-saas.js` if you use it) and skip the loader:
580
+
581
+ ```html
582
+ <script src="/js/ck-core.js"></script>
583
+ <script src="/js/ck-locales.js"></script>
584
+ <script src="/js/ck-ui.js"></script>
585
+ <script src="/js/ck-debug.js"></script>
586
+ ```
587
+
588
+ `ck-debug.js` is self-contained: it repeats the loader's activation check, so it
589
+ works with or without the loader. Do **not** add it to a page that already ships
590
+ the loader (an inline block from `ready/`, or the WordPress plugin) — the loader
591
+ fetches the panel itself, and a second copy would mount a second panel.
592
+
593
+ From npm it is a deliberate opt-in — `@ecomconsult/consentkit` does not pull
594
+ it in for you. Import it yourself, after the UI:
595
+
596
+ ```js
597
+ import '@ecomconsult/consentkit'; // core + locales + UI
598
+ import '@ecomconsult/consentkit/src/ck-debug.js';
599
+ ```
600
+
601
+ **Language.** The panel is Russian or English: it follows the banner's
602
+ configured `language`, falls back to `navigator.language` (`ru-*` → Russian),
603
+ and otherwise renders English. The JSON report it copies is language-neutral
604
+ whichever way the panel reads.
605
+
368
606
  ## TypeScript
369
607
 
370
608
  Types ship with the package; no `@types` needed.
@@ -440,13 +678,19 @@ has been verified and what has not.
440
678
  - `dataLayer` event trace for consent restore, upgrade and withdrawal.
441
679
  - npm entry points and TypeScript types: syntax and import smoke tests in Node
442
680
  without a DOM.
443
- - PHP files of the WordPress plugin pass `php -l` on 7.4 and 8.5.
681
+ - PHP files of the WordPress plugin pass `php -l` on 7.4, 8.3 and 8.5.
682
+ - The server-side rewriting engine has its own suite of 61 cases
683
+ (`plugins/wordpress/consentkit/tests/rewrite.test.php`), green on PHP 7.4,
684
+ 8.3 and 8.5. It is a plain PHP CLI script, so it runs outside `npm test`.
444
685
 
445
686
  ### Not verified — read before production use
446
687
 
447
- - **The WordPress plugin has never run on a live WordPress install.** It passes
448
- linting and review, but no activation, settings round-trip, theme conflict or
449
- multisite behaviour has been observed in a real installation.
688
+ - **The WordPress plugin has been verified on a live install, but only one.**
689
+ It was run on WordPress 7.1 / PHP 8.3 in Docker (activation, settings
690
+ round-trip, shortcode, uninstall, server-side markup end to end). The declared
691
+ floor of WordPress 6.0 / PHP 7.4 has not been exercised live — the PHP files
692
+ pass `php -l` and the rewriting test suite on 7.4, 8.3 and 8.5 — and no theme
693
+ conflict or multisite behaviour has been observed.
450
694
  - **The GTM container has never been through a real import.** The JSON is valid
451
695
  and structurally modelled on the documented export format, but Tag Manager has
452
696
  not accepted it in practice; some field names (notably GA4 config
package/npm/core.cjs CHANGED
@@ -21,7 +21,7 @@ function undecidedState() {
21
21
 
22
22
  function createStub() {
23
23
  return {
24
- version: '0.3.4',
24
+ version: '0.3.5',
25
25
  config: {},
26
26
  init: function () { return undecidedState(); },
27
27
  allowed: function (cat) { return cat === 'necessary'; },
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
@@ -24,7 +24,7 @@ function undecidedState() {
24
24
 
25
25
  function createStub() {
26
26
  return {
27
- version: '0.3.4',
27
+ version: '0.3.5',
28
28
  config: {},
29
29
  init: function () { return undecidedState(); },
30
30
  allowed: function (cat) { return cat === 'necessary'; },
@@ -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
@@ -88,6 +88,20 @@ export interface CkIntegrationsConfig {
88
88
  gtmDataLayer?: boolean;
89
89
  }
90
90
 
91
+ /** v0.4.0 (§2). How much the blocking engine holds back before consent. */
92
+ export interface CkBlockingConfig {
93
+ /**
94
+ * `'known'` (default) blocks what the tracker database recognises.
95
+ * `'strict'` additionally intercepts EVERY third-party `<script src>` and
96
+ * `<iframe src>` that is not same-site, not in `allow`, not in the built-in
97
+ * allowlist (`ConsentKit._baseAllow`) and not an already-granted
98
+ * necessary/functional host. Interceptions are filed under `marketing`.
99
+ */
100
+ mode?: 'known' | 'strict';
101
+ /** Hosts strict mode must never intercept. Suffix match: `p.com` covers `cdn.p.com`. */
102
+ allow?: string[];
103
+ }
104
+
91
105
  /** One declared cookie, shown under its category in the preferences panel. */
92
106
  export interface CkCookieTableEntry {
93
107
  name: string;
@@ -109,6 +123,15 @@ export interface CkConfig {
109
123
  /** Lifetime of the stored decision, in days. Default `365`. */
110
124
  consentTtlDays?: number;
111
125
  integrations?: CkIntegrationsConfig;
126
+ /** v0.4.0. Default `{ mode: 'known', allow: [] }`. */
127
+ blocking?: CkBlockingConfig;
128
+ /**
129
+ * v0.4.0 (§1.3). Extra `host: category` pairs merged into the tracker
130
+ * database. `init()` applies them before its initial scan, so scripts
131
+ * already in the markup are classified against them. In SaaS mode the
132
+ * service supplies this; `_extendHostDb()` does the same at any later point.
133
+ */
134
+ hostdb?: Record<string, CkCategory>;
112
135
  cookieTable?: CkCookieTableEntry[];
113
136
  }
114
137
 
@@ -141,6 +164,50 @@ export interface ConsentKitApi {
141
164
  show(): void;
142
165
  /** Dispatches `ck:ui:close`. */
143
166
  hide(): void;
167
+
168
+ /**
169
+ * v0.4.0 (§1.3). Merges `{ host: category }` into the runtime tracker
170
+ * database and returns how many pairs were accepted. Matching follows the
171
+ * built-in table: a bare domain also covers its subdomains, and an override
172
+ * wins over the shipped classification for the same host.
173
+ *
174
+ * Safe before AND after `init()`. Afterwards, nothing already inserted is
175
+ * re-examined — a script that has loaded cannot be unloaded — but every
176
+ * later insertion is classified against the extended map.
177
+ */
178
+ _extendHostDb(map: Record<string, CkCategory>): number;
179
+
180
+ /**
181
+ * v0.4.0 (§2). The built-in strict-mode allowlist, as hosts plus a few
182
+ * `host/path` entries (reCAPTCHA). A copy: mutating it changes nothing.
183
+ */
184
+ readonly _baseAllow: string[];
185
+
186
+ /** The category the engine would assign to a URL, or `null` if unknown. */
187
+ _categoryForUrl(url: string): CkCategory | null;
188
+
189
+ /** What is currently held back, host+path only — never a query string. */
190
+ _blocked(): CkBlockedEntry[];
191
+ }
192
+
193
+ /** One entry of `ConsentKit._blocked()`. */
194
+ export interface CkBlockedEntry {
195
+ host: string;
196
+ path: string;
197
+ /** `'script'`, `'iframe'`, … */
198
+ kind: string;
199
+ category: CkCategory | null;
200
+ /** `'engine'` — intercepted by the patches; `'markup'` — marked up by hand. */
201
+ origin: 'engine' | 'markup';
202
+ /** v0.4.0. True when strict mode held this back, i.e. the host is unknown. */
203
+ strict: boolean;
204
+ /**
205
+ * v0.4.0. `false` when the category was granted but the element still never
206
+ * loaded — typically a script created and given a `src` without ever being
207
+ * appended, which `applyConsentToDom()` cannot reach. Such entries stay in
208
+ * the report after consent precisely so they can be diagnosed.
209
+ */
210
+ revived: boolean;
144
211
  }
145
212
 
146
213
  declare const ConsentKit: ConsentKitApi;
@@ -156,6 +223,7 @@ export declare function rejectAll(): CkState;
156
223
  export declare function withdraw(): CkState;
157
224
  export declare function show(): void;
158
225
  export declare function hide(): void;
226
+ export declare function _extendHostDb(map: Record<string, CkCategory>): number;
159
227
 
160
228
  declare global {
161
229
  interface Window {
package/npm/index.mjs CHANGED
@@ -3,7 +3,9 @@
3
3
  * re-exports the public API.
4
4
  *
5
5
  * Load order is contractual: ck-core.js (blocking starts at parse time) →
6
- * ck-locales.js (extra language packs) → ck-ui.js (Shadow DOM layer).
6
+ * ck-locales.js (extra language packs) → ck-ui-branding.js (optional logo /
7
+ * attribution extension, which must register before the UI mounts) →
8
+ * ck-ui.js (Shadow DOM layer).
7
9
  *
8
10
  * Only the core is DOM-optional. `src/ck-ui.js` touches `document` at module
9
11
  * scope, so it is imported dynamically behind a `typeof document` guard —
@@ -25,6 +27,13 @@ if (hasDom) {
25
27
  await import('../src/ck-locales.js');
26
28
  } catch (e) { /* locales are optional; ck-ui falls back to built-in en/ru */ }
27
29
 
30
+ // Optional branding extension. Before ck-ui.js on purpose: it registers
31
+ // itself on ConsentKit._uiExtensions and must be there before the first
32
+ // mount(). Absent, the UI simply draws no logo and no attribution line.
33
+ try {
34
+ await import('../src/ck-ui-branding.js');
35
+ } catch (e) { /* branding is optional; the UI renders without it */ }
36
+
28
37
  // Required in the browser, fatal in Node — hence the guard above.
29
38
  try {
30
39
  await import('../src/ck-ui.js');
@@ -42,6 +51,9 @@ export const {
42
51
  rejectAll,
43
52
  withdraw,
44
53
  show,
45
- hide
54
+ hide,
55
+ // v0.4.0 (§1.3): merging service overrides into the tracker database is part
56
+ // of the public surface, so it must be reachable as a named import too.
57
+ _extendHostDb
46
58
  } = ConsentKit;
47
59
  export { ConsentKit };
@@ -28,7 +28,7 @@ export function undecidedState() {
28
28
  */
29
29
  export function createStub() {
30
30
  const stub = {
31
- version: '0.3.4',
31
+ version: '0.4.0',
32
32
  config: {},
33
33
  init: function () { return undecidedState(); },
34
34
  allowed: function (cat) { return cat === 'necessary'; },
@@ -39,6 +39,12 @@ export function createStub() {
39
39
  show: function () {},
40
40
  hide: function () {},
41
41
  _categories: CATEGORIES.slice(),
42
+ // v0.4.0: the stub mirrors the real surface, so consumer code that calls
43
+ // these needs no branching when the core failed to attach.
44
+ _extendHostDb: function () { return 0; },
45
+ _baseAllow: [],
46
+ _categoryForUrl: function () { return null; },
47
+ _blocked: function () { return []; },
42
48
  _isStub: true
43
49
  };
44
50
  return stub;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecomconsult/consentkit",
3
- "version": "0.3.4",
3
+ "version": "0.4.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",