@cookieyes/core 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,58 @@
1
- # @cookieyes/core
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/cookieyes/cookieyes/main/.github/assets/banner-dark.svg">
4
+ <img src="https://raw.githubusercontent.com/cookieyes/cookieyes/main/.github/assets/banner-light.svg" alt="CookieYes consent banner — powered by @cookieyes/core" width="820">
5
+ </picture>
6
+ </p>
2
7
 
3
- The headless consent engine powering the CookieYes SDK. Zero UI, zero runtime dependencies. This is the single source of truth for all consent logic — every framework adapter imports from this package exclusively.
8
+ <h1 align="center">@cookieyes/core</h1>
4
9
 
5
- ## Install
10
+ <p align="center"><strong>The headless consent engine powering the CookieYes SDK.</strong></p>
11
+
12
+ <p align="center">Zero UI, zero runtime dependencies — the single source of truth for all consent logic. Every framework adapter imports from this package.</p>
13
+
14
+ <p align="center">
15
+ <a href="https://www.npmjs.com/package/@cookieyes/core"><img src="https://img.shields.io/npm/v/@cookieyes/core" alt="npm version"></a>
16
+ <a href="https://www.npmjs.com/package/@cookieyes/core"><img src="https://img.shields.io/npm/dw/@cookieyes/core" alt="npm downloads"></a>
17
+ <a href="https://github.com/cookieyes/cookieyes/actions/workflows/ci.yml"><img src="https://github.com/cookieyes/cookieyes/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
18
+ <a href="./LICENSE"><img src="https://img.shields.io/npm/l/@cookieyes/core" alt="license"></a>
19
+ </p>
20
+
21
+ <p align="center">
22
+ <a href="#quick-start">Quick start</a> ·
23
+ <a href="#api">API</a> ·
24
+ <a href="#troubleshooting">Troubleshooting</a> ·
25
+ <a href="https://github.com/cookieyes/cookieyes/blob/main/docs/configuration.md">Docs</a>
26
+ </p>
27
+
28
+ ---
29
+
30
+ ## Building a UI? Use an adapter
31
+
32
+ `@cookieyes/core` is the headless engine — it has **no components**. Most developers want a
33
+ framework adapter, which bundles core plus ready-made banner/dialog UI:
34
+
35
+ - **[`@cookieyes/react`](https://github.com/cookieyes/cookieyes/tree/main/sdk/react)** — React components + hooks.
36
+ - **[`@cookieyes/nextjs`](https://github.com/cookieyes/cookieyes/tree/main/sdk/nextjs)** — Next.js App Router / Pages Router.
37
+
38
+ Use `@cookieyes/core` directly only for vanilla JS, a custom framework, or your own UI.
39
+
40
+ ## Key features
41
+
42
+ - **Headless engine** — all consent logic, no UI, no framework assumptions.
43
+ - **Zero dependencies** — nothing pulled into your bundle but the engine itself.
44
+ - **Offline or self-hosted** — cookie-only, or POST every decision to your own backend.
45
+ - **GDPR & CCPA** — regulation-aware consent state and payloads.
46
+ - **Cookie utilities** — read/write/parse the consent cookie directly if you need to.
47
+
48
+ ## Prerequisites
49
+
50
+ - **Node.js** ≥ 20
51
+ - A JavaScript environment with `document`/`window` (browser or SSR with a DOM). No framework required.
52
+
53
+ ## Quick start
54
+
55
+ **1. Install**
6
56
 
7
57
  ```bash
8
58
  npm install @cookieyes/core
@@ -11,57 +61,66 @@ yarn add @cookieyes/core
11
61
  bun add @cookieyes/core
12
62
  ```
13
63
 
14
- ## Usage
64
+ **2. Initialise the runtime**
65
+
66
+ > **Which API should I use?** `consentStore.subscribe` is the recommended way to
67
+ > read consent outside React. See the [shared decision tree](../../docs/which-api-should-i-use.md)
68
+ > if you're not sure which API applies to your situation — core also exposes a
69
+ > handful of lower-level options (see [Low-level / advanced API](#low-level--advanced-api))
70
+ > for specific edge cases.
15
71
 
16
- The recommended entry point is `getOrCreateConsentRuntime()`. It returns a
17
- process-wide singleton with a `consentStore` (reactive state) and a
18
- `consentManager` (imperative API).
72
+ `initCookieYes()` (an alias of `getOrCreateConsentRuntime()`) returns a process-wide singleton
73
+ with a `consentStore` (reactive state) and a `consentManager` (imperative API).
19
74
 
20
75
  ```ts
21
- import { getOrCreateConsentRuntime } from "@cookieyes/core";
76
+ import { initCookieYes } from "@cookieyes/core";
22
77
 
23
- const { consentManager, consentStore } = getOrCreateConsentRuntime({
24
- mode: "offline", // "offline" (cookie-only) | "self-hosted"
25
- overrides: { regulation: "GDPR" }, // "GDPR" | "CCPA" | "DEFAULT"
26
- colorScheme: "system", // "light" | "dark" | "system"
78
+ const { consentManager, consentStore } = initCookieYes({
79
+ mode: "cookie-only", // "cookie-only" | "self-hosted"
80
+ regulation: "GDPR", // "GDPR" | "CCPA" | "DEFAULT"
81
+ colorScheme: "system", // "light" | "dark" | "system"
27
82
  });
83
+ ```
28
84
 
29
- // React to every saved state change
85
+ **3. React to consent changes**
86
+
87
+ ```ts
88
+ // consentStore.subscribe fires on every state change — category saves,
89
+ // transient preference-dialog toggles, and the dialog opening/closing.
90
+ // That's the right level for "should this script run right now?" checks:
30
91
  const unsubscribe = consentStore.subscribe((state) => {
31
92
  if (state.has("analytics")) {
32
93
  // load analytics scripts (gtag, Mixpanel, …)
33
94
  }
34
- if (state.has("advertisement")) {
35
- // load ad scripts (Meta Pixel, Google Ads, …)
36
- }
37
95
  });
38
96
 
39
- // React only to saved preference changes (not transient UI toggles)
97
+ // Only saved preference changes (not transient UI toggles)
40
98
  consentStore
41
99
  .getState()
42
100
  .subscribeToConsentChanges(({ allowedCategories, deniedCategories }) => {
43
- console.log("Allowed:", allowedCategories);
44
- console.log("Denied:", deniedCategories);
101
+ console.log("Allowed:", allowedCategories, "Denied:", deniedCategories);
45
102
  });
103
+ ```
104
+
105
+ **4. Drive it imperatively**
46
106
 
47
- // Imperative actions
48
- consentStore.getState().has("analytics"); // → boolean
49
- consentStore.getState().saveConsents("all"); // accept all
50
- consentStore.getState().saveConsents("necessary"); // reject all (necessary only)
107
+ ```ts
108
+ consentStore.getState().has("analytics"); // → boolean
109
+ consentStore.getState().saveConsents("all"); // accept all
110
+ consentStore.getState().saveConsents("necessary"); // reject all (necessary only)
51
111
  consentStore.getState().setConsent("analytics", true);
52
112
  consentManager.showPreferences(); // open the preferences dialog
53
113
  consentManager.resetConsent(); // clear + re-prompt
54
-
55
114
  unsubscribe();
56
115
  ```
57
116
 
58
117
  ### Self-hosted mode
59
118
 
60
- Pass `mode: "self-hosted"` with either a `backendURL` (the SDK POSTs a
61
- `ConsentPayload` to it) or a custom `backend` adapter for full control:
119
+ `mode: "self-hosted"` POSTs a `ConsentPayload` to your endpoint on every decision. Provide
120
+ either an `apiUrl` or a custom `backend` adapter for full control:
62
121
 
63
122
  ```ts
64
- getOrCreateConsentRuntime({
123
+ initCookieYes({
65
124
  mode: "self-hosted",
66
125
  backend: {
67
126
  async persist(payload) {
@@ -75,58 +134,373 @@ getOrCreateConsentRuntime({
75
134
  });
76
135
  ```
77
136
 
137
+ ### Deprecated: `mode: "offline"`
138
+
139
+ `"offline"` was renamed to `"cookie-only"` — same behavior, clearer name. It
140
+ still works today and logs a one-time console warning, and will be removed
141
+ 3 releases from now.
142
+
143
+ ```diff
144
+ getOrCreateConsentRuntime({
145
+ - mode: "offline",
146
+ + mode: "cookie-only",
147
+ });
148
+ ```
149
+
78
150
  ## API
79
151
 
80
- ### `getOrCreateConsentRuntime(options)`
152
+ ### `initCookieYes(config)` / `getOrCreateConsentRuntime(config)`
81
153
 
82
- Returns `{ consentManager, consentStore }` (a singleton — call
83
- `resetConsentRuntime()` to clear it, primarily for tests).
154
+ Both accept the canonical `CookieYesConfig` and return `{ consentManager, consentStore }` (a
155
+ singleton — call `resetConsentRuntime()` to clear it, mainly for tests). `initCookieYes` is an
156
+ alias provided so one setup name reads across every package. Every option is documented once in
157
+ **[Configuration](https://github.com/cookieyes/cookieyes/blob/main/docs/configuration.md)**.
158
+ Migrating off the deprecated `overrides.regulation` / `backendURL` keys? See the
159
+ **[migration guide](https://github.com/cookieyes/cookieyes/blob/main/docs/migration/builder-to-config.md)**.
84
160
 
85
- **`options`** (`ConsentRuntimeOptions`):
161
+ **`config`** (`CookieYesConfig`):
86
162
 
87
163
  | Option | Type | Notes |
88
164
  |--------|------|-------|
89
- | `mode` | `"offline" \| "self-hosted"` | **Required.** |
90
- | `backendURL` | `string` | Self-hosted: endpoint the payload is POSTed to. |
165
+ | `mode` | `"cookie-only" \| "self-hosted"` | **Required.** See [Deprecated](#deprecated-mode-offline) for the retired `"offline"` name. |
166
+ | `regulation` | `"GDPR" \| "CCPA" \| "DEFAULT"` | Force the applicable regulation. (The deprecated `overrides.regulation` alias still works.) |
167
+ | `apiUrl` | `string` | Self-hosted: endpoint the payload is POSTed to. (The deprecated `backendURL` alias still works.) |
91
168
  | `backend` | `ConsentBackend` | Self-hosted: custom `persist(payload)` adapter. |
92
169
  | `apiKey` | `string` | Optional auth key. |
93
- | `overrides.regulation` | `"GDPR" \| "CCPA" \| "DEFAULT"` | Force the applicable regulation. |
94
170
  | `colorScheme` | `"light" \| "dark" \| "system"` | |
95
171
  | `theme` | `ThemeConfig` | Color / spacing tokens. |
96
172
  | `i18n` | `I18nConfig` | Translation messages / locale. |
97
173
  | `networkBlocker` | `NetworkBlockerConfig` | Block network requests by category. |
98
174
  | `reloadOnRevoke` | `boolean` | Reload the page when consent is revoked. |
99
- | `onConsentReady` / `onConsentUpdate` | `(state) => void` | Lifecycle callbacks. |
175
+ | `onConsentReady` / `onConsentUpdate` | `(state) => void` | Low-level lifecycle callbacks — see below. |
100
176
 
101
177
  **`consentStore`** — `subscribe(listener)` and `getState()`. State
102
178
  (`ConsentStoreState`) includes `consentId`, `hasActed`, `categories`,
103
179
  `regulation`, `lastRenewed`, `activeUI`, plus the methods `has()`,
104
- `saveConsents()`, `setConsent()`, and `subscribeToConsentChanges()`.
180
+ `saveConsents()`, `setConsent()`, and the low-level `subscribeToConsentChanges()`
181
+ (below).
105
182
 
106
183
  ### `createConsentManager(config)` (low-level)
107
184
 
108
- The underlying manager, if you want to bypass the store. Returns a
109
- `ConsentManager` with:
185
+ The underlying manager, if you want to bypass the store. Returns a `ConsentManager` with state
186
+ (`consentId`, `hasActed`, `categories`, `regulation`, `lastRenewed`, `isPreferencesOpen`) and
187
+ methods (`acceptAll()`, `rejectAll()`, `acceptSelected(cats)`, `updateCategory(cat, val)`,
188
+ `savePreferences()`, `resetConsent()`, `showPreferences()`, `hidePreferences()`, `subscribe(fn)`,
189
+ `registerScript(entry)`).
190
+
191
+ > The applicable regulation comes from your top-level `regulation` config (the deprecated
192
+ > `overrides.regulation` alias still works) and defaults to `"DEFAULT"`. The core engine does not
193
+ > perform IP-based geo-detection.
194
+
195
+ ## Reacting to consent changes
196
+
197
+ To run your own code when a visitor grants or withdraws consent, use
198
+ `consentStore.on(type, listener, options?)`:
199
+
200
+ ```ts
201
+ const { consentStore } = initCookieYes({ mode: "cookie-only" });
202
+
203
+ // "change" fires only when a category actually differs — the right place to
204
+ // (re)load a script, so a re-confirm of the same choices doesn't run it again.
205
+ const off = consentStore.on("change", ({ changedCategories }) => {
206
+ if (changedCategories.includes("analytics")) loadAnalytics();
207
+ });
208
+
209
+ // "save" fires on every save, even an unchanged re-confirm.
210
+ consentStore.on("save", () => toast("Preferences saved"));
211
+
212
+ off(); // stop listening when you're done
213
+ ```
214
+
215
+ - The listener fires **once immediately** with the current state
216
+ (`isInitial: true`), so a late listener isn't blind to earlier choices.
217
+ - Pass `{ category: "analytics" }` to only hear about one category.
218
+ - Payload: `{ categories, changedCategories, isInitial }`.
219
+
220
+ This is the recommended way to react to consent. The paths below still work but
221
+ aren't the primary one.
222
+
223
+ ## Translations & language
224
+
225
+ For a framework-less custom UI, `consentStore` carries the active language and
226
+ lets you switch it live (no reload):
227
+
228
+ ```ts
229
+ const { consentStore } = initCookieYes({
230
+ mode: "cookie-only",
231
+ i18n: { messages: { fr } }, // languages you support; each may be partial
232
+ });
233
+
234
+ consentStore.translations.acceptAll; // text for the active language
235
+ consentStore.getLanguageInfo(); // { language, direction, languages }
236
+
237
+ consentStore.subscribe(() => render()); // re-render on consent OR language change
238
+ await consentStore.setLanguage("fr"); // switch live; loads via i18n.loadLanguage if needed
239
+ ```
240
+
241
+ Missing text falls back to English; custom categories translate by id (see the
242
+ [configuration guide](https://github.com/cookieyes/cookieyes/blob/main/docs/configuration.md)).
243
+ In React, use the `useTranslations()` / `useLanguage()` hooks instead.
244
+
245
+ ## Low-level / advanced API
110
246
 
111
- - **State**: `consentId`, `hasActed`, `categories`, `regulation`, `lastRenewed`, `isPreferencesOpen`
112
- - **Methods**: `acceptAll()`, `rejectAll()`, `acceptSelected(cats)`, `updateCategory(cat, val)`, `savePreferences()`, `resetConsent()`, `showPreferences()`, `hidePreferences()`, `subscribe(fn)`, `registerScript(entry)`
247
+ You shouldn't need these for a typical integration — each exists for a
248
+ specific narrower situation than `consentStore.on` / `consentStore.subscribe`:
113
249
 
114
- `config` (`ConsentConfig`) accepts: `regulation`, `colorScheme`, `theme`,
115
- `apiUrl`, `apiKey`, `backend`, `reloadOnRevoke`, `onConsentReady`,
116
- `onConsentUpdate`.
250
+ | API | Use when |
251
+ |---|---|
252
+ | `subscribeToConsentChanges(listener)` (on `consentStore.getState()`) | Predates `consentStore.on`; fires on saved changes but without the `change`/`save` split, the `isInitial` flag, or per-category filtering. Prefer `on`. |
253
+ | `onConsentReady` (config option) | You need a one-time callback right after the initial state is known — e.g. conditionally loading analytics on first load — rather than an ongoing subscription. |
254
+ | `onConsentUpdate` (config option) | A saved-changes callback registered once at config time instead of dynamically after mount. Prefer `consentStore.on` unless you specifically need a config-time callback. |
255
+ | `createConsentManager(config)` | Bypasses `consentStore` entirely for direct access to the manager: `acceptAll()`, `rejectAll()`, `acceptSelected(cats)`, `updateCategory(cat, val)`, `savePreferences()`, `resetConsent()`, `showPreferences()`, `hidePreferences()`, `subscribe(fn)`, `registerScript(entry)`. `config` (`ConsentConfig`) accepts `regulation`, `colorScheme`, `theme`, `apiUrl`, `apiKey`, `backend`, `reloadOnRevoke`, `onConsentReady`, `onConsentUpdate`. |
256
+ | `parseCookie` / `serializeCookie` | Reading or writing the raw `cookieyes-consent` cookie directly — e.g. in a Next.js Server Component or route handler, where no live runtime or React hooks are available. |
117
257
 
118
- > The applicable regulation comes from your configuration
119
- > (`overrides.regulation` / `config.regulation`) and defaults to `"DEFAULT"`.
120
- > The core engine does not perform IP-based geo-detection.
258
+ ## Stopping tracking when consent is withdrawn
259
+
260
+ When a visitor revokes consent, the SDK stops tracking **without reloading the
261
+ page** — nothing they were doing (form input, scroll position, an open dialog)
262
+ is lost. There are three layers:
263
+
264
+ 1. **Network blocking** (`networkBlocker` / `blockNetwork`) — intercepts
265
+ `fetch`, `XMLHttpRequest`, **and `navigator.sendBeacon`** to blocked domains,
266
+ in real time, for as long as the page is open. `sendBeacon` matters because
267
+ GA4/Meta use it for exit/unload tracking that fetch/XHR interception misses.
268
+ 2. **Integration stop-handlers** (`integrations`) — call a vendor's own
269
+ documented "stop" API on revoke, and resume it on re-accept:
270
+
271
+ ```ts
272
+ getOrCreateConsentRuntime({
273
+ mode: "cookie-only",
274
+ integrations: [
275
+ { vendor: "meta" }, // fbq('consent','revoke'|'grant')
276
+ ],
277
+ });
278
+ ```
279
+
280
+ > **Google Analytics & Tag Manager are handled automatically** — you don't
281
+ > list them here. The SDK broadcasts Google Consent Mode v2 whenever a
282
+ > `dataLayer` is present (see [Google Consent Mode](#google-consent-mode-v2)
283
+ > below). You still set the **deny-by-default** state in your gtag snippet.
284
+ 3. **Your own scripts** (`customStopHandlers`) — for anything without a built-in
285
+ integration. Provide a clean `stop()`/`resume()`, or register it as
286
+ reload-only so revoking it shows the reload notice rather than silently
287
+ continuing to track:
288
+
289
+ ```ts
290
+ customStopHandlers: [
291
+ { id: "my-tool", category: "analytics", stop: () => window.myTool?.disable() },
292
+ { id: "legacy-widget", category: "advertisement", needsReload: true },
293
+ ]
294
+ ```
295
+
296
+ ### Vendor audit — which stop cleanly, which need a reload
297
+
298
+ | Vendor | Runtime stop | How |
299
+ |--------|-------------|-----|
300
+ | **Google Analytics 4 / Tag Manager** | ✅ automatic | Consent Mode v2 broadcast — no `integrations` entry needed (see [below](#google-consent-mode-v2)). |
301
+ | **Meta Pixel** | ✅ clean | `fbq('consent', 'revoke')` / `'grant'` |
302
+ | TikTok Pixel | ⚠️ reload | No runtime stop we could confidently verify; modelled as reload-only. |
303
+ | LinkedIn Insight Tag | ⚠️ reload | No documented runtime opt-out after load. |
304
+ | Hotjar | ⚠️ reload | No documented "stop after load"; gate before load instead. |
305
+ | Segment (analytics.js) | ⚠️ reload | No documented runtime "stop all"; gate `analytics.load()`. |
306
+
307
+ "Reload" vendors surface the reload notice (below) on a genuine revoke — the SDK
308
+ never continues tracking them silently. Any of them can be upgraded to a clean
309
+ stop later (in `resolveBuiltInIntegration`) once a real runtime API is confirmed.
310
+
311
+ ### Reload notice
312
+
313
+ If a revoked tool has no clean runtime stop, the manager computes
314
+ `manager.reloadNotice` (`{ required, reasons }`) automatically on revoke, with
315
+ `manager.dismissReloadNotice()` to clear it. The *state* is automatic; showing
316
+ it is up to you.
317
+
318
+ **If you configure any reload-only tool, surface this state to the visitor** —
319
+ otherwise a revoke that needs a reload is silent and that tool keeps running.
320
+ In React that means rendering the built-in `<ReloadNotice />` (dismissible,
321
+ `role="alert"`, wording via translations); it never reloads on its own. Outside
322
+ React, read `manager.reloadNotice.required` and render your own prompt.
323
+
324
+ ### `reloadOnRevoke` (legacy, off by default)
325
+
326
+ `reloadOnRevoke` performs a full page reload on revoke. It is **off by default**
327
+ — the clean stop-handlers above are the safe path. Turn it on only if you
328
+ explicitly want the old behavior; note it erases whatever the visitor was doing.
121
329
 
122
330
  ## Consent categories
123
331
 
332
+ By default the SDK ships the familiar five:
333
+
124
334
  `necessary` (always on), `functional`, `analytics`, `performance`, `advertisement`.
125
335
 
336
+ Configure nothing and you get exactly these, unchanged.
337
+
338
+ ### Defining your own categories
339
+
340
+ Pass a `categories` array to use your own taxonomy — rename, add, remove, or
341
+ restructure. Each entry is a [`CategoryDef`](./src/categories.ts):
342
+
343
+ ```ts
344
+ getOrCreateConsentRuntime({
345
+ mode: "cookie-only",
346
+ categories: [
347
+ { id: "essential", required: true, label: "Strictly Necessary" },
348
+ { id: "marketing", label: "Marketing & Ads",
349
+ gcm: ["ad_storage", "ad_user_data", "ad_personalization"] },
350
+ { id: "insights", label: "Product Insights",
351
+ gcm: ["analytics_storage"] },
352
+ ],
353
+ });
354
+ ```
355
+
356
+ - **`id`** — the stable key stored in the cookie and used everywhere (banner,
357
+ preferences UI, read APIs, `gate`/integration category names, events). Pick it
358
+ once and keep it stable; renaming an `id` is a taxonomy change (see below).
359
+ - **`required`** — the always-on, non-optional category. **Mark it explicitly** —
360
+ it is *never* inferred from the name `necessary`, so you can rename it freely.
361
+ At least one category must be `required: true`.
362
+ - **`label` / `description`** — shown in the preferences UI. For the five
363
+ built-in ids these fall back to the translation strings if omitted; for a
364
+ custom id with no `label`, the UI falls back to the `id` itself.
365
+ - **`gcm`** — which Google Consent Mode signals this category governs (see
366
+ [below](#google-consent-mode-v2)).
367
+
368
+ **Id rules.** An `id` must be a non-empty string, unique within the list, and
369
+ must not contain `,` or `:` or be one of the cookie's reserved keys (`consentid`,
370
+ `consent`, `action`, `tax`, `lastRenewedDate`) — those would corrupt the stored
371
+ cookie. Otherwise any string is fine (spaces and unicode are OK).
372
+
373
+ **Invalid config is safe.** If the array is empty, has duplicate/reserved/invalid
374
+ ids, or has no `required` category, the SDK logs a `console.warn` and falls back
375
+ to the built-in five rather than leaving you a broken or unprotected banner.
376
+
377
+ ### Changing your taxonomy later (upgrade behaviour)
378
+
379
+ Every stored consent record is stamped with a **taxonomy signature** (a hash of
380
+ the ids, `required` flags, and `gcm` mappings — visible as `taxonomyHash` on the
381
+ snapshot and `tax:` in the cookie). This lets the SDK tell what a returning
382
+ visitor actually agreed to.
383
+
384
+ - **Signature unchanged** → the returning visitor's stored consent is reused
385
+ silently. No re-prompt.
386
+ - **Signature changed** (you renamed/added/removed a category or changed a `gcm`
387
+ mapping) → the SDK **re-requests consent**: it discards the stale record and
388
+ shows the banner again, so the visitor consents against the taxonomy that's
389
+ actually in effect. This is the one documented outcome for a taxonomy change.
390
+ - **Legacy cookies** written before this feature (no `tax:` stamp) are treated
391
+ as the built-in five: if you're still on the default taxonomy they're honoured
392
+ as-is (returning visitors are **never** silently reset by upgrading the SDK);
393
+ if you've since moved to a custom taxonomy they re-request like any other
394
+ change.
395
+
396
+ ## Google Consent Mode v2
397
+
398
+ If a Google `dataLayer` is present on the page, the SDK **broadcasts** all seven
399
+ Consent Mode v2 signals — on load and on every consent change — for every
400
+ visitor. This is what governs Google Analytics 4 and Tag Manager; you do **not**
401
+ register them under `integrations`.
402
+
403
+ Each signal is `granted` when any granted category maps to it (via its `gcm`
404
+ field), otherwise `denied`. `security_storage` is always `granted`. The built-in
405
+ five map like this:
406
+
407
+ | Category | GCM signals |
408
+ |----------|-------------|
409
+ | `necessary` | *(none — `security_storage` is always granted)* |
410
+ | `functional` | `functionality_storage`, `personalization_storage` |
411
+ | `analytics` | `analytics_storage` |
412
+ | `performance` | *(none)* |
413
+ | `advertisement` | `ad_storage`, `ad_user_data`, `ad_personalization` |
414
+
415
+ Under the hood the broadcast does the equivalent of:
416
+
417
+ ```js
418
+ dataLayer.push(["consent", "update", {
419
+ ad_storage: "denied",
420
+ ad_user_data: "denied",
421
+ ad_personalization: "denied",
422
+ analytics_storage: "granted",
423
+ functionality_storage: "granted",
424
+ personalization_storage: "granted",
425
+ security_storage: "granted",
426
+ }]);
427
+ ```
428
+
429
+ > **You still own the default.** Consent Mode requires a **deny-by-default**
430
+ > state set *before* your Google tags load — the SDK can't set it because it
431
+ > doesn't control that load order. Put it in your gtag bootstrap snippet:
432
+ >
433
+ > ```js
434
+ > gtag('consent', 'default', {
435
+ > ad_storage: 'denied',
436
+ > ad_user_data: 'denied',
437
+ > ad_personalization: 'denied',
438
+ > analytics_storage: 'denied',
439
+ > functionality_storage: 'denied',
440
+ > personalization_storage: 'denied',
441
+ > security_storage: 'granted',
442
+ > wait_for_update: 500,
443
+ > });
444
+ > ```
445
+ >
446
+ > Set all seven signals explicitly: deny the six consent-gated ones and grant
447
+ > `security_storage` (it's strictly necessary). Leaving any signal unspecified
448
+ > makes Google treat it as granted until the SDK's `update` fires, leaking it for
449
+ > that first moment. The SDK owns the `update`; you own the `default`.
450
+
451
+ To wire Consent Mode to a **custom** taxonomy, put the `gcm` field on whichever
452
+ of your categories should drive each signal — see the example under
453
+ [Defining your own categories](#defining-your-own-categories) (the `marketing`
454
+ and `insights` entries carry `gcm` mappings). A signal no category maps to
455
+ simply stays `denied`.
456
+
126
457
  ## Cookie
127
458
 
128
459
  Consent is persisted in the `cookieyes-consent` cookie (`SameSite=Lax`, `path=/`).
129
- Use `parseCookie` / `serializeCookie` from this package to read or write it directly.
460
+ It stores each category id as `id:yes|no`, plus a `tax:` stamp recording the
461
+ [taxonomy signature](#changing-your-taxonomy-later-upgrade-behaviour) that was in
462
+ effect when the consent was recorded. Use `parseCookie` / `serializeCookie` from
463
+ this package to read or write it directly.
464
+
465
+ ## Troubleshooting
466
+
467
+ **The runtime isn't initialising (or hooks/consumers see no state).**
468
+ `initCookieYes()` returns a **singleton** — the first call wins, later calls return the same
469
+ instance. Call it once at startup before anything reads consent. In tests, call
470
+ `resetConsentRuntime()` between cases or state leaks across them.
471
+
472
+ **Mode / config type errors.**
473
+ `CookieYesConfig` is a discriminated union on `mode`. Backend keys (`apiUrl`, `apiKey`,
474
+ `backend`) are only valid with `mode: "self-hosted"` — supplying them under `mode: "cookie-only"`
475
+ is a compile error. `mode: "self-hosted"` needs either `apiUrl` or a `backend` adapter.
476
+
477
+ **Consent doesn't persist between reloads.**
478
+ State lives in the `cookieyes-consent` cookie. Confirm it isn't blocked by a browser privacy
479
+ setting or extension, that you're on a `document`-bearing environment (not a bare Node worker),
480
+ and that you aren't calling `resetConsentRuntime()` on every load.
481
+
482
+ Still stuck? [Open an issue](https://github.com/cookieyes/cookieyes/issues).
483
+
484
+ ## Community & support
485
+
486
+ - [Open an issue](https://github.com/cookieyes/cookieyes/issues) — bug reports and feature requests.
487
+ - Email — [support@cookieyes.com](mailto:support@cookieyes.com).
488
+ - [Full documentation](https://github.com/cookieyes/cookieyes/blob/main/docs/configuration.md).
489
+
490
+ _(A community chat channel is on the roadmap.)_
491
+
492
+ ## Contributing
493
+
494
+ Contributions are welcome. Read our
495
+ [Contributing Guidelines](https://github.com/cookieyes/cookieyes/blob/main/CONTRIBUTING.md) and
496
+ [Code of Conduct](https://github.com/cookieyes/cookieyes/blob/main/CODE_OF_CONDUCT.md), then open
497
+ a pull request.
498
+
499
+ ### Security
500
+
501
+ Found a vulnerability? **Do not open a public issue** — follow our
502
+ [Security Policy](https://github.com/cookieyes/cookieyes/blob/main/SECURITY.md) and use GitHub's
503
+ private vulnerability reporting.
130
504
 
131
505
  ## License
132
506
 
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";const e="cookieyes-consent",t=["necessary","functional","analytics","performance","advertisement"];function n(e){const t={},n=e.split(",");for(const e of n){const n=e.indexOf(":");if(-1===n)continue;const r=e.slice(0,n).trim(),s=e.slice(n+1).trim();(r in t||o.has(r))&&(t[r]=s)}return t}const o=new Set(["consentid","consent","action","necessary","functional","analytics","performance","advertisement","lastRenewedDate"]);function r(e){const n=[`consentid:${e.consentId}`,"consent:"+(e.hasActed?"yes":"no"),"action:"+(e.hasActed?"yes":"no")];for(const o of t)n.push(`${o}:${e.categories[o]?"yes":"no"}`);return n.push(`lastRenewedDate:${e.lastRenewed??Date.now()}`),n.join(",")}function s(t){if("undefined"==typeof document)return;const n=encodeURIComponent(r(t));document.cookie=`${e}=${n}; max-age=31536000; path=/; SameSite=Lax`}function a(){const e=new Uint8Array(32);if("undefined"!=typeof crypto&&crypto.getRandomValues)crypto.getRandomValues(e);else for(let t=0;t<e.length;t++)e[t]=Math.floor(256*Math.random());return btoa(String.fromCharCode(...e)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"").slice(0,44)}function c(e,t){const n="CCPA"===t;return{consentId:e,hasActed:!1,categories:{necessary:!0,functional:n,analytics:n,performance:n,advertisement:n},regulation:t}}const i={bannerTitle:"We value your privacy",bannerDescription:"We use cookies to enhance your browsing experience, serve personalised ads or content, and analyse our traffic. By clicking “Accept All”, you consent to our use of cookies.",acceptAll:"Accept All",rejectAll:"Reject All",managePreferences:"Customise",savePreferences:"Save My Preferences",doNotSell:"Do Not Sell or Share My Personal Information",ccpaDescription:"This website or its third-party tools process personal data. You can opt out of the sale of your personal information by clicking on the “Do Not Sell or Share My Personal Information” link.",accept:"Accept",poweredBy:"Powered by CookieYes",preferencesTitle:"Customise Consent Preferences",preferencesIntro:"We use cookies to help you navigate efficiently and perform certain functions. You will find detailed information about all cookies under each consent category below.",categories:{necessary:{label:"Necessary",description:"Necessary cookies are required to enable the basic features of this site, such as providing secure log-in or adjusting your consent preferences. These cookies do not store any personally identifiable data."},functional:{label:"Functional",description:"Functional cookies help perform certain functionalities like sharing the content of the website on social media platforms, collecting feedback, and other third-party features."},analytics:{label:"Analytics",description:"Analytical cookies are used to understand how visitors interact with the website. These cookies help provide information on metrics such as the number of visitors, bounce rate, traffic source, etc."},performance:{label:"Performance",description:"Performance cookies are used to understand and analyse the key performance indexes of the website which helps in delivering a better user experience for the visitors."},advertisement:{label:"Advertisement",description:"Advertisement cookies are used to provide visitors with customised advertisements based on the pages you visited previously and to analyse the effectiveness of the ad campaigns."}},optOut:{title:"Opt-out Preferences",description:'We use third-party cookies that help us analyse how you use this website, store your preferences, and provide the content and advertisements that are relevant to you. However, you can opt out of these cookies by checking "Do Not Sell or Share My Personal Information" and clicking the "Save My Preferences" button. Once you opt out, you can opt in again at any time by unchecking "Do Not Sell or Share My Personal Information" and clicking the "Save My Preferences" button.',cancel:"Cancel",successText:"Your opt-out preference has been honored.",successCountdown:"Banner closes automatically in {seconds} s..."}};const l=new Map,d=new Set,u=new Map;function f(e){if("undefined"!=typeof document)for(const[t,n]of l){const o=!0===e[n.category],r=n.strategy??"afterConsent";if(o){if("lazyOnce"===r&&d.has(t))continue;u.has(t)||p(t,n)}else h(t)}}function p(e,t){if(document.getElementById(e))return;const n=document.createElement("script");n.id=e,n.src=t.src,n.async=!0,t.onLoad&&n.addEventListener("load",t.onLoad,{once:!0}),document.head.appendChild(n),u.set(e,n),d.add(e)}function h(e){const t=u.get(e);t&&(t.remove(),u.delete(e))}function y(e){return{consentId:e.consentId,categories:e.categories,regulation:e.regulation,domain:"undefined"!=typeof window?window.location.hostname:"unknown"}}const g=["necessary","functional","analytics","performance","advertisement"];function m(t){const o=new Set;let r,i,d=!1;const u=function(){if("undefined"==typeof document)return null;const t=document.cookie.split(";");for(const o of t){const t=o.trim(),r=t.indexOf("=");if(-1!==r&&t.slice(0,r).trim()===e){const e=t.slice(r+1).trim();return n(decodeURIComponent(e))}}return null}(),p=t.regulation??"DEFAULT";if(u)r=function(e,t){const n={necessary:!0,functional:"yes"===e.functional,analytics:"yes"===e.analytics,performance:"yes"===e.performance,advertisement:"yes"===e.advertisement};return{consentId:e.consentid??a(),hasActed:"yes"===e.action,categories:n,regulation:t,lastRenewed:e.lastRenewedDate?Number(e.lastRenewedDate):void 0}}(u,p);else{const e=a();r=c(e,p),"CCPA"===r.regulation&&s(r)}function h(){const e={consentId:r.consentId,hasActed:r.hasActed,categories:{...r.categories},regulation:r.regulation,lastRenewed:r.lastRenewed};for(const t of o)t(e);f(r.categories)}function m(){if(r={...r,hasActed:!0,lastRenewed:Date.now()},s(r),t.backend)try{Promise.resolve(t.backend.persist(y(r))).catch(()=>{})}catch{}else t.apiUrl&&async function(e,t,n){const o=y(n),r={"Content-Type":"application/json"};t&&(r.Authorization=`Bearer ${t}`);try{await fetch(e,{method:"POST",headers:r,body:JSON.stringify(o),keepalive:!0})}catch{}}(t.apiUrl,t.apiKey,r);let e=!1;for(const t of g)if(i[t]&&!r.categories[t]){e=!0;break}i={...r.categories},h(),t.onConsentUpdate?.(r),e&&t.reloadOnRevoke&&"undefined"!=typeof window&&window.location.reload()}i={...r.categories},Promise.resolve().then(()=>t.onConsentReady?.(r));const w={get consentId(){return r.consentId},get hasActed(){return r.hasActed},get categories(){return{...r.categories}},get regulation(){return r.regulation},get lastRenewed(){return r.lastRenewed},get isPreferencesOpen(){return d},acceptAll(){r={...r,categories:{necessary:!0,functional:!0,analytics:!0,performance:!0,advertisement:!0}},d=!1,m()},rejectAll(){r={...r,categories:{necessary:!0,functional:!1,analytics:!1,performance:!1,advertisement:!1}},d=!1,m()},acceptSelected(e){const t={...r.categories};for(const n of g)"necessary"!==n&&(t[n]=e.includes(n));r={...r,categories:t},d=!1,m()},updateCategory(e,t){"necessary"!==e&&(r={...r,categories:{...r.categories,[e]:t}},h())},savePreferences(){d=!1,m()},resetConsent(){"undefined"!=typeof document&&(document.cookie=`${e}=; max-age=0; path=/`);const t=a();r=c(t,r.regulation),d=!1,h()},showPreferences(){d=!0,h()},hidePreferences(){d=!1,h()},subscribe:e=>(o.add(e),()=>o.delete(e)),registerScript(e){!function(e){l.set(e.id,e)}(e),f(r.categories)}};return f(r.categories),w}function w(e,t,n,o){let r;try{const e="undefined"!=typeof window?window.location.href:"http://localhost";r=new URL(t,e)}catch{return null}const s=r.hostname.toLowerCase(),a=r.pathname+r.search,c=n.toUpperCase();for(const t of e){const e=t.domain.toLowerCase();if((s===e||s.endsWith("."+e))&&((!t.pathIncludes||a.includes(t.pathIncludes))&&(!t.methods||t.methods.map(e=>e.toUpperCase()).includes(c))&&!o(t.category)))return t}return null}let k=null;function v(e,t){if("undefined"==typeof window)return()=>{};if(k)return()=>{};if(!e.rules.length)return()=>{};const n={originalFetch:window.fetch,originalXhrOpen:XMLHttpRequest.prototype.open,originalXhrSend:XMLHttpRequest.prototype.send};k=n;const o=!1!==e.logBlockedRequests;function r(t){o&&console.warn(`[cookieyes] blocked ${t.method} ${t.url} (rule "${t.rule.id}", category: ${t.rule.category})`),e.onRequestBlocked?.(t)}return window.fetch=function(o,s){let a="",c=s?.method??"GET";"string"==typeof o?a=o:o instanceof URL?a=o.toString():(a=o.url,c=s?.method??o.method);const i=w(e.rules,a,c,t);return i?(r({rule:i,url:a,method:c}),Promise.reject(new TypeError(`Blocked by consent (rule: ${i.id}, category: ${i.category})`))):n.originalFetch.call(window,o,s)},XMLHttpRequest.prototype.open=function(e,t,...o){return this._cyUrl=t.toString(),this._cyMethod=e,n.originalXhrOpen.apply(this,[e,t,...o])},XMLHttpRequest.prototype.send=function(o){const s=this._cyUrl??"",a=this._cyMethod??"GET",c=w(e.rules,s,a,t);return c?(r({rule:c,url:s,method:a}),void this.abort()):n.originalXhrSend.call(this,o)},b}function b(){k&&("undefined"!=typeof window&&(window.fetch=k.originalFetch,XMLHttpRequest.prototype.open=k.originalXhrOpen,XMLHttpRequest.prototype.send=k.originalXhrSend),k=null)}let C=null;exports.createConsentManager=m,exports.defaultTranslations=i,exports.generateConsentId=a,exports.getOrCreateConsentRuntime=function(e){if(C)return C;const t=new Set,n=e.onConsentUpdate,o={};"self-hosted"===e.mode&&(e.backend?o.backend=e.backend:e.backendURL&&(o.apiUrl=e.backendURL)),e.apiKey&&(o.apiKey=e.apiKey),e.overrides?.regulation&&(o.regulation=e.overrides.regulation),e.colorScheme&&(o.colorScheme=e.colorScheme),e.theme&&(o.theme=e.theme),e.reloadOnRevoke&&(o.reloadOnRevoke=e.reloadOnRevoke),e.onConsentReady&&(o.onConsentReady=e.onConsentReady),o.onConsentUpdate=e=>{n?.(e);const o=function(e){const t=[],n=[];for(const o of Object.keys(e))e[o]?t.push(o):n.push(o);return{allowedCategories:t,deniedCategories:n}}(e.categories);for(const e of t)e(o)};const r=m(o);function s(){const e=r.categories;return{consentId:r.consentId,hasActed:r.hasActed,categories:e,consents:e,regulation:r.regulation,lastRenewed:r.lastRenewed,activeUI:r.isPreferencesOpen?"dialog":r.hasActed?null:"banner",has:e=>r.categories[e]??!1,saveConsents:async e=>{"all"===e?r.acceptAll():"necessary"===e?r.rejectAll():r.acceptSelected(e)},setConsent:(e,t)=>r.updateCategory(e,t),subscribeToConsentChanges:e=>(t.add(e),()=>{t.delete(e)})}}const a={subscribe:e=>r.subscribe(()=>e(s())),getState:s};return e.networkBlocker&&e.networkBlocker.rules.length>0&&v(e.networkBlocker,e=>!0===r.categories[e]),C={consentManager:r,consentStore:a},C},exports.installNetworkBlocker=v,exports.parseCookie=n,exports.resetConsentRuntime=function(){C=null},exports.resolveTranslations=function(e){const t=e?.messages??{},n=e?.detectBrowserLanguage??!0,o=[];e?.locale&&o.push(e.locale),n&&"undefined"!=typeof navigator&&navigator.language&&o.push(navigator.language);for(const e of o){const n=e.split("-")[0]?.toLowerCase()??"",o=t[e]??(n?t[n]:void 0);if(o)return o}return t.en??i},exports.serializeCookie=r,exports.uninstallNetworkBlocker=b;
1
+ "use strict";const e="cookieyes-consent",t=new Set(["consentid","consent","action","tax","lastRenewedDate"]);function o(e){const o={categories:{}};for(const n of e.split(",")){const e=n.indexOf(":");if(-1===e)continue;const r=n.slice(0,e).trim(),s=n.slice(e+1).trim();t.has(r)?o[r]=s:r.length>0&&(o.categories[r]=s)}return o}function n(e){const t=[`consentid:${e.consentId}`,"consent:"+(e.hasActed?"yes":"no"),"action:"+(e.hasActed?"yes":"no")];e.taxonomyHash&&t.push(`tax:${e.taxonomyHash}`);for(const[o,n]of Object.entries(e.categories))t.push(`${o}:${n?"yes":"no"}`);return t.push(`lastRenewedDate:${e.lastRenewed??Date.now()}`),t.join(",")}function r(t){if("undefined"==typeof document)return;const o=encodeURIComponent(n(t));document.cookie=`${e}=${o}; max-age=31536000; path=/; SameSite=Lax`}function s(){"undefined"!=typeof document&&(document.cookie=`${e}=; max-age=0; path=/`)}function a(){const e=new Uint8Array(32);if("undefined"!=typeof crypto&&crypto.getRandomValues)crypto.getRandomValues(e);else for(let t=0;t<e.length;t++)e[t]=Math.floor(256*Math.random());return btoa(String.fromCharCode(...e)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"").slice(0,44)}function i(e,t,o){const n="CCPA"===t,r={};for(const e of o.ids)r[e]=!!o.requiredIds.has(e)||n;return{consentId:e,hasActed:!1,categories:r,regulation:t,taxonomyHash:o.taxonomyHash}}const c=[{id:"necessary",required:!0},{id:"functional",gcm:["functionality_storage","personalization_storage"]},{id:"analytics",gcm:["analytics_storage"]},{id:"performance"},{id:"advertisement",gcm:["ad_storage","ad_user_data","ad_personalization"]}];function d(e){let t=2166136261;for(let o=0;o<e.length;o++)t^=e.charCodeAt(o),t=Math.imul(t,16777619);return(t>>>0).toString(36)}function l(e,t){const o=e.map(e=>e.id),n=new Set(e.filter(e=>e.required).map(e=>e.id)),r=e.map(e=>`${e.id}:${e.required?1:0}:${(e.gcm??[]).join("+")}`).join("|");return{list:e,ids:o,requiredIds:n,taxonomyHash:d(r),isDefault:t}}function u(e){if(!e||0===e.length)return l(c,!0);const o=function(e){const o=e.map(e=>e.id);return o.some(e=>"string"!=typeof e||0===e.length)?"every category needs a non-empty string id":o.some(e=>e.includes(",")||e.includes(":"))?"category ids must not contain ',' or ':'":new Set(o).size!==o.length?"category ids must be unique":o.some(e=>t.has(e))?`category ids must not be one of the reserved keys: ${[...t].join(", ")}`:e.some(e=>!0===e.required)?null:"at least one category must be marked { required: true }"}(e);return o?("undefined"!=typeof console&&console.warn(`[cookieyes] Invalid categories config (${o}). Falling back to the default five (necessary, functional, analytics, performance, advertisement).`),l(c,!0)):l(e,!1)}function g(e){"undefined"!=typeof console&&console.warn(e)}function f(e){const t={mode:e.mode},o=e.overrides?.regulation;return void 0!==e.regulation?(t.regulation=e.regulation,void 0!==o&&g("[CookieYes] Received both `regulation` and the deprecated `overrides.regulation`. Using the top-level `regulation` and ignoring `overrides`. Drop the `overrides` object — it is deprecated and will be removed after three release cycles.")):void 0!==o&&(t.regulation=o),void 0!==e.colorScheme&&(t.colorScheme=e.colorScheme),void 0!==e.theme&&(t.theme=e.theme),void 0!==e.i18n&&(t.i18n=e.i18n),void 0!==e.consentCategories&&(t.consentCategories=e.consentCategories),void 0!==e.categories&&(t.categories=e.categories),void 0!==e.networkBlocker&&(t.networkBlocker=e.networkBlocker),void 0!==e.reloadOnRevoke&&(t.reloadOnRevoke=e.reloadOnRevoke),void 0!==e.integrations&&(t.integrations=e.integrations),void 0!==e.customStopHandlers&&(t.customStopHandlers=e.customStopHandlers),void 0!==e.onConsentReady&&(t.onConsentReady=e.onConsentReady),void 0!==e.onConsentUpdate&&(t.onConsentUpdate=e.onConsentUpdate),"self-hosted"===e.mode&&(void 0!==e.apiKey&&(t.apiKey=e.apiKey),void 0!==e.backend&&(t.backend=e.backend),void 0!==e.apiUrl?(t.apiUrl=e.apiUrl,void 0!==e.backendURL&&g("[CookieYes] Received both `apiUrl` and the deprecated `backendURL`. Using `apiUrl` and ignoring `backendURL`. Rename `backendURL` to `apiUrl` — the alias is deprecated and will be removed after three release cycles.")):void 0!==e.backendURL&&(t.apiUrl=e.backendURL)),t}let p=!1;function h(){p||(p=!0,"undefined"!=typeof console&&console.warn('[cookieyes] mode: "offline" has been renamed to "cookie-only". Both do exactly the same thing, but "offline" is deprecated and will be removed in 3 releases. Update to .mode("cookie-only") (or { mode: "cookie-only" }).'))}function y(e){const t={save:new Set,change:new Set};let o={...e()};function n(e,t,o){try{e.listener(o)}catch(e){"undefined"!=typeof console&&console.error(`[cookieyes] a consent "${t}" listener threw; others are unaffected:`,e)}}function r(e,o){for(const r of[...t[e]])r.category&&!o.changedCategories.includes(r.category)||n(r,e,o)}return{on(o,r,s){const a=s?.category?{listener:r,category:s.category}:{listener:r};return t[o].add(a),n(a,o,{categories:{...e()},changedCategories:[],isInitial:!0}),()=>{t[o].delete(a)}},push(e){const t={...e},n=[];for(const e of Object.keys(t))o[e]!==t[e]&&n.push(e);o=t,r("save",{categories:t,changedCategories:n,isInitial:!1}),n.length>0&&r("change",{categories:t,changedCategories:n,isInitial:!1})}}}const m=["ad_storage","ad_user_data","ad_personalization","analytics_storage","functionality_storage","personalization_storage","security_storage"];function w(e,t){const o={};for(const e of m)o[e]="denied";o.security_storage="granted";for(const n of e.list)if(n.gcm&&0!==n.gcm.length&&t[n.id])for(const e of n.gcm)o[e]="granted";return o}function v(e,t){if("undefined"==typeof window||!Array.isArray(window.dataLayer))return;const o=w(e,t),n=window.dataLayer;if(!n)return;!function(){n.push(arguments)}("consent","update",o)}const k={bannerTitle:"We value your privacy",bannerDescription:"We use cookies to enhance your browsing experience, serve personalised ads or content, and analyse our traffic. By clicking “Accept All”, you consent to our use of cookies.",acceptAll:"Accept All",rejectAll:"Reject All",managePreferences:"Customise",savePreferences:"Save My Preferences",doNotSell:"Do Not Sell or Share My Personal Information",ccpaDescription:"This website or its third-party tools process personal data. You can opt out of the sale of your personal information by clicking on the “Do Not Sell or Share My Personal Information” link.",accept:"Accept",poweredBy:"Powered by CookieYes",preferencesTitle:"Customise Consent Preferences",preferencesIntro:"We use cookies to help you navigate efficiently and perform certain functions. You will find detailed information about all cookies under each consent category below.",categories:{necessary:{label:"Necessary",description:"Necessary cookies are required to enable the basic features of this site, such as providing secure log-in or adjusting your consent preferences. These cookies do not store any personally identifiable data."},functional:{label:"Functional",description:"Functional cookies help perform certain functionalities like sharing the content of the website on social media platforms, collecting feedback, and other third-party features."},analytics:{label:"Analytics",description:"Analytical cookies are used to understand how visitors interact with the website. These cookies help provide information on metrics such as the number of visitors, bounce rate, traffic source, etc."},performance:{label:"Performance",description:"Performance cookies are used to understand and analyse the key performance indexes of the website which helps in delivering a better user experience for the visitors."},advertisement:{label:"Advertisement",description:"Advertisement cookies are used to provide visitors with customised advertisements based on the pages you visited previously and to analyse the effectiveness of the ad campaigns."}},optOut:{title:"Opt-out Preferences",description:'We use third-party cookies that help us analyse how you use this website, store your preferences, and provide the content and advertisements that are relevant to you. However, you can opt out of these cookies by checking "Do Not Sell or Share My Personal Information" and clicking the "Save My Preferences" button. Once you opt out, you can opt in again at any time by unchecking "Do Not Sell or Share My Personal Information" and clicking the "Save My Preferences" button.',cancel:"Cancel",successText:"Your opt-out preference has been honored.",successCountdown:"Banner closes automatically in {seconds} s..."},reloadNotice:{message:"Some tracking on this page can only be fully stopped by reloading. Reload to apply your change, or dismiss to keep browsing.",reloadButton:"Reload page",dismissButton:"Dismiss"}},b=new Set(["ar","he","fa","ur","ps","sd","yi","dv"]);function C(e){return e.split("-")[0]?.toLowerCase()??""}function S(e){return b.has(C(e))?"rtl":"ltr"}function R(e,t){if(!t)return e;const o={...e};for(const[n,r]of Object.entries(t)){if(null==r)continue;const t=e[n],s="object"==typeof r&&!Array.isArray(r)&&"object"==typeof t&&null!=t;o[n]=s?R(t,r):r}return o}function x(e){const t=e?.messages??{},o=[];e?.locale&&o.push(e.locale),(e?.detectBrowserLanguage??1)&&"undefined"!=typeof navigator&&navigator.language&&o.push(navigator.language);for(const e of o){if(t[e])return e;const o=C(e);if(o&&t[o])return o}return"en"}function A(e,t){const o={...e?.messages},n=e?.loadLanguage,r=new Set;let s=x(e),a=l(s),i=u();function c(e){return o[e]??o[C(e)]}function d(e){return"en"===C(e)||void 0!==c(e)}function l(e){return R(k,c(e))}function u(){return{language:s,direction:S(s),languages:Array.from(new Set(["en",...Object.keys(o)]))}}function g(e){s=e,a=l(e),i=u(),t()}function f(e,t){r.has(e)||"undefined"==typeof console||(r.add(e),console.warn(`[cookieyes] no translations for language "${e}"; staying on "${s}". Add it to i18n.messages or provide i18n.loadLanguage.`,t??""))}function p(e){return d(e)?(g(e),Promise.resolve()):n?Promise.resolve().then(()=>n(e)).then(t=>{o[e]=t,g(e)}).catch(t=>f(e,t)):(f(e),Promise.resolve())}return n&&e?.locale&&!d(e.locale)&&"undefined"!=typeof window&&p(e.locale),{getTranslations:()=>a,getLanguageInfo:()=>i,setLanguage:p,getCategoryText:function(e){return c(s)?.categories?.[e]}}}const U=new Map,I=new Map;function L(e,t){if(document.getElementById(e))return;const o=document.createElement("script");o.id=e,o.src=t.src,o.async=!0,t.onLoad&&o.addEventListener("load",t.onLoad,{once:!0}),document.head.appendChild(o),I.set(e,o)}function B(e){return"needsReload"in e&&!0===e.needsReload}function H(e){switch(e.vendor){case"meta":return{id:"meta",category:e.category??"advertisement",stop:()=>window.fbq?.("consent","revoke"),resume:()=>window.fbq?.("consent","grant")};case"tiktok":return{id:"tiktok",category:e.category??"advertisement",needsReload:!0};case"linkedin":return{id:"linkedin",category:e.category??"advertisement",needsReload:!0};case"hotjar":return{id:"hotjar",category:e.category??"analytics",needsReload:!0};case"segment":return{id:"segment",category:e.category??"analytics",needsReload:!0}}}const O=new Map,P=new Set,T=new Set;function _(e){O.set(e.id,e)}function q(e){const t=[];for(const o of O.values()){const n=!0!==e[o.category];if(B(o))n?T.has(o.id)&&(t.push(o.id),T.delete(o.id)):T.add(o.id);else if(n){if(!P.has(o.id))try{o.stop(),P.add(o.id)}catch{t.push(o.id)}}else if(P.has(o.id)){P.delete(o.id);try{o.resume?.()}catch{}}}return{reloadRequiredBy:t}}function M(e){return{consentId:e.consentId,categories:e.categories,regulation:e.regulation,domain:"undefined"!=typeof window?window.location.hostname:"unknown"}}function $(t){const n=new Set,c=u(t.categories);let d,l,g,f=!1;function p(e){const t={};for(const o of c.ids)t[o]=!!c.requiredIds.has(o)||e(o);return t}let h=[],y=!1;for(const e of t.integrations??[])_(H(e));for(const e of t.customStopHandlers??[])_(e);const m=function(){if("undefined"==typeof document)return null;const t=document.cookie.split(";");for(const n of t){const t=n.trim(),r=t.indexOf("=");if(-1!==r&&t.slice(0,r).trim()===e){const e=t.slice(r+1).trim();return o(decodeURIComponent(e))}}return null}(),w=t.regulation??"DEFAULT",k=m?.tax,b=k===c.taxonomyHash,C=null!=m&&(b||void 0===k&&c.isDefault);if(null!=m&&C)d=function(e,t,o){const n={};for(const t of o.ids)n[t]=!!o.requiredIds.has(t)||"yes"===e.categories[t];return{consentId:e.consentid??a(),hasActed:"yes"===e.action,categories:n,regulation:t,lastRenewed:e.lastRenewedDate?Number(e.lastRenewedDate):void 0,taxonomyHash:e.tax}}(m,w,c);else{const e=m?.consentid??a();d=i(e,w,c),null!=m&&s(),"CCPA"===d.regulation&&r(d)}function S(){const e={consentId:d.consentId,hasActed:d.hasActed,categories:{...d.categories},regulation:d.regulation,lastRenewed:d.lastRenewed,taxonomyHash:d.taxonomyHash};for(const t of n)t(e)}function R(){!function(e){if("undefined"!=typeof document)for(const[t,o]of U)!0===e[o.category]&&(I.has(t)||L(t,o))}(g)}function x(){if(d={...d,hasActed:!0,lastRenewed:Date.now()},r(d),t.backend)try{Promise.resolve(t.backend.persist(M(d))).catch(()=>{})}catch{}else t.apiUrl&&async function(e,t,o){const n=M(o),r={"Content-Type":"application/json"};t&&(r.Authorization=`Bearer ${t}`);try{await fetch(e,{method:"POST",headers:r,body:JSON.stringify(n),keepalive:!0})}catch{}}(t.apiUrl,t.apiKey,d);let e=!1;for(const t of c.ids)if(l[t]&&!d.categories[t]){e=!0;break}l={...d.categories},g={...d.categories},R();const{reloadRequiredBy:o}=q(g);var n;((n=o).length!==h.length||n.some((e,t)=>e!==h[t]))&&(h=n,y=!1),v(c,g),S(),t.onConsentUpdate?.(d),e&&t.reloadOnRevoke&&"undefined"!=typeof window&&window.location.reload()}d={...d,taxonomyHash:c.taxonomyHash},l={...d.categories},g={...d.categories},Promise.resolve().then(()=>t.onConsentReady?.(d));const A={get consentId(){return d.consentId},get hasActed(){return d.hasActed},get categories(){return{...d.categories}},get committedCategories(){return{...g}},get regulation(){return d.regulation},get lastRenewed(){return d.lastRenewed},get taxonomyHash(){return d.taxonomyHash},get isPreferencesOpen(){return f},acceptAll(){d={...d,categories:p(()=>!0)},f=!1,x()},rejectAll(){d={...d,categories:p(()=>!1)},f=!1,x()},acceptSelected(e){d={...d,categories:p(t=>e.includes(t))},f=!1,x()},updateCategory(e,t){c.requiredIds.has(e)||c.ids.includes(e)&&(d={...d,categories:{...d.categories,[e]:t}},S())},savePreferences(){f=!1,x()},resetConsent(){s();const e=a();d=i(e,d.regulation,c),g={...d.categories},l={...d.categories},f=!1,q(g),v(c,g),h=[],y=!1,S()},showPreferences(){f=!0,S()},hidePreferences(){f=!1,S()},subscribe:e=>(n.add(e),()=>n.delete(e)),registerScript(e){!function(e){U.set(e.id,e)}(e),R()},get reloadNotice(){return{required:h.length>0&&!y,reasons:[...h]}},dismissReloadNotice(){y||(y=!0,S())}};return R(),function(e){for(const t of O.values()){const o=!0!==e[t.category];if(B(t))o?T.delete(t.id):T.add(t.id);else try{o?(t.stop(),P.add(t.id)):(P.delete(t.id),t.resume?.())}catch{}}}(d.categories),v(c,d.categories),A}function j(e,t,o,n){let r;try{const e="undefined"!=typeof window?window.location.href:"http://localhost";r=new URL(t,e)}catch{return null}const s=r.hostname.toLowerCase(),a=r.pathname+r.search,i=o.toUpperCase();for(const t of e){const e=t.domain.toLowerCase();if((s===e||s.endsWith("."+e))&&((!t.pathIncludes||a.includes(t.pathIncludes))&&(!t.methods||t.methods.map(e=>e.toUpperCase()).includes(i))&&!n(t.category)))return t}return null}let D=null;function N(e,t){if("undefined"==typeof window)return()=>{};if(D)return()=>{};if(!e.rules.length)return()=>{};const o={originalFetch:window.fetch,originalXhrOpen:XMLHttpRequest.prototype.open,originalXhrSend:XMLHttpRequest.prototype.send,originalSendBeacon:"undefined"!=typeof navigator&&"function"==typeof navigator.sendBeacon?navigator.sendBeacon:void 0};D=o;const n=!1!==e.logBlockedRequests;function r(t){n&&console.warn(`[cookieyes] blocked ${t.method} ${t.url} (rule "${t.rule.id}", category: ${t.rule.category})`),e.onRequestBlocked?.(t)}if(window.fetch=function(n,s){let a="",i=s?.method??"GET";"string"==typeof n?a=n:n instanceof URL?a=n.toString():(a=n.url,i=s?.method??n.method);const c=j(e.rules,a,i,t);return c?(r({rule:c,url:a,method:i}),Promise.reject(new TypeError(`Blocked by consent (rule: ${c.id}, category: ${c.category})`))):o.originalFetch.call(window,n,s)},XMLHttpRequest.prototype.open=function(e,t,...n){return this._cyUrl=t.toString(),this._cyMethod=e,o.originalXhrOpen.apply(this,[e,t,...n])},XMLHttpRequest.prototype.send=function(n){const s=this._cyUrl??"",a=this._cyMethod??"GET",i=j(e.rules,s,a,t);return i?(r({rule:i,url:s,method:a}),void this.abort()):o.originalXhrSend.call(this,n)},o.originalSendBeacon){const n=o.originalSendBeacon;navigator.sendBeacon=function(o,s){const a=j(e.rules,o.toString(),"POST",t);return a?(r({rule:a,url:o.toString(),method:"POST"}),!0):n.call(navigator,o,s)}}return X}function X(){D&&("undefined"!=typeof window&&(window.fetch=D.originalFetch,XMLHttpRequest.prototype.open=D.originalXhrOpen,XMLHttpRequest.prototype.send=D.originalXhrSend,D.originalSendBeacon&&(navigator.sendBeacon=D.originalSendBeacon)),D=null)}let E=null;function z(e){if(E)return E;"offline"===e.mode&&h();const t=f(e),o=new Set,n=t.onConsentUpdate;let r;const s={};"self-hosted"===t.mode&&(t.backend?s.backend=t.backend:t.apiUrl&&(s.apiUrl=t.apiUrl)),t.apiKey&&(s.apiKey=t.apiKey),t.regulation&&(s.regulation=t.regulation),t.colorScheme&&(s.colorScheme=t.colorScheme),t.theme&&(s.theme=t.theme),t.reloadOnRevoke&&(s.reloadOnRevoke=t.reloadOnRevoke),t.integrations&&(s.integrations=t.integrations),t.customStopHandlers&&(s.customStopHandlers=t.customStopHandlers),t.categories&&(s.categories=t.categories),t.onConsentReady&&(s.onConsentReady=t.onConsentReady),s.onConsentUpdate=e=>{n?.(e),r.push(e.categories);const t=function(e){const t=[],o=[];for(const n of Object.keys(e))e[n]?t.push(n):o.push(n);return{allowedCategories:t,deniedCategories:o}}(e.categories);for(const e of o)e(t)};const a=$(s);r=y(()=>a.committedCategories);const i=u(t.categories),c=new Set;function d(){const e=g();for(const t of c)t(e)}a.subscribe(d);const l=A(t.i18n,d);function g(){const e=a.categories;return{consentId:a.consentId,hasActed:a.hasActed,categories:e,consents:e,committedConsents:a.committedCategories,regulation:a.regulation,lastRenewed:a.lastRenewed,taxonomyHash:a.taxonomyHash,activeUI:a.isPreferencesOpen?"dialog":a.hasActed?null:"banner",has:e=>!0===a.committedCategories[e],saveConsents:async e=>{"all"===e?a.acceptAll():"necessary"===e?a.rejectAll():a.acceptSelected(e)},setConsent:(e,t)=>a.updateCategory(e,t),subscribeToConsentChanges:e=>(o.add(e),()=>{o.delete(e)})}}const p={subscribe:e=>(c.add(e),()=>{c.delete(e)}),getState:g,on:(e,t,o)=>r.on(e,t,o),get translations(){return l.getTranslations()},getLanguageInfo:l.getLanguageInfo,setLanguage:l.setLanguage,getCategoryText:l.getCategoryText,categories:i};return t.networkBlocker&&t.networkBlocker.rules.length>0&&N(t.networkBlocker,e=>!0===a.committedCategories[e]),E={consentManager:a,consentStore:p},E}exports.DEFAULT_CATEGORIES=c,exports._clearStopHandlers=function(){O.clear(),P.clear(),T.clear()},exports._normalizeConfig=f,exports._resetOfflineModeWarning=function(){p=!1},exports._warnOfflineModeDeprecated=h,exports.broadcastGoogleConsent=v,exports.computeGoogleConsent=w,exports.createConsentEmitter=y,exports.createConsentManager=$,exports.createLanguageController=A,exports.defaultTranslations=k,exports.generateConsentId=a,exports.getOrCreateConsentRuntime=z,exports.getTextDirection=S,exports.initCookieYes=function(e){return z(e)},exports.installNetworkBlocker=N,exports.mergeTranslations=R,exports.parseCookie=o,exports.pickLanguage=x,exports.primaryOf=C,exports.registerStopHandler=_,exports.resetConsentRuntime=function(){E=null},exports.resolveBuiltInIntegration=H,exports.resolveCategories=u,exports.resolveTranslations=function(e){const t=e?.messages??{},o=x(e);return R(k,t[o]??t[C(o)])},exports.serializeCookie=n,exports.uninstallNetworkBlocker=X;
2
2
  //# sourceMappingURL=index.cjs.map