@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 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
+ ![status: prototype v0.3](https://img.shields.io/badge/status-prototype%20v0.3-orange)
4
+ ![license: MIT](https://img.shields.io/badge/license-MIT-blue)
5
+ ![dependencies: 0](https://img.shields.io/badge/dependencies-0-brightgreen)
6
+ ![no build step](https://img.shields.io/badge/build-none-lightgrey)
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;