@ecomconsult/consentkit 0.3.2
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/LICENSE +33 -0
- package/README.md +433 -0
- package/npm/core.cjs +67 -0
- package/npm/core.mjs +33 -0
- package/npm/index.cjs +80 -0
- package/npm/index.d.ts +224 -0
- package/npm/index.mjs +47 -0
- package/npm/internal-stub.mjs +83 -0
- package/npm/react.mjs +153 -0
- package/package.json +65 -0
- package/src/ck-core.js +1044 -0
- package/src/ck-locales.js +936 -0
- package/src/ck-saas.js +363 -0
- package/src/ck-ui.js +1421 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 E-COM CONSULT PLUS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
--------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
EXCEPTION: WordPress plugin
|
|
26
|
+
|
|
27
|
+
The WordPress plugin in plugins/wordpress/consentkit/ is distributed under the
|
|
28
|
+
GNU General Public License version 2 or later (GPL-2.0-or-later), as required
|
|
29
|
+
by the WordPress plugin ecosystem. See plugins/wordpress/consentkit/LICENSE
|
|
30
|
+
for its full text.
|
|
31
|
+
|
|
32
|
+
The plugin bundles copies of the MIT-licensed files from src/ in its assets/
|
|
33
|
+
directory; MIT permits this combination.
|
package/README.md
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# ConsentKit
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
GDPR cookie consent for the web: consent state, a blocking engine that stops
|
|
9
|
+
trackers *before* they run, a Shadow DOM banner, and Google Consent Mode v2.
|
|
10
|
+
|
|
11
|
+
Most cookie banners are decoration — the trackers fire on the first frame no
|
|
12
|
+
matter which button you press. ConsentKit blocks at parse time: nothing but
|
|
13
|
+
`necessary` runs until the visitor says so, and no request leaves the page
|
|
14
|
+
before that, including requests to a CDN or a font host.
|
|
15
|
+
|
|
16
|
+
Vanilla ES2020, zero dependencies, no build step.
|
|
17
|
+
|
|
18
|
+
- **Categories:** `necessary` (always on), `functional`, `analytics`, `marketing`
|
|
19
|
+
- **Blocking:** manual markup (`type="text/plain"`) plus automatic interception
|
|
20
|
+
of dynamically injected scripts
|
|
21
|
+
- **UI:** banner (`bar` / `box` / `modal`), preferences panel, floating re-open
|
|
22
|
+
button, light/dark, 30+ locales
|
|
23
|
+
- **SSR-safe:** importing on the server never touches the DOM
|
|
24
|
+
- **Equal-weight buttons, no pre-ticked boxes** — the consent invariants are
|
|
25
|
+
fixed by design, see [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
26
|
+
|
|
27
|
+
> **Status: prototype (v0.3).** The core, the UI and the demo are verified in a
|
|
28
|
+
> browser; several distribution paths are not yet tested against live systems.
|
|
29
|
+
> See [Project status](#project-status) before shipping this to production.
|
|
30
|
+
|
|
31
|
+
**Не программист?** Пошаговая инструкция по-русски, с картинками и разбором по
|
|
32
|
+
кликам: **[INSTALL.ru.md](INSTALL.ru.md)**.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
Four ways to add ConsentKit to a site, from simplest to most integrated.
|
|
37
|
+
|
|
38
|
+
| # | Method | Best for | Docs |
|
|
39
|
+
|---|---|---|---|
|
|
40
|
+
| 1 | **Script tags** — copy `src/` to your server, three `<script>` tags in `<head>` | Any site you control | [Quickstart below](#quickstart--script-tags) |
|
|
41
|
+
| 2 | **npm** — `npm install @ecomconsult/consentkit` | Bundled apps, React | [Quickstart below](#quickstart--npm) |
|
|
42
|
+
| 3 | **WordPress plugin** — copy the plugin folder to `wp-content/plugins/` | WordPress / WooCommerce | [`plugins/wordpress/consentkit/`](plugins/wordpress/consentkit/) |
|
|
43
|
+
| 4 | **Google Tag Manager** — import the container, trigger tags on consent events | Sites already running GTM | [`integrations/gtm/README.md`](integrations/gtm/README.md) |
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npm install @ecomconsult/consentkit
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or drop the files in directly — no bundler required.
|
|
50
|
+
|
|
51
|
+
Site builders that will not let you upload files (free Tilda and similar) need a
|
|
52
|
+
single self-contained `<script>` block instead; the repository ships a generator
|
|
53
|
+
for that, and loading ConsentKit from a third-party CDN is deliberately *not*
|
|
54
|
+
recommended — the CDN would receive the visitor's IP before any consent exists.
|
|
55
|
+
|
|
56
|
+
## Quickstart — script tags
|
|
57
|
+
|
|
58
|
+
Load order is contractual. `ck-core.js` starts blocking at parse time, so it
|
|
59
|
+
must come first and should not be deferred.
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<script src="/consentkit/src/ck-core.js"></script>
|
|
63
|
+
<script src="/consentkit/src/ck-locales.js"></script><!-- optional: extra languages -->
|
|
64
|
+
<script src="/consentkit/src/ck-ui.js"></script>
|
|
65
|
+
<script>
|
|
66
|
+
ConsentKit.init({
|
|
67
|
+
policyVersion: '1',
|
|
68
|
+
language: 'auto',
|
|
69
|
+
layout: { type: 'box', position: 'bottom-left' },
|
|
70
|
+
theme: { accent: '#2B50D8', mode: 'auto' }
|
|
71
|
+
});
|
|
72
|
+
</script>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Quickstart — npm
|
|
76
|
+
|
|
77
|
+
The main entry is a side-effect import: it loads the core, the locales and the
|
|
78
|
+
UI, then re-exports the API.
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
import ConsentKit from '@ecomconsult/consentkit';
|
|
82
|
+
|
|
83
|
+
ConsentKit.init({
|
|
84
|
+
policyVersion: '1',
|
|
85
|
+
layout: { type: 'bar', position: 'bottom' },
|
|
86
|
+
theme: { accent: '#2B50D8', mode: 'auto' }
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (ConsentKit.allowed('analytics')) {
|
|
90
|
+
// start analytics
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
CommonJS works too:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
const ConsentKit = require('@ecomconsult/consentkit');
|
|
98
|
+
ConsentKit.init({ policyVersion: '1' });
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Named exports are available alongside the default:
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
import { init, allowed, getState, accept, rejectAll, withdraw, show } from '@ecomconsult/consentkit';
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Core without the UI
|
|
108
|
+
|
|
109
|
+
`@ecomconsult/consentkit/core` loads the consent engine and blocking only — no banner, no
|
|
110
|
+
locales. Use it when you ship your own interface.
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
import ConsentKit from '@ecomconsult/consentkit/core';
|
|
114
|
+
|
|
115
|
+
ConsentKit.init({ policyVersion: '1' });
|
|
116
|
+
ConsentKit.accept({ analytics: true, marketing: false });
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Import it once
|
|
120
|
+
|
|
121
|
+
`@ecomconsult/consentkit` is a side-effect module and the core is a singleton on the global
|
|
122
|
+
object. Import it at your entry point; importing it again elsewhere is harmless
|
|
123
|
+
but does not create a second instance.
|
|
124
|
+
|
|
125
|
+
## React
|
|
126
|
+
|
|
127
|
+
`react` is an optional peer dependency (`>=17`) — install it yourself.
|
|
128
|
+
`useConsent()` subscribes to the event bus and unsubscribes on unmount.
|
|
129
|
+
|
|
130
|
+
```jsx
|
|
131
|
+
import '@ecomconsult/consentkit'; // side effect: core + locales + UI
|
|
132
|
+
import { useConsent } from '@ecomconsult/consentkit/react';
|
|
133
|
+
|
|
134
|
+
function CookieStatus() {
|
|
135
|
+
const { state, allowed, accept, rejectAll, withdraw, show } = useConsent();
|
|
136
|
+
|
|
137
|
+
if (!state.decided) return <p>Waiting for a choice…</p>;
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div>
|
|
141
|
+
<p>Analytics: {allowed('analytics') ? 'on' : 'off'}</p>
|
|
142
|
+
<button onClick={() => accept('all')}>Accept all</button>
|
|
143
|
+
<button onClick={() => accept({ analytics: true })}>Analytics only</button>
|
|
144
|
+
<button onClick={rejectAll}>Reject all</button>
|
|
145
|
+
<button onClick={withdraw}>Withdraw consent</button>
|
|
146
|
+
<button onClick={show}>Cookie settings</button>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Only mount the analytics-dependent part once consent exists:
|
|
153
|
+
|
|
154
|
+
```jsx
|
|
155
|
+
function Analytics() {
|
|
156
|
+
const { allowed } = useConsent();
|
|
157
|
+
if (!allowed('analytics')) return null;
|
|
158
|
+
return <Tracker />;
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Server rendering
|
|
163
|
+
|
|
164
|
+
`useConsent()` returns `decided: false`, all opt-in categories `false`, and
|
|
165
|
+
no-op actions on the server, then re-renders with the real state after
|
|
166
|
+
hydration. Guard on `state.decided` rather than assuming a value on first paint.
|
|
167
|
+
|
|
168
|
+
## Configuration
|
|
169
|
+
|
|
170
|
+
Pass any subset to `init()`. Nested objects merge with the defaults.
|
|
171
|
+
|
|
172
|
+
| Key | Type | Default | Notes |
|
|
173
|
+
|---|---|---|---|
|
|
174
|
+
| `policyVersion` | `string \| number` | `"1"` | Bump to invalidate stored consent and re-show the banner |
|
|
175
|
+
| `language` | `string` | `"auto"` | `"auto"` reads `navigator.language`. Falls back `pt-BR` → `pt` → `en` |
|
|
176
|
+
| `layout.type` | `"bar" \| "modal" \| "box"` | `"bar"` | `box` is a compact ~360px card |
|
|
177
|
+
| `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 |
|
|
178
|
+
| `theme.accent` | `string` | `"#2B50D8"` | Exposed as `--ck-accent` |
|
|
179
|
+
| `theme.radius` | `string` | `"10px"` | Exposed as `--ck-radius` |
|
|
180
|
+
| `theme.mode` | `"auto" \| "light" \| "dark"` | `"auto"` | `auto` follows `prefers-color-scheme` |
|
|
181
|
+
| `theme.dark` | `{ bg, ink, accent }` | built-in | Overrides the dark palette |
|
|
182
|
+
| `categories.*.enabled` | `boolean` | `true` | Per category: `functional`, `analytics`, `marketing`. Hides the toggle when `false` |
|
|
183
|
+
| `consentTtlDays` | `number` | `365` | Lifetime of the stored decision |
|
|
184
|
+
| `integrations.gcm` | `boolean` | `true` | Google Consent Mode v2 signals |
|
|
185
|
+
| `integrations.gtmDataLayer` | `boolean` | `true` | Push consent events to `window.dataLayer` |
|
|
186
|
+
| `cookieTable` | `CkCookieTableEntry[]` | `[]` | Declared cookies, listed per category in the panel |
|
|
187
|
+
|
|
188
|
+
`cookieTable` entries:
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
{ name: '_ga', category: 'analytics', vendor: 'Google', purpose: 'Visit statistics', expiry: '2 years' }
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## API
|
|
195
|
+
|
|
196
|
+
All methods are safe to call at any time and never throw.
|
|
197
|
+
|
|
198
|
+
| Method | Returns | Description |
|
|
199
|
+
|---|---|---|
|
|
200
|
+
| `init(config?)` | `CkState` | Idempotent. Restores stored consent, then dispatches `ck:init`. Calling again merges config only |
|
|
201
|
+
| `allowed(category)` | `boolean` | `necessary` is always `true` |
|
|
202
|
+
| `getState()` | `CkState` | A fresh object on every call |
|
|
203
|
+
| `accept('all')` | `CkState` | Grants everything. `method: 'accept_all'` |
|
|
204
|
+
| `accept({ ... })` | `CkState` | Per-category choice. `method: 'custom'`. Omitted categories stay denied |
|
|
205
|
+
| `rejectAll()` | `CkState` | Denies every opt-in category. `method: 'reject_all'` |
|
|
206
|
+
| `withdraw()` | `CkState` | Clears storage and known cookies, sends GCM `denied`, resets to `decided: false` |
|
|
207
|
+
| `show()` | `void` | Opens the preferences panel |
|
|
208
|
+
| `hide()` | `void` | Closes the panel |
|
|
209
|
+
| `config` | `CkConfig` | The merged, effective config |
|
|
210
|
+
| `version` | `string` | Core version string |
|
|
211
|
+
|
|
212
|
+
### State
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
{
|
|
216
|
+
decided: false, // false until the visitor chooses — the banner shows while false
|
|
217
|
+
id: null, // uuid of the stored decision
|
|
218
|
+
ts: null, // ISO timestamp
|
|
219
|
+
policyVersion: '1',
|
|
220
|
+
categories: { necessary: true, functional: false, analytics: false, marketing: false },
|
|
221
|
+
method: null // 'accept_all' | 'reject_all' | 'custom'
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Already-loaded scripts are not unloaded by `withdraw()` — cookies are cleared
|
|
226
|
+
and the next page load is clean.
|
|
227
|
+
|
|
228
|
+
## Events
|
|
229
|
+
|
|
230
|
+
All are `CustomEvent` on `document`, with the payload in `detail`.
|
|
231
|
+
|
|
232
|
+
| Event | `detail` | When |
|
|
233
|
+
|---|---|---|
|
|
234
|
+
| `ck:init` | `{ state, config }` | From `init()`, after stored state is restored |
|
|
235
|
+
| `ck:consent` | `{ state }` | The visitor's first choice |
|
|
236
|
+
| `ck:change` | `{ state }` | Any change, including `withdraw()` |
|
|
237
|
+
| `ck:ui:open-preferences` | `{ state, config }` | Command for the UI layer — `show()` dispatches it |
|
|
238
|
+
| `ck:ui:close` | `{ state }` | Command for the UI layer — `hide()` dispatches it |
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
document.addEventListener('ck:change', (e) => {
|
|
242
|
+
const { state } = e.detail;
|
|
243
|
+
if (state.categories.analytics) startAnalytics();
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The core never touches the UI directly; it only dispatches these events, and the
|
|
248
|
+
UI layer only calls the public API.
|
|
249
|
+
|
|
250
|
+
## Blocking trackers
|
|
251
|
+
|
|
252
|
+
### Manual markup
|
|
253
|
+
|
|
254
|
+
Mark a script as `type="text/plain"` with a `data-ck` category. The browser will
|
|
255
|
+
not execute it. Once the category is granted, ConsentKit recreates the element
|
|
256
|
+
with its real type and `src`.
|
|
257
|
+
|
|
258
|
+
```html
|
|
259
|
+
<!-- external -->
|
|
260
|
+
<script type="text/plain" data-ck="marketing" data-src="https://connect.facebook.net/en_US/fbevents.js"></script>
|
|
261
|
+
|
|
262
|
+
<!-- inline -->
|
|
263
|
+
<script type="text/plain" data-ck="analytics">
|
|
264
|
+
console.log('runs only after analytics is granted');
|
|
265
|
+
</script>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Iframes use `data-src`, which is applied once the category is allowed:
|
|
269
|
+
|
|
270
|
+
```html
|
|
271
|
+
<iframe data-ck="marketing" data-src="https://www.youtube.com/embed/VIDEO_ID"
|
|
272
|
+
width="560" height="315" style="background:#e9edf5;border:0"></iframe>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`data-ck` accepts any category name: `functional`, `analytics`, `marketing`.
|
|
276
|
+
|
|
277
|
+
### Automatic blocking
|
|
278
|
+
|
|
279
|
+
Scripts injected at runtime are intercepted without any markup. ConsentKit
|
|
280
|
+
patches `document.createElement`, `Element.prototype.setAttribute` and the
|
|
281
|
+
`HTMLScriptElement.prototype.src` setter at parse time, matching the URL against
|
|
282
|
+
a built-in host list.
|
|
283
|
+
|
|
284
|
+
```js
|
|
285
|
+
// Blocked until analytics is granted, then loaded automatically.
|
|
286
|
+
const s = document.createElement('script');
|
|
287
|
+
s.src = 'https://www.google-analytics.com/analytics.js';
|
|
288
|
+
document.head.appendChild(s);
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Blocked elements are marked `data-ck-blocked` and their URL is remembered, so
|
|
292
|
+
granting consent later loads them without a reload. Recognised hosts include
|
|
293
|
+
Google Analytics, Google Tag Manager, Facebook, Yandex Metrica, Hotjar, TikTok
|
|
294
|
+
and DoubleClick.
|
|
295
|
+
|
|
296
|
+
Because the patches install at parse time, `ck-core.js` must load before any
|
|
297
|
+
tracker — put it first in `<head>` and do not add `defer`.
|
|
298
|
+
|
|
299
|
+
## Google Consent Mode v2
|
|
300
|
+
|
|
301
|
+
With `integrations.gcm` (default), the core pushes `consent: default` with every
|
|
302
|
+
signal `denied` at parse time, then `consent: update` after each choice:
|
|
303
|
+
`analytics` → `analytics_storage`; `marketing` → `ad_storage`, `ad_user_data`,
|
|
304
|
+
`ad_personalization`.
|
|
305
|
+
|
|
306
|
+
## Storage
|
|
307
|
+
|
|
308
|
+
The decision is stored in a `ck_consent` cookie (base64 JSON, `path=/`,
|
|
309
|
+
`SameSite=Lax`, `consentTtlDays`) and mirrored to `localStorage`. It is
|
|
310
|
+
discarded — and the banner shown again — when `policyVersion` changes or the TTL
|
|
311
|
+
expires.
|
|
312
|
+
|
|
313
|
+
## TypeScript
|
|
314
|
+
|
|
315
|
+
Types ship with the package; no `@types` needed.
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
import ConsentKit from '@ecomconsult/consentkit';
|
|
319
|
+
import type { CkConfig, CkState, CkCategory, UseConsentResult } from '@ecomconsult/consentkit';
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`document.addEventListener('ck:change', …)` is typed through a
|
|
323
|
+
`DocumentEventMap` augmentation, so `e.detail.state` resolves.
|
|
324
|
+
|
|
325
|
+
Requires `moduleResolution` of `node16`, `nodenext` or `bundler` — the package
|
|
326
|
+
uses `exports` subpaths, which the legacy `node` resolution cannot read.
|
|
327
|
+
|
|
328
|
+
## Browser support
|
|
329
|
+
|
|
330
|
+
Any browser with Shadow DOM and ES2020: Chrome/Edge 79+, Firefox 72+, Safari
|
|
331
|
+
13.1+. No polyfills, no external fonts or assets.
|
|
332
|
+
|
|
333
|
+
## SaaS mode (experimental)
|
|
334
|
+
|
|
335
|
+
> Not announced, not supported, and not part of any release. The hosted API it
|
|
336
|
+
> talks to does not exist publicly yet. Everything below can change without
|
|
337
|
+
> notice.
|
|
338
|
+
|
|
339
|
+
`src/ck-saas.js` is an optional extra file that fetches the configuration from a
|
|
340
|
+
server instead of taking it from an inline `init()` call, and (optionally) writes
|
|
341
|
+
each consent decision to a journal endpoint. Standalone usage is completely
|
|
342
|
+
unaffected: pages that do not load this file behave exactly as documented above,
|
|
343
|
+
and the prebuilt inline bundles do not contain it.
|
|
344
|
+
|
|
345
|
+
```html
|
|
346
|
+
<script src="ck-core.js"></script>
|
|
347
|
+
<script src="ck-locales.js"></script>
|
|
348
|
+
<script src="ck-ui.js"></script>
|
|
349
|
+
<script src="ck-saas.js" data-ck-id="YOUR_SITE_ID"></script>
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
`ck-saas.js` calls `ConsentKit.init()` itself once it has a configuration, so the
|
|
353
|
+
page must **not** call `init()` as well. `data-ck-api` overrides the API base URL.
|
|
354
|
+
|
|
355
|
+
| Situation | Behaviour |
|
|
356
|
+
|---|---|
|
|
357
|
+
| Config cached in `localStorage` | `init()` runs immediately from cache; the config is revalidated in the background with `If-None-Match`. A changed config applies from the **next** page load. |
|
|
358
|
+
| No cache | Config is fetched with a 3s timeout, then `init()` runs and the result is cached. |
|
|
359
|
+
| Fetch fails, times out, or returns 404 | **Strict fallback**: the banner is shown, every opt-in category stays denied, the journal is disabled, and the reason is logged with `console.warn`. |
|
|
360
|
+
|
|
361
|
+
When the configuration contains a `log` endpoint, each decision is POSTed with
|
|
362
|
+
`fetch(keepalive: true)`, retried once after 2s on a network error, and flushed
|
|
363
|
+
via `sendBeacon` on `pagehide`. Withdrawals are sent with `method: "withdraw"`.
|
|
364
|
+
No other network requests are made.
|
|
365
|
+
|
|
366
|
+
To try it locally, a mock API is included:
|
|
367
|
+
|
|
368
|
+
```sh
|
|
369
|
+
node demo/mock-api.mjs # http://localhost:8788
|
|
370
|
+
# serve the repo root, then open demo/saas.html
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Project status
|
|
374
|
+
|
|
375
|
+
**This is a prototype (v0.3), not a released product.** It is honest about what
|
|
376
|
+
has been verified and what has not.
|
|
377
|
+
|
|
378
|
+
### Verified
|
|
379
|
+
|
|
380
|
+
- Consent core, blocking engine and storage, exercised in a browser against the
|
|
381
|
+
demo shop: no tracker runs and no non-necessary cookie is set before a choice;
|
|
382
|
+
selective consent loads only the matching tracker; `withdraw()` clears cookies.
|
|
383
|
+
- Banner, preferences panel and floating button across `bar` / `box` / `modal`,
|
|
384
|
+
light and dark, with keyboard and ARIA checks.
|
|
385
|
+
- `dataLayer` event trace for consent restore, upgrade and withdrawal.
|
|
386
|
+
- npm entry points and TypeScript types: syntax and import smoke tests in Node
|
|
387
|
+
without a DOM.
|
|
388
|
+
- PHP files of the WordPress plugin pass `php -l` on 7.4 and 8.5.
|
|
389
|
+
|
|
390
|
+
### Not verified — read before production use
|
|
391
|
+
|
|
392
|
+
- **The WordPress plugin has never run on a live WordPress install.** It passes
|
|
393
|
+
linting and review, but no activation, settings round-trip, theme conflict or
|
|
394
|
+
multisite behaviour has been observed in a real installation.
|
|
395
|
+
- **The GTM container has never been through a real import.** The JSON is valid
|
|
396
|
+
and structurally modelled on the documented export format, but Tag Manager has
|
|
397
|
+
not accepted it in practice; some field names (notably GA4 config
|
|
398
|
+
`measurementId` vs `tagId`) may need correction on first import.
|
|
399
|
+
- **Translations beyond `en`, `ru`, `de` and `fr` are drafts.** They are usable
|
|
400
|
+
but have not been reviewed by native speakers. Legal wording — "Reject all",
|
|
401
|
+
"always active" — should be checked by someone who knows the local regulator's
|
|
402
|
+
language before you rely on it.
|
|
403
|
+
- **There is no server-side consent log.** Consent lives only in the visitor's
|
|
404
|
+
browser (cookie plus `localStorage`). GDPR accountability may require you to
|
|
405
|
+
be able to *demonstrate* that consent was given; that record-keeping is not
|
|
406
|
+
part of this prototype and you would have to build it yourself.
|
|
407
|
+
- No automated test suite and no CI beyond the Pages deployment; verification is
|
|
408
|
+
the manual smoke checklist in [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
409
|
+
- Not audited by a lawyer. ConsentKit is a technical building block, not legal
|
|
410
|
+
advice, and it cannot make a site compliant on its own — your privacy policy,
|
|
411
|
+
your cookie inventory and your record-keeping are still yours.
|
|
412
|
+
|
|
413
|
+
### Contributing
|
|
414
|
+
|
|
415
|
+
Structure of the repository, the GDPR invariants that must not change, and how
|
|
416
|
+
to run the checks: [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
417
|
+
|
|
418
|
+
## License
|
|
419
|
+
|
|
420
|
+
Copyright (c) 2026 E-COM CONSULT PLUS.
|
|
421
|
+
|
|
422
|
+
| Part | Licence |
|
|
423
|
+
|---|---|
|
|
424
|
+
| Client (`src/`), npm package, inline builder, demo | [MIT](LICENSE) |
|
|
425
|
+
| WordPress plugin (`plugins/wordpress/consentkit/`) | [GPL-2.0-or-later](plugins/wordpress/consentkit/LICENSE) |
|
|
426
|
+
|
|
427
|
+
The client is MIT so it can be embedded anywhere without licence friction. The
|
|
428
|
+
WordPress plugin ships under GPLv2+ because the WordPress ecosystem effectively
|
|
429
|
+
requires it; MIT permits the plugin to bundle copies of the client in its
|
|
430
|
+
`assets/` directory.
|
|
431
|
+
|
|
432
|
+
Contributions require a `Signed-off-by` line (DCO) — see
|
|
433
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|
package/npm/core.cjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @ecomconsult/consentkit/core — CommonJS entry, core only (no UI, no locales).
|
|
3
|
+
* Present for `exports` symmetry with core.mjs. See index.cjs for why the
|
|
4
|
+
* require() return value is ignored in favour of the global.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
var CATEGORIES = ['necessary', 'functional', 'analytics', 'marketing'];
|
|
10
|
+
|
|
11
|
+
function undecidedState() {
|
|
12
|
+
return {
|
|
13
|
+
decided: false,
|
|
14
|
+
id: null,
|
|
15
|
+
ts: null,
|
|
16
|
+
policyVersion: '1',
|
|
17
|
+
categories: { necessary: true, functional: false, analytics: false, marketing: false },
|
|
18
|
+
method: null
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createStub() {
|
|
23
|
+
return {
|
|
24
|
+
version: '0.2.0',
|
|
25
|
+
config: {},
|
|
26
|
+
init: function () { return undecidedState(); },
|
|
27
|
+
allowed: function (cat) { return cat === 'necessary'; },
|
|
28
|
+
getState: undecidedState,
|
|
29
|
+
accept: function () { return undecidedState(); },
|
|
30
|
+
rejectAll: function () { return undecidedState(); },
|
|
31
|
+
withdraw: function () { return undecidedState(); },
|
|
32
|
+
show: function () {},
|
|
33
|
+
hide: function () {},
|
|
34
|
+
_categories: CATEGORIES.slice(),
|
|
35
|
+
_isStub: true
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The core binds to `window` when it exists, else `globalThis`. Check `window`
|
|
40
|
+
// first: under jsdom / Web Workers / an SSR DOM shim they are different objects
|
|
41
|
+
// and the API lands only on `window`.
|
|
42
|
+
function globalCandidates() {
|
|
43
|
+
var seen = [];
|
|
44
|
+
function push(g) { if (g && seen.indexOf(g) === -1) seen.push(g); }
|
|
45
|
+
try { if (typeof window !== 'undefined') push(window); } catch (e) { /* noop */ }
|
|
46
|
+
try { if (typeof globalThis !== 'undefined') push(globalThis); } catch (e) { /* noop */ }
|
|
47
|
+
try { if (typeof global !== 'undefined') push(global); } catch (e) { /* noop */ }
|
|
48
|
+
try { if (typeof self !== 'undefined') push(self); } catch (e) { /* noop */ }
|
|
49
|
+
return seen;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveApi() {
|
|
53
|
+
var candidates = globalCandidates();
|
|
54
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
55
|
+
var api = candidates[i].ConsentKit;
|
|
56
|
+
if (api && typeof api.getState === 'function') return api;
|
|
57
|
+
}
|
|
58
|
+
return createStub();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try { require('../src/ck-core.js'); } catch (e) { /* fall through to stub */ }
|
|
62
|
+
|
|
63
|
+
var ConsentKit = resolveApi();
|
|
64
|
+
|
|
65
|
+
module.exports = ConsentKit;
|
|
66
|
+
module.exports.ConsentKit = ConsentKit;
|
|
67
|
+
module.exports.default = ConsentKit;
|
package/npm/core.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @ecomconsult/consentkit/core — ESM entry, core only (no UI, no locales).
|
|
3
|
+
*
|
|
4
|
+
* `src/ck-core.js` is a classic side-effect script: it attaches the public API
|
|
5
|
+
* to the global object and (under CommonJS) to `module.exports`. Because this
|
|
6
|
+
* package declares `"type": "module"`, Node parses it as ESM, so the namespace
|
|
7
|
+
* is empty and the global is the only reliable handle. Read it, never the
|
|
8
|
+
* import result.
|
|
9
|
+
*
|
|
10
|
+
* The core is DOM-optional: it guards every `document` / `window` access and
|
|
11
|
+
* works unchanged in Node, so on the server we export the real thing. The stub
|
|
12
|
+
* below is a last-resort fallback for the case where the core failed to attach.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { resolveApi } from './internal-stub.mjs';
|
|
16
|
+
|
|
17
|
+
// Side-effect import: activates blocking at parse time in the browser.
|
|
18
|
+
import '../src/ck-core.js';
|
|
19
|
+
|
|
20
|
+
const ConsentKit = resolveApi();
|
|
21
|
+
|
|
22
|
+
export default ConsentKit;
|
|
23
|
+
export const {
|
|
24
|
+
init,
|
|
25
|
+
allowed,
|
|
26
|
+
getState,
|
|
27
|
+
accept,
|
|
28
|
+
rejectAll,
|
|
29
|
+
withdraw,
|
|
30
|
+
show,
|
|
31
|
+
hide
|
|
32
|
+
} = ConsentKit;
|
|
33
|
+
export { ConsentKit };
|
package/npm/index.cjs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* consentkit — CommonJS entry. Same side effects as index.mjs.
|
|
3
|
+
*
|
|
4
|
+
* `require('../src/ck-core.js')` cannot be trusted for a value: this package is
|
|
5
|
+
* `"type": "module"`, so Node parses the src files as ESM and hands back an
|
|
6
|
+
* empty namespace instead of `module.exports`. The core attaches itself to the
|
|
7
|
+
* global either way, and that is what we read.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
var CATEGORIES = ['necessary', 'functional', 'analytics', 'marketing'];
|
|
13
|
+
|
|
14
|
+
function undecidedState() {
|
|
15
|
+
return {
|
|
16
|
+
decided: false,
|
|
17
|
+
id: null,
|
|
18
|
+
ts: null,
|
|
19
|
+
policyVersion: '1',
|
|
20
|
+
categories: { necessary: true, functional: false, analytics: false, marketing: false },
|
|
21
|
+
method: null
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createStub() {
|
|
26
|
+
return {
|
|
27
|
+
version: '0.2.0',
|
|
28
|
+
config: {},
|
|
29
|
+
init: function () { return undecidedState(); },
|
|
30
|
+
allowed: function (cat) { return cat === 'necessary'; },
|
|
31
|
+
getState: undecidedState,
|
|
32
|
+
accept: function () { return undecidedState(); },
|
|
33
|
+
rejectAll: function () { return undecidedState(); },
|
|
34
|
+
withdraw: function () { return undecidedState(); },
|
|
35
|
+
show: function () {},
|
|
36
|
+
hide: function () {},
|
|
37
|
+
_categories: CATEGORIES.slice(),
|
|
38
|
+
_isStub: true
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The core binds to `window` when it exists, else `globalThis`. Check `window`
|
|
43
|
+
// first: under jsdom / Web Workers / an SSR DOM shim they are different objects
|
|
44
|
+
// and the API lands only on `window`.
|
|
45
|
+
function globalCandidates() {
|
|
46
|
+
var seen = [];
|
|
47
|
+
function push(g) { if (g && seen.indexOf(g) === -1) seen.push(g); }
|
|
48
|
+
try { if (typeof window !== 'undefined') push(window); } catch (e) { /* noop */ }
|
|
49
|
+
try { if (typeof globalThis !== 'undefined') push(globalThis); } catch (e) { /* noop */ }
|
|
50
|
+
try { if (typeof global !== 'undefined') push(global); } catch (e) { /* noop */ }
|
|
51
|
+
try { if (typeof self !== 'undefined') push(self); } catch (e) { /* noop */ }
|
|
52
|
+
return seen;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveApi() {
|
|
56
|
+
var candidates = globalCandidates();
|
|
57
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
58
|
+
var api = candidates[i].ConsentKit;
|
|
59
|
+
if (api && typeof api.getState === 'function') return api;
|
|
60
|
+
}
|
|
61
|
+
return createStub();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 1. Core — required, DOM-optional, starts blocking at parse time.
|
|
65
|
+
try { require('../src/ck-core.js'); } catch (e) { /* fall through to stub */ }
|
|
66
|
+
|
|
67
|
+
var hasDom = typeof document !== 'undefined' && typeof window !== 'undefined';
|
|
68
|
+
|
|
69
|
+
if (hasDom) {
|
|
70
|
+
// 2. Locales — optional language packs.
|
|
71
|
+
try { require('../src/ck-locales.js'); } catch (e) { /* optional */ }
|
|
72
|
+
// 3. UI — touches `document` at module scope, browser only.
|
|
73
|
+
try { require('../src/ck-ui.js'); } catch (e) { /* best effort */ }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
var ConsentKit = resolveApi();
|
|
77
|
+
|
|
78
|
+
module.exports = ConsentKit;
|
|
79
|
+
module.exports.ConsentKit = ConsentKit;
|
|
80
|
+
module.exports.default = ConsentKit;
|