@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 +117 -19
- package/npm/index.d.ts +160 -2
- package/npm/internal-stub.mjs +6 -1
- package/package.json +1 -1
- package/src/ck-core.js +146 -13
- package/src/ck-debug.js +120 -1
- package/src/ck-ui.js +489 -43
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
|
|
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.
|
|
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
|
|
364
|
-
(`
|
|
365
|
-
`
|
|
366
|
-
|
|
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.
|
|
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 |
|
|
492
|
-
| `ready/ru-bar.txt` | ru, ro, en |
|
|
493
|
-
| `ready/ru-box.txt` | ru, ro, en |
|
|
494
|
-
| `ready/ru-box-right.txt` | ru, ro, en |
|
|
495
|
-
| `ready/ru-modal.txt` | ru, ro, en |
|
|
496
|
-
| `ready/eu-bar.txt` | 34 languages |
|
|
497
|
-
|
|
498
|
-
0.
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
/**
|
|
66
|
-
|
|
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()`. */
|
package/npm/internal-stub.mjs
CHANGED
|
@@ -28,7 +28,7 @@ export function undecidedState() {
|
|
|
28
28
|
*/
|
|
29
29
|
export function createStub() {
|
|
30
30
|
const stub = {
|
|
31
|
-
version: '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
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
|
-
|
|
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
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
|
|
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.
|
|
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
|
|
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
|