@cookieyes/core 0.1.0 → 0.2.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 +371 -47
- package/dist/index.cjs +1 -595
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +375 -26
- package/dist/index.js +1 -584
- package/dist/index.js.map +1 -1
- package/package.json +9 -10
- package/dist/index.d.cts +0 -210
package/README.md
CHANGED
|
@@ -1,8 +1,58 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
8
|
+
<h1 align="center">@cookieyes/core</h1>
|
|
4
9
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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 {
|
|
76
|
+
import { initCookieYes } from "@cookieyes/core";
|
|
22
77
|
|
|
23
|
-
const { consentManager, consentStore } =
|
|
24
|
-
mode: "
|
|
25
|
-
|
|
26
|
-
colorScheme: "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
|
+
```
|
|
84
|
+
|
|
85
|
+
**3. React to consent changes**
|
|
28
86
|
|
|
29
|
-
|
|
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
|
-
//
|
|
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
|
+
```
|
|
46
104
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
consentStore.getState().
|
|
105
|
+
**4. Drive it imperatively**
|
|
106
|
+
|
|
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
|
-
|
|
61
|
-
`
|
|
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
|
-
|
|
123
|
+
initCookieYes({
|
|
65
124
|
mode: "self-hosted",
|
|
66
125
|
backend: {
|
|
67
126
|
async persist(payload) {
|
|
@@ -75,58 +134,323 @@ 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(
|
|
152
|
+
### `initCookieYes(config)` / `getOrCreateConsentRuntime(config)`
|
|
81
153
|
|
|
82
|
-
|
|
83
|
-
`resetConsentRuntime()` to clear it,
|
|
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
|
-
**`
|
|
161
|
+
**`config`** (`CookieYesConfig`):
|
|
86
162
|
|
|
87
163
|
| Option | Type | Notes |
|
|
88
164
|
|--------|------|-------|
|
|
89
|
-
| `mode` | `"
|
|
90
|
-
| `
|
|
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` |
|
|
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
|
-
`
|
|
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
|
+
## Low-level / advanced API
|
|
196
|
+
|
|
197
|
+
You shouldn't need these for a typical integration — each exists for a
|
|
198
|
+
specific narrower situation than `consentStore.subscribe`:
|
|
199
|
+
|
|
200
|
+
| API | Use when |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `subscribeToConsentChanges(listener)` (on `consentStore.getState()`) | You only care about *saved* consent decisions (accept/reject/save), not every transient toggle or dialog open/close that `subscribe` also reports. |
|
|
203
|
+
| `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. |
|
|
204
|
+
| `onConsentUpdate` (config option) | Like `subscribeToConsentChanges`, scoped to saved changes only, but registered once at config time instead of dynamically after mount. Prefer `subscribeToConsentChanges` unless you specifically need a config-time callback. |
|
|
205
|
+
| `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`. |
|
|
206
|
+
| `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. |
|
|
207
|
+
|
|
208
|
+
## Stopping tracking when consent is withdrawn
|
|
209
|
+
|
|
210
|
+
When a visitor revokes consent, the SDK stops tracking **without reloading the
|
|
211
|
+
page** — nothing they were doing (form input, scroll position, an open dialog)
|
|
212
|
+
is lost. There are three layers:
|
|
213
|
+
|
|
214
|
+
1. **Network blocking** (`networkBlocker` / `blockNetwork`) — intercepts
|
|
215
|
+
`fetch`, `XMLHttpRequest`, **and `navigator.sendBeacon`** to blocked domains,
|
|
216
|
+
in real time, for as long as the page is open. `sendBeacon` matters because
|
|
217
|
+
GA4/Meta use it for exit/unload tracking that fetch/XHR interception misses.
|
|
218
|
+
2. **Integration stop-handlers** (`integrations`) — call a vendor's own
|
|
219
|
+
documented "stop" API on revoke, and resume it on re-accept:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
getOrCreateConsentRuntime({
|
|
223
|
+
mode: "cookie-only",
|
|
224
|
+
integrations: [
|
|
225
|
+
{ vendor: "meta" }, // fbq('consent','revoke'|'grant')
|
|
226
|
+
],
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
> **Google Analytics & Tag Manager are handled automatically** — you don't
|
|
231
|
+
> list them here. The SDK broadcasts Google Consent Mode v2 whenever a
|
|
232
|
+
> `dataLayer` is present (see [Google Consent Mode](#google-consent-mode-v2)
|
|
233
|
+
> below). You still set the **deny-by-default** state in your gtag snippet.
|
|
234
|
+
3. **Your own scripts** (`customStopHandlers`) — for anything without a built-in
|
|
235
|
+
integration. Provide a clean `stop()`/`resume()`, or register it as
|
|
236
|
+
reload-only so revoking it shows the reload notice rather than silently
|
|
237
|
+
continuing to track:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
customStopHandlers: [
|
|
241
|
+
{ id: "my-tool", category: "analytics", stop: () => window.myTool?.disable() },
|
|
242
|
+
{ id: "legacy-widget", category: "advertisement", needsReload: true },
|
|
243
|
+
]
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Vendor audit — which stop cleanly, which need a reload
|
|
247
|
+
|
|
248
|
+
| Vendor | Runtime stop | How |
|
|
249
|
+
|--------|-------------|-----|
|
|
250
|
+
| **Google Analytics 4 / Tag Manager** | ✅ automatic | Consent Mode v2 broadcast — no `integrations` entry needed (see [below](#google-consent-mode-v2)). |
|
|
251
|
+
| **Meta Pixel** | ✅ clean | `fbq('consent', 'revoke')` / `'grant'` |
|
|
252
|
+
| TikTok Pixel | ⚠️ reload | No runtime stop we could confidently verify; modelled as reload-only. |
|
|
253
|
+
| LinkedIn Insight Tag | ⚠️ reload | No documented runtime opt-out after load. |
|
|
254
|
+
| Hotjar | ⚠️ reload | No documented "stop after load"; gate before load instead. |
|
|
255
|
+
| Segment (analytics.js) | ⚠️ reload | No documented runtime "stop all"; gate `analytics.load()`. |
|
|
110
256
|
|
|
111
|
-
|
|
112
|
-
|
|
257
|
+
"Reload" vendors surface the reload notice (below) on a genuine revoke — the SDK
|
|
258
|
+
never continues tracking them silently. Any of them can be upgraded to a clean
|
|
259
|
+
stop later (in `resolveBuiltInIntegration`) once a real runtime API is confirmed.
|
|
113
260
|
|
|
114
|
-
|
|
115
|
-
`apiUrl`, `apiKey`, `backend`, `reloadOnRevoke`, `onConsentReady`,
|
|
116
|
-
`onConsentUpdate`.
|
|
261
|
+
### Reload notice
|
|
117
262
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
263
|
+
If a revoked tool has no clean runtime stop, the manager computes
|
|
264
|
+
`manager.reloadNotice` (`{ required, reasons }`) automatically on revoke, with
|
|
265
|
+
`manager.dismissReloadNotice()` to clear it. The *state* is automatic; showing
|
|
266
|
+
it is up to you.
|
|
267
|
+
|
|
268
|
+
**If you configure any reload-only tool, surface this state to the visitor** —
|
|
269
|
+
otherwise a revoke that needs a reload is silent and that tool keeps running.
|
|
270
|
+
In React that means rendering the built-in `<ReloadNotice />` (dismissible,
|
|
271
|
+
`role="alert"`, wording via translations); it never reloads on its own. Outside
|
|
272
|
+
React, read `manager.reloadNotice.required` and render your own prompt.
|
|
273
|
+
|
|
274
|
+
### `reloadOnRevoke` (legacy, off by default)
|
|
275
|
+
|
|
276
|
+
`reloadOnRevoke` performs a full page reload on revoke. It is **off by default**
|
|
277
|
+
— the clean stop-handlers above are the safe path. Turn it on only if you
|
|
278
|
+
explicitly want the old behavior; note it erases whatever the visitor was doing.
|
|
121
279
|
|
|
122
280
|
## Consent categories
|
|
123
281
|
|
|
282
|
+
By default the SDK ships the familiar five:
|
|
283
|
+
|
|
124
284
|
`necessary` (always on), `functional`, `analytics`, `performance`, `advertisement`.
|
|
125
285
|
|
|
286
|
+
Configure nothing and you get exactly these, unchanged.
|
|
287
|
+
|
|
288
|
+
### Defining your own categories
|
|
289
|
+
|
|
290
|
+
Pass a `categories` array to use your own taxonomy — rename, add, remove, or
|
|
291
|
+
restructure. Each entry is a [`CategoryDef`](./src/categories.ts):
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
getOrCreateConsentRuntime({
|
|
295
|
+
mode: "cookie-only",
|
|
296
|
+
categories: [
|
|
297
|
+
{ id: "essential", required: true, label: "Strictly Necessary" },
|
|
298
|
+
{ id: "marketing", label: "Marketing & Ads",
|
|
299
|
+
gcm: ["ad_storage", "ad_user_data", "ad_personalization"] },
|
|
300
|
+
{ id: "insights", label: "Product Insights",
|
|
301
|
+
gcm: ["analytics_storage"] },
|
|
302
|
+
],
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
- **`id`** — the stable key stored in the cookie and used everywhere (banner,
|
|
307
|
+
preferences UI, read APIs, `gate`/integration category names, events). Pick it
|
|
308
|
+
once and keep it stable; renaming an `id` is a taxonomy change (see below).
|
|
309
|
+
- **`required`** — the always-on, non-optional category. **Mark it explicitly** —
|
|
310
|
+
it is *never* inferred from the name `necessary`, so you can rename it freely.
|
|
311
|
+
At least one category must be `required: true`.
|
|
312
|
+
- **`label` / `description`** — shown in the preferences UI. For the five
|
|
313
|
+
built-in ids these fall back to the translation strings if omitted; for a
|
|
314
|
+
custom id with no `label`, the UI falls back to the `id` itself.
|
|
315
|
+
- **`gcm`** — which Google Consent Mode signals this category governs (see
|
|
316
|
+
[below](#google-consent-mode-v2)).
|
|
317
|
+
|
|
318
|
+
**Id rules.** An `id` must be a non-empty string, unique within the list, and
|
|
319
|
+
must not contain `,` or `:` or be one of the cookie's reserved keys (`consentid`,
|
|
320
|
+
`consent`, `action`, `tax`, `lastRenewedDate`) — those would corrupt the stored
|
|
321
|
+
cookie. Otherwise any string is fine (spaces and unicode are OK).
|
|
322
|
+
|
|
323
|
+
**Invalid config is safe.** If the array is empty, has duplicate/reserved/invalid
|
|
324
|
+
ids, or has no `required` category, the SDK logs a `console.warn` and falls back
|
|
325
|
+
to the built-in five rather than leaving you a broken or unprotected banner.
|
|
326
|
+
|
|
327
|
+
### Changing your taxonomy later (upgrade behaviour)
|
|
328
|
+
|
|
329
|
+
Every stored consent record is stamped with a **taxonomy signature** (a hash of
|
|
330
|
+
the ids, `required` flags, and `gcm` mappings — visible as `taxonomyHash` on the
|
|
331
|
+
snapshot and `tax:` in the cookie). This lets the SDK tell what a returning
|
|
332
|
+
visitor actually agreed to.
|
|
333
|
+
|
|
334
|
+
- **Signature unchanged** → the returning visitor's stored consent is reused
|
|
335
|
+
silently. No re-prompt.
|
|
336
|
+
- **Signature changed** (you renamed/added/removed a category or changed a `gcm`
|
|
337
|
+
mapping) → the SDK **re-requests consent**: it discards the stale record and
|
|
338
|
+
shows the banner again, so the visitor consents against the taxonomy that's
|
|
339
|
+
actually in effect. This is the one documented outcome for a taxonomy change.
|
|
340
|
+
- **Legacy cookies** written before this feature (no `tax:` stamp) are treated
|
|
341
|
+
as the built-in five: if you're still on the default taxonomy they're honoured
|
|
342
|
+
as-is (returning visitors are **never** silently reset by upgrading the SDK);
|
|
343
|
+
if you've since moved to a custom taxonomy they re-request like any other
|
|
344
|
+
change.
|
|
345
|
+
|
|
346
|
+
## Google Consent Mode v2
|
|
347
|
+
|
|
348
|
+
If a Google `dataLayer` is present on the page, the SDK **broadcasts** all seven
|
|
349
|
+
Consent Mode v2 signals — on load and on every consent change — for every
|
|
350
|
+
visitor. This is what governs Google Analytics 4 and Tag Manager; you do **not**
|
|
351
|
+
register them under `integrations`.
|
|
352
|
+
|
|
353
|
+
Each signal is `granted` when any granted category maps to it (via its `gcm`
|
|
354
|
+
field), otherwise `denied`. `security_storage` is always `granted`. The built-in
|
|
355
|
+
five map like this:
|
|
356
|
+
|
|
357
|
+
| Category | GCM signals |
|
|
358
|
+
|----------|-------------|
|
|
359
|
+
| `necessary` | *(none — `security_storage` is always granted)* |
|
|
360
|
+
| `functional` | `functionality_storage`, `personalization_storage` |
|
|
361
|
+
| `analytics` | `analytics_storage` |
|
|
362
|
+
| `performance` | *(none)* |
|
|
363
|
+
| `advertisement` | `ad_storage`, `ad_user_data`, `ad_personalization` |
|
|
364
|
+
|
|
365
|
+
Under the hood the broadcast does the equivalent of:
|
|
366
|
+
|
|
367
|
+
```js
|
|
368
|
+
dataLayer.push(["consent", "update", {
|
|
369
|
+
ad_storage: "denied",
|
|
370
|
+
ad_user_data: "denied",
|
|
371
|
+
ad_personalization: "denied",
|
|
372
|
+
analytics_storage: "granted",
|
|
373
|
+
functionality_storage: "granted",
|
|
374
|
+
personalization_storage: "granted",
|
|
375
|
+
security_storage: "granted",
|
|
376
|
+
}]);
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
> **You still own the default.** Consent Mode requires a **deny-by-default**
|
|
380
|
+
> state set *before* your Google tags load — the SDK can't set it because it
|
|
381
|
+
> doesn't control that load order. Put it in your gtag bootstrap snippet:
|
|
382
|
+
>
|
|
383
|
+
> ```js
|
|
384
|
+
> gtag('consent', 'default', {
|
|
385
|
+
> ad_storage: 'denied',
|
|
386
|
+
> ad_user_data: 'denied',
|
|
387
|
+
> ad_personalization: 'denied',
|
|
388
|
+
> analytics_storage: 'denied',
|
|
389
|
+
> functionality_storage: 'denied',
|
|
390
|
+
> personalization_storage: 'denied',
|
|
391
|
+
> security_storage: 'granted',
|
|
392
|
+
> wait_for_update: 500,
|
|
393
|
+
> });
|
|
394
|
+
> ```
|
|
395
|
+
>
|
|
396
|
+
> Set all seven signals explicitly: deny the six consent-gated ones and grant
|
|
397
|
+
> `security_storage` (it's strictly necessary). Leaving any signal unspecified
|
|
398
|
+
> makes Google treat it as granted until the SDK's `update` fires, leaking it for
|
|
399
|
+
> that first moment. The SDK owns the `update`; you own the `default`.
|
|
400
|
+
|
|
401
|
+
To wire Consent Mode to a **custom** taxonomy, put the `gcm` field on whichever
|
|
402
|
+
of your categories should drive each signal — see the example under
|
|
403
|
+
[Defining your own categories](#defining-your-own-categories) (the `marketing`
|
|
404
|
+
and `insights` entries carry `gcm` mappings). A signal no category maps to
|
|
405
|
+
simply stays `denied`.
|
|
406
|
+
|
|
126
407
|
## Cookie
|
|
127
408
|
|
|
128
409
|
Consent is persisted in the `cookieyes-consent` cookie (`SameSite=Lax`, `path=/`).
|
|
129
|
-
|
|
410
|
+
It stores each category id as `id:yes|no`, plus a `tax:` stamp recording the
|
|
411
|
+
[taxonomy signature](#changing-your-taxonomy-later-upgrade-behaviour) that was in
|
|
412
|
+
effect when the consent was recorded. Use `parseCookie` / `serializeCookie` from
|
|
413
|
+
this package to read or write it directly.
|
|
414
|
+
|
|
415
|
+
## Troubleshooting
|
|
416
|
+
|
|
417
|
+
**The runtime isn't initialising (or hooks/consumers see no state).**
|
|
418
|
+
`initCookieYes()` returns a **singleton** — the first call wins, later calls return the same
|
|
419
|
+
instance. Call it once at startup before anything reads consent. In tests, call
|
|
420
|
+
`resetConsentRuntime()` between cases or state leaks across them.
|
|
421
|
+
|
|
422
|
+
**Mode / config type errors.**
|
|
423
|
+
`CookieYesConfig` is a discriminated union on `mode`. Backend keys (`apiUrl`, `apiKey`,
|
|
424
|
+
`backend`) are only valid with `mode: "self-hosted"` — supplying them under `mode: "cookie-only"`
|
|
425
|
+
is a compile error. `mode: "self-hosted"` needs either `apiUrl` or a `backend` adapter.
|
|
426
|
+
|
|
427
|
+
**Consent doesn't persist between reloads.**
|
|
428
|
+
State lives in the `cookieyes-consent` cookie. Confirm it isn't blocked by a browser privacy
|
|
429
|
+
setting or extension, that you're on a `document`-bearing environment (not a bare Node worker),
|
|
430
|
+
and that you aren't calling `resetConsentRuntime()` on every load.
|
|
431
|
+
|
|
432
|
+
Still stuck? [Open an issue](https://github.com/cookieyes/cookieyes/issues).
|
|
433
|
+
|
|
434
|
+
## Community & support
|
|
435
|
+
|
|
436
|
+
- [Open an issue](https://github.com/cookieyes/cookieyes/issues) — bug reports and feature requests.
|
|
437
|
+
- Email — [support@cookieyes.com](mailto:support@cookieyes.com).
|
|
438
|
+
- [Full documentation](https://github.com/cookieyes/cookieyes/blob/main/docs/configuration.md).
|
|
439
|
+
|
|
440
|
+
_(A community chat channel is on the roadmap.)_
|
|
441
|
+
|
|
442
|
+
## Contributing
|
|
443
|
+
|
|
444
|
+
Contributions are welcome. Read our
|
|
445
|
+
[Contributing Guidelines](https://github.com/cookieyes/cookieyes/blob/main/CONTRIBUTING.md) and
|
|
446
|
+
[Code of Conduct](https://github.com/cookieyes/cookieyes/blob/main/CODE_OF_CONDUCT.md), then open
|
|
447
|
+
a pull request.
|
|
448
|
+
|
|
449
|
+
### Security
|
|
450
|
+
|
|
451
|
+
Found a vulnerability? **Do not open a public issue** — follow our
|
|
452
|
+
[Security Policy](https://github.com/cookieyes/cookieyes/blob/main/SECURITY.md) and use GitHub's
|
|
453
|
+
private vulnerability reporting.
|
|
130
454
|
|
|
131
455
|
## License
|
|
132
456
|
|