@consenti/ui 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 +325 -117
- package/dist/angular.d.cts +233 -34
- package/dist/angular.d.ts +233 -34
- package/dist/angular.js +1 -73
- package/dist/angular.js.map +1 -1
- package/dist/angular.mjs +1 -52
- package/dist/angular.mjs.map +1 -1
- package/dist/chunk-K2UN3O67.mjs +2 -0
- package/dist/{chunk-ARALKXMB.mjs.map → chunk-K2UN3O67.mjs.map} +1 -1
- package/dist/hmac-UKMRMKN6.mjs +2 -0
- package/dist/{hmac-N6H7F2K2.mjs.map → hmac-UKMRMKN6.mjs.map} +1 -1
- package/dist/http-UU2NIW6M.mjs +2 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +217 -25
- package/dist/index.mjs +1 -2220
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +1 -2304
- package/dist/index.umd.js.map +1 -1
- package/dist/react.d.cts +233 -29
- package/dist/react.d.ts +233 -29
- package/dist/react.js +1 -60
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -39
- package/dist/react.mjs.map +1 -1
- package/dist/testing.js +1 -94
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +1 -73
- package/dist/testing.mjs.map +1 -1
- package/dist/vue.d.cts +244 -53
- package/dist/vue.d.ts +244 -53
- package/dist/vue.js +1 -72
- package/dist/vue.js.map +1 -1
- package/dist/vue.mjs +1 -51
- package/dist/vue.mjs.map +1 -1
- package/package.json +13 -5
- package/dist/chunk-ARALKXMB.mjs +0 -20
- package/dist/hmac-N6H7F2K2.mjs +0 -26
- package/dist/http-VHOGZ6ZJ.mjs +0 -7
- /package/dist/{http-VHOGZ6ZJ.mjs.map → http-UU2NIW6M.mjs.map} +0 -0
package/README.md
CHANGED
|
@@ -8,12 +8,13 @@ Zero-dependency, GDPR-compliant cookie consent widget for any web framework.
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
## Why
|
|
11
|
+
## Why Consenti?
|
|
12
12
|
|
|
13
13
|
- **Zero runtime dependencies** — browser built-ins only (`fetch`, `crypto.subtle`, `BroadcastChannel`, `CustomEvent`, `document.cookie`, `localStorage`)
|
|
14
|
-
- **Fully standalone or API-connected** — works offline with
|
|
15
|
-
- **Multi-regulation** — GDPR,
|
|
16
|
-
- **
|
|
14
|
+
- **Fully standalone or API-connected** — works offline with pre-built profiles, or connects to `@consenti/api` for dashboard-managed profiles
|
|
15
|
+
- **Multi-regulation** — GDPR, CCPA, CPRA, LGPD, DPDPA, PIPL, and more via 8 built-in compliance groups
|
|
16
|
+
- **Automatic geo-resolution** — detects visitor jurisdiction client-side (timezone + language) or server-side (`/resolve-profile`)
|
|
17
|
+
- **GPC aware** — Global Privacy Control signal detection with `'honor'` / `'strict'` / `'ignore'` modes per profile
|
|
17
18
|
- **GTM / Google Consent Mode v2** — built-in `dataLayer` integration
|
|
18
19
|
- **SSR-safe** — all browser API access is guarded; `new ConsentiSetup()` during SSR is a silent no-op
|
|
19
20
|
- **Fully typed** — ships TypeScript definitions; no `@types` needed
|
|
@@ -48,7 +49,7 @@ import '@consenti/ui/dist/index.css'
|
|
|
48
49
|
<script src="https://cdn.jsdelivr.net/npm/@consenti/ui/dist/index.umd.js"></script>
|
|
49
50
|
<script>
|
|
50
51
|
const { ConsentiSetup } = ConsentiUI
|
|
51
|
-
new ConsentiSetup({
|
|
52
|
+
new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
52
53
|
</script>
|
|
53
54
|
```
|
|
54
55
|
|
|
@@ -57,7 +58,7 @@ import '@consenti/ui/dist/index.css'
|
|
|
57
58
|
```html
|
|
58
59
|
<script type="module">
|
|
59
60
|
import { ConsentiSetup } from 'https://esm.sh/@consenti/ui'
|
|
60
|
-
new ConsentiSetup({
|
|
61
|
+
new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
61
62
|
</script>
|
|
62
63
|
```
|
|
63
64
|
|
|
@@ -74,8 +75,16 @@ import '@consenti/ui/dist/index.css'
|
|
|
74
75
|
## Quick Start
|
|
75
76
|
|
|
76
77
|
```ts
|
|
77
|
-
// Simplest possible —
|
|
78
|
-
new ConsentiSetup({
|
|
78
|
+
// Simplest possible — auto-detects jurisdiction from browser timezone + language
|
|
79
|
+
new ConsentiSetup({})
|
|
80
|
+
|
|
81
|
+
// Fixed GDPR (EU opt-in) profile, no backend required
|
|
82
|
+
new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
83
|
+
|
|
84
|
+
// API-backed — server geo-resolves the right profile per visitor
|
|
85
|
+
new ConsentiSetup({
|
|
86
|
+
api: { enabled: true, baseUrl: 'https://your-backend.com' },
|
|
87
|
+
})
|
|
79
88
|
```
|
|
80
89
|
|
|
81
90
|
A consent banner appears on first visit. Everything below is optional.
|
|
@@ -101,20 +110,27 @@ import { ConsentiSetup } from '@consenti/ui'
|
|
|
101
110
|
import '@consenti/ui/dist/index.css'
|
|
102
111
|
|
|
103
112
|
const widget = new ConsentiSetup({
|
|
104
|
-
// ──
|
|
113
|
+
// ── Core (optional) ──────────────────────────────────────────────────────────
|
|
105
114
|
core: {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
storage: 'cookie',
|
|
111
|
-
cookieDomains: '.example.com',
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
allowReceipt: true,
|
|
116
|
-
disableCssTemplate: false,
|
|
115
|
+
tenantId: 'acme', // identifies your tenant's profiles; default: 'default'
|
|
116
|
+
locale: 'en', // BCP 47; 'auto' = navigator.language; default: 'auto'
|
|
117
|
+
autoHonorGPC: true, // explicit operator override: false | true | 'strict'
|
|
118
|
+
// overrides profile.gpcMode when set
|
|
119
|
+
storage: 'cookie', // 'cookie' | 'localStorage'; default: 'cookie'
|
|
120
|
+
cookieDomains: '.example.com', // comma-separated; first entry used as Domain attribute
|
|
121
|
+
cookieSigningKey: 'min-32-char-secret', // HMAC-SHA256 signing; implicit from presence
|
|
122
|
+
allowReceipt: true, // allow consent receipt download; default: false
|
|
123
|
+
disableCssTemplate: false, // skip all style injection; default: false
|
|
117
124
|
userId: 'server-assigned-uuid', // authenticated users — enables cross-device sync
|
|
125
|
+
|
|
126
|
+
// Pre-built profile fallback
|
|
127
|
+
usePrebuiltProfiles: 'all', // 'all' (default) | ['opt-in', 'opt-out', ...] (non-empty)
|
|
128
|
+
// controls which of the 8 built-in profiles are available
|
|
129
|
+
// as fallbacks when the API is unavailable
|
|
130
|
+
cacheResolvedProfiles: true, // cache /resolve-profile response in sessionStorage; default: true
|
|
131
|
+
console: ['error'], // log levels emitted: 'info' | 'log' | 'warning' | 'error'
|
|
132
|
+
// default: ['error'] only — no noise in production
|
|
133
|
+
|
|
118
134
|
theme: {
|
|
119
135
|
bgColor: '#ffffff',
|
|
120
136
|
textColor: '#1a1a1a',
|
|
@@ -136,6 +152,28 @@ const widget = new ConsentiSetup({
|
|
|
136
152
|
},
|
|
137
153
|
},
|
|
138
154
|
|
|
155
|
+
// ── Compliance routing (optional) ─────────────────────────────────────────
|
|
156
|
+
compliance: {
|
|
157
|
+
// 'auto' → geo-resolve per visitor (default)
|
|
158
|
+
// api.enabled=true → server resolves via /resolve-profile
|
|
159
|
+
// api.enabled=false → resolves client-side from timezone + language
|
|
160
|
+
// ComplianceGroupId → fixed group for all visitors (skip geo)
|
|
161
|
+
// localProfileType → use a locally registered ConsentiProfile
|
|
162
|
+
type: 'auto',
|
|
163
|
+
|
|
164
|
+
// Client-side geo resolver (used only when api.enabled = false)
|
|
165
|
+
// 'default' → browser timezone + navigator.language heuristic (built-in, zero deps)
|
|
166
|
+
// WidgetCountryResolverFn → custom async function: () => Promise<{ country, region, confidence }>
|
|
167
|
+
geoDataProvider: 'default',
|
|
168
|
+
|
|
169
|
+
// Age gate (optional)
|
|
170
|
+
ageGate: {
|
|
171
|
+
enabled: false,
|
|
172
|
+
minimumAge: 13,
|
|
173
|
+
requireParentalConsent: false,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
|
|
139
177
|
// ── Mount point (optional) ────────────────────────────────────────────────
|
|
140
178
|
rootEl: '#my-consent-wrapper', // CSS selector or HTMLElement; default: appends to body
|
|
141
179
|
|
|
@@ -147,6 +185,9 @@ const widget = new ConsentiSetup({
|
|
|
147
185
|
enabled: true,
|
|
148
186
|
baseUrl: 'https://your-site.com', // default: window.location.origin
|
|
149
187
|
authToken: '', // sent as Authorization: Bearer <token>
|
|
188
|
+
tenantId: 'acme', // overrides core.tenantId for API calls
|
|
189
|
+
complianceGroup: 'opt-in', // pin a specific group on the API side (Scenario 2B)
|
|
190
|
+
trustDomain: false, // true = bypass allowedOrigins check (use in dev only)
|
|
150
191
|
},
|
|
151
192
|
|
|
152
193
|
// ── GTM / Google Consent Mode v2 (optional) ───────────────────────────────
|
|
@@ -172,45 +213,109 @@ const widget = new ConsentiSetup({
|
|
|
172
213
|
|
|
173
214
|
---
|
|
174
215
|
|
|
175
|
-
##
|
|
176
|
-
|
|
177
|
-
| Value | Region | Model | Notes |
|
|
178
|
-
|--------------|---------------------|---------------------|------------------------------------------------------------------------|
|
|
179
|
-
| `'gdpr'` | EU / EEA | Opt-in | Default. Banner on first visit; all non-mandatory cookies denied until granted. |
|
|
180
|
-
| `'uk-gdpr'` | UK | Opt-in | Same model as GDPR; ICO-enforced; age gate at 13. |
|
|
181
|
-
| `'ccpa'` | California (US) | Opt-out | All cookies default to `'granted'`; consent written silently; no banner. |
|
|
182
|
-
| `'cpra'` | California (US) | Opt-out / Opt-in | Supersedes CCPA. Opt-out for sale/sharing; opt-in for sensitive data. GPC triggers Do Not Sell + Do Not Share. |
|
|
183
|
-
| `'lgpd'` | Brazil | Opt-in | 10 lawful bases; ANPD-enforced; parental consent gate under-12. |
|
|
184
|
-
| `'dpdpa'` | India | Opt-in | Fiduciary name + grievance officer in modal; GPC ignored. |
|
|
185
|
-
| `'pipeda'` | Canada | Opt-in | PIPEDA + Quebec Law 25 (GDPR-aligned). |
|
|
186
|
-
| `'popia'` | South Africa | Opt-in | 8 processing conditions; Information Regulator-enforced. |
|
|
187
|
-
| `'pdpa-th'` | Thailand | Opt-in | Cross-border transfer rules; PDPC-enforced. |
|
|
188
|
-
| `'appi'` | Japan | Opt-in / Opt-out | Opt-in for sensitive data and overseas transfers; opt-out for general third-party sharing. |
|
|
189
|
-
| `'kvkk'` | Turkey | Opt-in | Explicit consent for sensitive personal data; KVK Board-enforced. |
|
|
216
|
+
## Compliance Groups
|
|
190
217
|
|
|
191
|
-
|
|
218
|
+
Consenti ships 8 pre-built English profiles, one per compliance group. The active group is resolved automatically per visitor (API or client-side) or fixed globally via `compliance.type`.
|
|
192
219
|
|
|
193
|
-
|
|
220
|
+
| Group | Region / Law | Model | GPC Default |
|
|
221
|
+
|----------------------------|-------------------------------|------------|-------------|
|
|
222
|
+
| `'opt-in'` | EU / EEA — GDPR | Opt-in | `'honor'` |
|
|
223
|
+
| `'opt-out'` | California — CCPA | Opt-out | `'honor'` |
|
|
224
|
+
| `'opt-out-strict'` | California — CPRA | Strict opt-out | `'strict'` |
|
|
225
|
+
| `'opt-in-dpdpa'` | India — DPDPA | Opt-in | `'honor'` |
|
|
226
|
+
| `'opt-in-china'` | China — PIPL | Opt-in | `'ignore'` |
|
|
227
|
+
| `'opt-in-brazil'` | Brazil — LGPD | Opt-in | `'honor'` |
|
|
228
|
+
| `'general-privacy-consent'`| Global / general | Opt-in | `'honor'` |
|
|
229
|
+
| `'notice-only'` | Informational | Notice | `'ignore'` |
|
|
230
|
+
|
|
231
|
+
### Fixed group (no geo-routing)
|
|
194
232
|
|
|
195
233
|
```ts
|
|
234
|
+
// All visitors see the GDPR opt-in banner
|
|
235
|
+
new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
236
|
+
|
|
237
|
+
// All visitors see the CCPA opt-out notice
|
|
238
|
+
new ConsentiSetup({ compliance: { type: 'opt-out' } })
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Auto geo-routing (default)
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
// Client-side: timezone + navigator.language → compliance group
|
|
245
|
+
new ConsentiSetup({ compliance: { type: 'auto' } })
|
|
246
|
+
|
|
247
|
+
// Server-side: /resolve-profile → right profile per visitor country
|
|
196
248
|
new ConsentiSetup({
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
autoHonorGPC: true, // pre-deny 'listenGpc' cookies and show GPC banner variant
|
|
200
|
-
// autoHonorGPC: 'strict' // pre-deny silently — no banner shown at all
|
|
201
|
-
},
|
|
249
|
+
api: { enabled: true, baseUrl: 'https://consent.example.com' },
|
|
250
|
+
compliance: { type: 'auto' },
|
|
202
251
|
})
|
|
203
252
|
```
|
|
204
253
|
|
|
205
|
-
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Profile Resolution Scenarios
|
|
257
|
+
|
|
258
|
+
| Scenario | When | How |
|
|
259
|
+
|---|---|---|
|
|
260
|
+
| **1A** | `api.enabled = false`, `compliance.type = 'auto'` | Timezone + language → group → pre-built profile |
|
|
261
|
+
| **1B** | `api.enabled = false`, `compliance.type = ComplianceGroupId` | Direct pre-built profile load |
|
|
262
|
+
| **2A** | `api.enabled = true`, `compliance.type = 'auto'` | `GET /resolve-profile?tz&lang&locale&tenantId` → `filePath` → static JSON |
|
|
263
|
+
| **2B** | `api.enabled = true`, `compliance.type = ComplianceGroupId` | `GET /profiles/:tenantId/:group/:locale.json` |
|
|
264
|
+
| **Local** | `compliance.type = localKey` + registered `ConsentiProfile` | Locally registered profile |
|
|
265
|
+
| **Fallback** | Any fetch failure | Pre-built profile → `DEFAULT_PROFILE` |
|
|
266
|
+
|
|
267
|
+
The `/resolve-profile` response URL is cached in `sessionStorage` for 1 hour per tab (`core.cacheResolvedProfiles`). The profile JSON itself is HTTP-cached via `Cache-Control: public, max-age=3600` on the server.
|
|
268
|
+
|
|
269
|
+
### Domain allowlist
|
|
270
|
+
|
|
271
|
+
If a profile's `allowedOrigins` list is configured on the server, the widget checks `window.location.origin` against it before using the profile. A mismatch falls back to the pre-built profile for that group.
|
|
272
|
+
|
|
273
|
+
Set `api.trustDomain: true` to bypass this check (useful for localhost / dev environments where `allowedOrigins` contains only production domains).
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## GPC — Global Privacy Control
|
|
278
|
+
|
|
279
|
+
GPC mode is set per-profile on the server (`gpcMode: 'honor' | 'strict' | 'ignore'`). Widget config (`core.autoHonorGPC`) overrides the profile value when explicitly set.
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
// Respect profile's gpcMode setting (default)
|
|
283
|
+
new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
284
|
+
|
|
285
|
+
// Force honor GPC regardless of profile setting
|
|
286
|
+
new ConsentiSetup({ core: { autoHonorGPC: true } })
|
|
287
|
+
|
|
288
|
+
// Force strict GPC — pre-deny silently, no banner
|
|
289
|
+
new ConsentiSetup({ core: { autoHonorGPC: 'strict' } })
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
| `core.autoHonorGPC` | Effect |
|
|
293
|
+
|---|---|
|
|
294
|
+
| `undefined` (default) | Profile's `gpcMode` takes effect |
|
|
295
|
+
| `false` | GPC signal always ignored |
|
|
296
|
+
| `true` | `'honor'` — pre-deny GPC cookies; show GPC banner variant |
|
|
297
|
+
| `'strict'` | Pre-deny silently — no banner shown |
|
|
206
298
|
|
|
207
299
|
---
|
|
208
300
|
|
|
209
301
|
## Profiles
|
|
210
302
|
|
|
211
|
-
###
|
|
303
|
+
### Pre-built profiles (no backend)
|
|
304
|
+
|
|
305
|
+
The widget includes 8 pre-built English profiles as dynamic-import chunks. Only the matched chunk downloads at runtime.
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
// GDPR banner — no server needed
|
|
309
|
+
new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
310
|
+
|
|
311
|
+
// CCPA notice — no server needed
|
|
312
|
+
new ConsentiSetup({ compliance: { type: 'opt-out' } })
|
|
212
313
|
|
|
213
|
-
|
|
314
|
+
// Restrict which pre-built profiles are available as fallbacks
|
|
315
|
+
new ConsentiSetup({
|
|
316
|
+
core: { usePrebuiltProfiles: ['opt-in', 'opt-out'] },
|
|
317
|
+
})
|
|
318
|
+
```
|
|
214
319
|
|
|
215
320
|
### Local profile (no backend)
|
|
216
321
|
|
|
@@ -221,11 +326,12 @@ import { ConsentiProfile, ConsentiSetup } from '@consenti/ui'
|
|
|
221
326
|
|
|
222
327
|
const profile = new ConsentiProfile({
|
|
223
328
|
defaultLocale: 'en',
|
|
329
|
+
complianceGroup: 'opt-in',
|
|
224
330
|
cookies: [
|
|
225
331
|
{ id: 'necessary', mandatory: true, expiry: 365 },
|
|
226
332
|
{ id: 'analytics', listenGpc: true, expiry: 365 },
|
|
227
333
|
{ id: 'marketing', listenGpc: true, expiry: 365, cpraCategory: 'sale' },
|
|
228
|
-
// cpraCategory: 'sale' | 'sharing' | 'sensitive' — used
|
|
334
|
+
// cpraCategory: 'sale' | 'sharing' | 'sensitive' — used by CPRA group
|
|
229
335
|
],
|
|
230
336
|
translations: {
|
|
231
337
|
en: {
|
|
@@ -240,10 +346,9 @@ const profile = new ConsentiProfile({
|
|
|
240
346
|
buttons: [
|
|
241
347
|
{ text: 'Accept All', style: 'primary', action: 'custom', cookies: '*' },
|
|
242
348
|
// cookies: '*' = grant all | '!' = deny all | ['id1','id2'] = grant specific
|
|
243
|
-
{ text: 'Reject
|
|
244
|
-
{ text: '
|
|
349
|
+
{ text: 'Reject Optional', style: 'secondary', action: 'custom', cookies: '!' },
|
|
350
|
+
{ text: 'Customize', style: 'secondary', action: 'manage' },
|
|
245
351
|
{ text: 'Privacy Policy', style: 'text', action: 'link', url: '/privacy' },
|
|
246
|
-
// action: 'link' + url — renders as a text link below the banner
|
|
247
352
|
],
|
|
248
353
|
},
|
|
249
354
|
gpcBanner: { // shown instead of mainBanner when GPC detected
|
|
@@ -253,13 +358,13 @@ const profile = new ConsentiProfile({
|
|
|
253
358
|
showLocaleSwitcher: false,
|
|
254
359
|
htmlText: "Your browser's GPC signal was detected. Ad cookies have been pre-denied.",
|
|
255
360
|
buttons: [
|
|
256
|
-
{ text: 'Understood',
|
|
257
|
-
{ text: '
|
|
361
|
+
{ text: 'Understood', style: 'primary', action: 'custom', cookies: '!' },
|
|
362
|
+
{ text: 'Customize', style: 'secondary', action: 'manage' },
|
|
258
363
|
],
|
|
259
364
|
},
|
|
260
365
|
preferenceModal: {
|
|
261
366
|
heading: 'Cookie Preferences',
|
|
262
|
-
headingTag: 'h2',
|
|
367
|
+
headingTag: 'h2',
|
|
263
368
|
subheading: 'Choose which cookies you allow.',
|
|
264
369
|
htmlText: 'We use different types of cookies. You can enable or disable each category below.',
|
|
265
370
|
position: 'center', // 'left' | 'right' | 'center'
|
|
@@ -267,16 +372,17 @@ const profile = new ConsentiProfile({
|
|
|
267
372
|
showLocaleSwitcher: false,
|
|
268
373
|
persistent: false, // true = cannot dismiss by clicking outside
|
|
269
374
|
overlayOpacity: 50,
|
|
375
|
+
mobileFullScreenBreakpoint: 576,
|
|
270
376
|
buttons: [
|
|
271
377
|
{ text: 'Accept All', style: 'primary', action: 'custom', cookies: '*' },
|
|
272
378
|
{ text: 'Save Preferences', style: 'primary', action: 'submit' },
|
|
273
|
-
{ text: 'Reject
|
|
379
|
+
{ text: 'Reject Optional', style: 'text', action: 'custom', cookies: '!' },
|
|
274
380
|
],
|
|
275
381
|
categories: [
|
|
276
382
|
{
|
|
277
383
|
id: 'cat-necessary',
|
|
278
384
|
heading: 'Strictly Necessary',
|
|
279
|
-
headingTag: 'h3',
|
|
385
|
+
headingTag: 'h3',
|
|
280
386
|
htmlText: 'Required for the site to function. <strong>Cannot be disabled.</strong>',
|
|
281
387
|
mandatory: true,
|
|
282
388
|
cookies: ['necessary'],
|
|
@@ -286,7 +392,6 @@ const profile = new ConsentiProfile({
|
|
|
286
392
|
heading: 'Analytics',
|
|
287
393
|
htmlText: 'Helps us understand how visitors use the site.',
|
|
288
394
|
type: 'consent', // 'consent' | 'legitimate_interest'
|
|
289
|
-
// type: 'legitimate_interest' — refusal stored as 'objected' instead of 'denied'
|
|
290
395
|
cookies: ['analytics'],
|
|
291
396
|
},
|
|
292
397
|
{
|
|
@@ -307,35 +412,30 @@ const profile = new ConsentiProfile({
|
|
|
307
412
|
})
|
|
308
413
|
|
|
309
414
|
new ConsentiSetup({
|
|
310
|
-
|
|
311
|
-
profileId: profile.getId(), // always use .getId() — never hardcode the ID
|
|
312
|
-
regulation: 'gdpr',
|
|
313
|
-
locale: 'fr',
|
|
314
|
-
},
|
|
415
|
+
compliance: { type: profile.getComplianceGroup() },
|
|
315
416
|
})
|
|
316
417
|
```
|
|
317
418
|
|
|
318
|
-
> **Note:** Local profile IDs are auto-assigned starting from `1000` to avoid collisions with API profile IDs (which start from `1`). Always call `profile.getId()`.
|
|
319
|
-
|
|
320
419
|
### API profile (with backend)
|
|
321
420
|
|
|
322
421
|
```ts
|
|
323
422
|
new ConsentiSetup({
|
|
324
|
-
core: { profileId: 3, regulation: 'gdpr', locale: 'de' },
|
|
325
423
|
api: {
|
|
326
424
|
enabled: true,
|
|
327
425
|
baseUrl: 'https://consent.example.com',
|
|
426
|
+
tenantId: 'acme',
|
|
328
427
|
},
|
|
428
|
+
// compliance.type: 'auto' resolves the right profile per visitor country
|
|
329
429
|
})
|
|
330
430
|
```
|
|
331
431
|
|
|
332
|
-
The widget
|
|
432
|
+
The widget calls `GET /resolve-profile?tz&lang&locale&tenantId`, receives the profile file path, then fetches that static JSON directly. Falls back to pre-built profile if the API is unavailable.
|
|
333
433
|
|
|
334
434
|
---
|
|
335
435
|
|
|
336
436
|
## Cookie Format
|
|
337
437
|
|
|
338
|
-
Consent is persisted as a single cookie named `consenti_{profileId}`.
|
|
438
|
+
Consent is persisted as a single cookie named `consenti_{userId}_{profileId}`.
|
|
339
439
|
|
|
340
440
|
```
|
|
341
441
|
t:{unixTimestamp}::{cookieId}:{status}|{cookieId}:{status}|…[::sig:{hmac}]
|
|
@@ -346,18 +446,38 @@ Example:
|
|
|
346
446
|
t:1782133054::analytics:granted|marketing:denied|necessary:granted::sig:abc123
|
|
347
447
|
```
|
|
348
448
|
|
|
449
|
+
The consent cookie lifetime equals the shortest `cookie.expiry` value across all profile cookies (in days, converted to `max-age` seconds). When that cookie expires the browser deletes it, the widget sees no consent, and re-shows the banner.
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Debugging
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
new ConsentiSetup({
|
|
457
|
+
core: {
|
|
458
|
+
console: ['error', 'warning', 'info'], // add 'log' for verbose output
|
|
459
|
+
},
|
|
460
|
+
})
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
The default is `['error']` only — no noise in production. Add levels when investigating profile resolution, GPC behavior, or domain allowlist failures. All Consenti log lines are prefixed `[Consenti]`.
|
|
464
|
+
|
|
349
465
|
---
|
|
350
466
|
|
|
351
467
|
## Widget API Methods
|
|
352
468
|
|
|
353
469
|
```ts
|
|
354
|
-
const widget = new ConsentiSetup({
|
|
470
|
+
const widget = new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
355
471
|
|
|
356
472
|
// Consent state
|
|
357
|
-
widget.hasConsent()
|
|
358
|
-
widget.getConsent()
|
|
359
|
-
widget.getGTMConsent()
|
|
360
|
-
widget.getConsentDate()
|
|
473
|
+
widget.hasConsent() // boolean — true if valid consent record exists
|
|
474
|
+
widget.getConsent() // Record<string, 'granted'|'denied'|'objected'> | null
|
|
475
|
+
widget.getGTMConsent() // consent in GTM / Google Consent Mode v2 format
|
|
476
|
+
widget.getConsentDate() // Date | false — time of last submission
|
|
477
|
+
widget.isCookieGranted('analytics') // boolean — true if that cookie is 'granted'
|
|
478
|
+
widget.isCookieGranted('analytics', true)// 'granted' | 'denied' | 'objected' | false
|
|
479
|
+
widget.isCategoryGranted('cat-analytics') // boolean — true if ALL cookies in category are granted
|
|
480
|
+
widget.isCategoryGranted('cat-analytics', true) // [{ analytics: 'granted' }, { pixel: 'denied' }]
|
|
361
481
|
|
|
362
482
|
// Visibility
|
|
363
483
|
widget.showBanner(gpc?) // show main banner (or GPC variant)
|
|
@@ -367,6 +487,12 @@ widget.hideModal() // close preference modal
|
|
|
367
487
|
widget.bannerVisibility() // 'main' | 'gpc' | false
|
|
368
488
|
widget.modalVisibility() // 'preference' | false
|
|
369
489
|
|
|
490
|
+
// Bulk consent actions
|
|
491
|
+
widget.grantAll() // grant all cookies and dismiss banner
|
|
492
|
+
widget.grantAll(true) // grant only mandatory; deny everything else
|
|
493
|
+
widget.denyAll() // deny non-mandatory; mandatory stay 'granted'
|
|
494
|
+
widget.denyAll(true) // deny all including mandatory (logs a warning)
|
|
495
|
+
|
|
370
496
|
// Actions
|
|
371
497
|
widget.init() // manually start init (when autoInit: false)
|
|
372
498
|
widget.onReady(callback) // called once widget is fully initialised
|
|
@@ -374,15 +500,61 @@ widget.switchLocale(locale) // switch locale and re-render (e.g. 'fr', 'de-
|
|
|
374
500
|
widget.submitConsent(consent) // programmatically submit consent values
|
|
375
501
|
widget.deleteConsent() // delete consent record (cookie + backend if enabled)
|
|
376
502
|
widget.reConsent() // delete consent and re-show banner
|
|
377
|
-
widget.downloadReceipt() // trigger JSON consent receipt download
|
|
378
503
|
widget.destroy() // unmount DOM elements and remove all event listeners
|
|
504
|
+
|
|
505
|
+
// Runtime configuration
|
|
506
|
+
widget.setDarkMode() // toggle dark mode
|
|
507
|
+
widget.setDarkMode(true) // force dark on
|
|
508
|
+
widget.setDarkMode(false) // force light
|
|
509
|
+
widget.setTheme({ primaryColor: '#ff0000' }) // hot-swap CSS tokens (merges into current theme)
|
|
510
|
+
widget.setConfig({ darkMode: true }) // deep-merge partial config (no re-init)
|
|
511
|
+
widget.setProfile({ mainBanner: { heading: 'Updated' } }) // re-render UI with new profile data
|
|
512
|
+
|
|
513
|
+
// Diagnostics
|
|
514
|
+
widget.version() // { package: '1.0.0', profileVersion: 2, consentVersion: 1 }
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### Consent checks
|
|
518
|
+
|
|
519
|
+
```ts
|
|
520
|
+
// Gate analytics code on a single cookie
|
|
521
|
+
if (widget.isCookieGranted('analytics_storage')) {
|
|
522
|
+
initAnalytics()
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Read the raw status string when you need it
|
|
526
|
+
const status = widget.isCookieGranted('marketing', true) // 'granted' | 'denied' | 'objected' | false
|
|
527
|
+
|
|
528
|
+
// Gate a feature on an entire category (all cookies in category must be granted)
|
|
529
|
+
if (widget.isCategoryGranted('cat-analytics')) {
|
|
530
|
+
loadHeatmaps()
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Inspect each cookie's status in a category
|
|
534
|
+
const statuses = widget.isCategoryGranted('cat-marketing', true)
|
|
535
|
+
// [{ ad_storage: 'granted' }, { ad_personalization: 'denied' }]
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Typed event subscriptions
|
|
539
|
+
|
|
540
|
+
```ts
|
|
541
|
+
// Both forms are accepted — 'consenti:' prefix is optional
|
|
542
|
+
const handler = (data) => console.log('Consent saved:', data.consent)
|
|
543
|
+
widget.on('consentSubmitted', handler)
|
|
544
|
+
|
|
545
|
+
// Remove later (must pass the same function reference)
|
|
546
|
+
widget.off('consentSubmitted', handler)
|
|
547
|
+
|
|
548
|
+
// Available event names:
|
|
549
|
+
// 'bannerInitialized' | 'bannerVisibility' | 'modalVisibility'
|
|
550
|
+
// 'consentBeingSubmitted' | 'consentSubmitted'
|
|
379
551
|
```
|
|
380
552
|
|
|
381
553
|
### Manual init
|
|
382
554
|
|
|
383
555
|
```ts
|
|
384
556
|
const widget = new ConsentiSetup({
|
|
385
|
-
|
|
557
|
+
compliance: { type: 'opt-in' },
|
|
386
558
|
rootEl: '#consent-mount',
|
|
387
559
|
autoInit: false,
|
|
388
560
|
})
|
|
@@ -400,32 +572,39 @@ await widget.submitConsent({
|
|
|
400
572
|
marketing: 'denied',
|
|
401
573
|
necessary: 'granted', // mandatory cookies are always 'granted' regardless
|
|
402
574
|
})
|
|
575
|
+
|
|
576
|
+
// Or use the convenience methods:
|
|
577
|
+
await widget.grantAll() // accept all
|
|
578
|
+
await widget.denyAll() // reject all non-mandatory
|
|
403
579
|
```
|
|
404
580
|
|
|
405
581
|
---
|
|
406
582
|
|
|
407
583
|
## Events
|
|
408
584
|
|
|
409
|
-
Custom DOM events fire on `
|
|
410
|
-
|
|
411
|
-
| Event | Fired when |
|
|
412
|
-
|
|
413
|
-
| `consenti:bannerInitialized` | Widget initialises and determines banner visibility |
|
|
414
|
-
| `consenti:bannerVisibility` | Banner shows or hides |
|
|
415
|
-
| `consenti:modalVisibility` | Preference modal shows or hides |
|
|
416
|
-
| `consenti:consentBeingSubmitted` | User clicked a consent button (before API call) |
|
|
417
|
-
| `consenti:consentSubmitted` | Consent saved (cookie written + API call if configured) |
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
585
|
+
Custom DOM events fire on `window` at every lifecycle step. All are prefixed `consenti:`.
|
|
586
|
+
|
|
587
|
+
| Event | Fired when |
|
|
588
|
+
|--------------------------------|----------------------------------------------------------|
|
|
589
|
+
| `consenti:bannerInitialized` | Widget initialises and determines banner visibility |
|
|
590
|
+
| `consenti:bannerVisibility` | Banner shows or hides |
|
|
591
|
+
| `consenti:modalVisibility` | Preference modal shows or hides |
|
|
592
|
+
| `consenti:consentBeingSubmitted` | User clicked a consent button (before API call) |
|
|
593
|
+
| `consenti:consentSubmitted` | Consent saved (cookie written + API call if configured) |
|
|
594
|
+
|
|
595
|
+
**Recommended:** use `widget.on()` / `widget.off()` for typed subscriptions (the `consenti:` prefix is optional):
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
const handler = (data: ConsentEvent) => console.log(data.consent)
|
|
599
|
+
widget.on('consentSubmitted', handler)
|
|
600
|
+
widget.off('consentSubmitted', handler) // same reference to unsubscribe
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**Raw DOM listeners** (equivalent):
|
|
604
|
+
|
|
605
|
+
```ts
|
|
606
|
+
window.addEventListener('consenti:consentSubmitted', (e: Event) => {
|
|
607
|
+
const detail = (e as CustomEvent<ConsentEvent>).detail
|
|
429
608
|
})
|
|
430
609
|
```
|
|
431
610
|
|
|
@@ -435,7 +614,7 @@ document.addEventListener('consenti:consentSubmitted', (e: Event) => {
|
|
|
435
614
|
|
|
436
615
|
```ts
|
|
437
616
|
new ConsentiSetup({
|
|
438
|
-
|
|
617
|
+
compliance: { type: 'opt-in' },
|
|
439
618
|
utils: {
|
|
440
619
|
gtm: {
|
|
441
620
|
containerId: 'GTM-XXXXXX',
|
|
@@ -483,6 +662,8 @@ new ConsentScript({
|
|
|
483
662
|
src: 'https://www.googletagmanager.com/gtag/js?id=G-XXXXX',
|
|
484
663
|
onLoad: () => console.log('Analytics loaded'),
|
|
485
664
|
onRevoke: () => console.log('Analytics removed'),
|
|
665
|
+
// bind: true (default) — auto-removes script on consent revoke, re-injects on re-grant
|
|
666
|
+
// bind: false — check consent once at construction; never auto-remove
|
|
486
667
|
})
|
|
487
668
|
```
|
|
488
669
|
|
|
@@ -555,8 +736,8 @@ Override any token in your own stylesheet:
|
|
|
555
736
|
|
|
556
737
|
```ts
|
|
557
738
|
new ConsentiSetup({
|
|
739
|
+
compliance: { type: 'opt-in' },
|
|
558
740
|
core: {
|
|
559
|
-
regulation: 'gdpr',
|
|
560
741
|
theme: {
|
|
561
742
|
primaryColor: '#7c3aed', // purple brand
|
|
562
743
|
primaryTextColor: '#ffffff',
|
|
@@ -572,7 +753,7 @@ new ConsentiSetup({
|
|
|
572
753
|
|
|
573
754
|
```ts
|
|
574
755
|
new ConsentiSetup({
|
|
575
|
-
|
|
756
|
+
compliance: { type: 'opt-in' },
|
|
576
757
|
darkMode: true, // or: window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
577
758
|
})
|
|
578
759
|
```
|
|
@@ -631,7 +812,8 @@ import '@consenti/ui/dist/index.css'
|
|
|
631
812
|
export function ConsentSetup() {
|
|
632
813
|
useEffect(() => {
|
|
633
814
|
const widget = new ConsentiSetup({
|
|
634
|
-
|
|
815
|
+
compliance: { type: 'opt-in' },
|
|
816
|
+
core: { locale: 'en' },
|
|
635
817
|
})
|
|
636
818
|
return () => widget.destroy()
|
|
637
819
|
}, [])
|
|
@@ -687,8 +869,8 @@ export function ConsentSetup() {
|
|
|
687
869
|
let widget: WidgetType
|
|
688
870
|
import('@consenti/ui').then(({ ConsentiSetup }) => {
|
|
689
871
|
widget = new ConsentiSetup({
|
|
690
|
-
core: { regulation: 'gdpr', locale: 'en', autoHonorGPC: true },
|
|
691
872
|
api: { enabled: true, baseUrl: process.env.NEXT_PUBLIC_API_URL },
|
|
873
|
+
core: { autoHonorGPC: true },
|
|
692
874
|
})
|
|
693
875
|
widgetRef.current = widget
|
|
694
876
|
})
|
|
@@ -710,7 +892,7 @@ let widget: unknown = null
|
|
|
710
892
|
onMounted(async () => {
|
|
711
893
|
const { ConsentiSetup } = await import('@consenti/ui')
|
|
712
894
|
await import('@consenti/ui/dist/index.css')
|
|
713
|
-
widget = new ConsentiSetup({
|
|
895
|
+
widget = new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
714
896
|
})
|
|
715
897
|
|
|
716
898
|
onBeforeUnmount(() => (widget as { destroy?: () => void })?.destroy?.())
|
|
@@ -739,7 +921,7 @@ export class ConsentService implements OnDestroy {
|
|
|
739
921
|
async init() {
|
|
740
922
|
if (!isPlatformBrowser(this.platformId)) return
|
|
741
923
|
const { ConsentiSetup } = await import('@consenti/ui')
|
|
742
|
-
this.widget = new ConsentiSetup({
|
|
924
|
+
this.widget = new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
743
925
|
}
|
|
744
926
|
|
|
745
927
|
ngOnDestroy() {
|
|
@@ -766,7 +948,7 @@ export class MyComponent {
|
|
|
766
948
|
import { ConsentiSetup } from '@consenti/ui'
|
|
767
949
|
import '@consenti/ui/dist/index.css'
|
|
768
950
|
|
|
769
|
-
const widget = new ConsentiSetup({
|
|
951
|
+
const widget = new ConsentiSetup({ compliance: { type: 'opt-in' }, core: { locale: 'en' } })
|
|
770
952
|
|
|
771
953
|
document.querySelector('#cookie-settings')?.addEventListener('click', () => {
|
|
772
954
|
widget.showModal()
|
|
@@ -798,7 +980,7 @@ class MyPlugin extends ConsentiPlugin {
|
|
|
798
980
|
}
|
|
799
981
|
|
|
800
982
|
new ConsentiSetup({
|
|
801
|
-
|
|
983
|
+
compliance: { type: 'opt-in' },
|
|
802
984
|
plugins: [new MyPlugin()],
|
|
803
985
|
})
|
|
804
986
|
```
|
|
@@ -809,7 +991,7 @@ new ConsentiSetup({
|
|
|
809
991
|
|
|
810
992
|
```ts
|
|
811
993
|
// Safe to call during SSR — silently no-ops, never touches the DOM:
|
|
812
|
-
const widget = new ConsentiSetup({
|
|
994
|
+
const widget = new ConsentiSetup({ compliance: { type: 'opt-in' } })
|
|
813
995
|
// All browser API access is guarded by isClient() internally.
|
|
814
996
|
```
|
|
815
997
|
|
|
@@ -830,8 +1012,8 @@ import {
|
|
|
830
1012
|
simulateConsentSubmitted,
|
|
831
1013
|
} from '@consenti/ui/testing'
|
|
832
1014
|
|
|
833
|
-
mockAllGranted() // fake-grant
|
|
834
|
-
mockAllDenied() // fake-deny
|
|
1015
|
+
mockAllGranted(['analytics', 'marketing']) // fake-grant these cookies for unit tests
|
|
1016
|
+
mockAllDenied(['analytics', 'marketing']) // fake-deny these cookies
|
|
835
1017
|
simulateConsentSubmitted({ analytics: 'granted' })
|
|
836
1018
|
```
|
|
837
1019
|
|
|
@@ -840,20 +1022,31 @@ simulateConsentSubmitted({ analytics: 'granted' })
|
|
|
840
1022
|
## Minimal Recipes
|
|
841
1023
|
|
|
842
1024
|
```ts
|
|
843
|
-
//
|
|
844
|
-
new ConsentiSetup({
|
|
1025
|
+
// Auto geo-route — no backend, client-side timezone + language heuristic
|
|
1026
|
+
new ConsentiSetup({})
|
|
1027
|
+
|
|
1028
|
+
// CCPA opt-out for all visitors
|
|
1029
|
+
new ConsentiSetup({ compliance: { type: 'opt-out' } })
|
|
845
1030
|
|
|
846
1031
|
// Cross-subdomain consent
|
|
847
|
-
new ConsentiSetup({ core: {
|
|
1032
|
+
new ConsentiSetup({ core: { storage: 'cookie', cookieDomains: '.example.com' } })
|
|
848
1033
|
|
|
849
1034
|
// Authenticated user — cross-device sync via API
|
|
850
|
-
new ConsentiSetup({
|
|
1035
|
+
new ConsentiSetup({
|
|
1036
|
+
core: { userId: '{{ server_user_id }}' },
|
|
1037
|
+
api: { enabled: true },
|
|
1038
|
+
})
|
|
851
1039
|
|
|
852
1040
|
// GPC strict mode + GTM
|
|
853
1041
|
new ConsentiSetup({
|
|
854
|
-
core: {
|
|
1042
|
+
core: { autoHonorGPC: 'strict' },
|
|
855
1043
|
utils: { gtm: { containerId: 'GTM-XXXXXX', adsDataRedaction: true } },
|
|
856
1044
|
})
|
|
1045
|
+
|
|
1046
|
+
// Verbose debug logging during development
|
|
1047
|
+
new ConsentiSetup({
|
|
1048
|
+
core: { console: ['error', 'warning', 'info', 'log'] },
|
|
1049
|
+
})
|
|
857
1050
|
```
|
|
858
1051
|
|
|
859
1052
|
---
|
|
@@ -864,17 +1057,32 @@ All types are exported from the main entry:
|
|
|
864
1057
|
|
|
865
1058
|
```ts
|
|
866
1059
|
import type {
|
|
867
|
-
ConsentiConfig,
|
|
868
|
-
CoreConfig,
|
|
869
|
-
ApiConfig,
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1060
|
+
ConsentiConfig, // top-level config object
|
|
1061
|
+
CoreConfig, // core section
|
|
1062
|
+
ApiConfig, // api section
|
|
1063
|
+
ComplianceWidgetConfig, // compliance section
|
|
1064
|
+
AgeGateWidgetConfig, // compliance.ageGate section
|
|
1065
|
+
WidgetCountryResolverFn, // custom geo resolver function type
|
|
1066
|
+
UtilsConfig, // utils section
|
|
1067
|
+
GtmConfig, // utils.gtm section
|
|
1068
|
+
ThemeConfig, // core.theme section
|
|
1069
|
+
ConsentValue, // 'granted' | 'denied' | 'objected'
|
|
1070
|
+
ConsentiProfile, // local profile class
|
|
1071
|
+
NonEmptyArray, // [T, ...T[]] — used by usePrebuiltProfiles
|
|
875
1072
|
} from '@consenti/ui'
|
|
876
1073
|
```
|
|
877
1074
|
|
|
1075
|
+
### Breaking changes from v0.0.x
|
|
1076
|
+
|
|
1077
|
+
The following `CoreConfig` fields were **removed** in v0.1.x:
|
|
1078
|
+
|
|
1079
|
+
| Removed field | Replacement |
|
|
1080
|
+
|----------------------|-------------|
|
|
1081
|
+
| `core.profileId` | Use `compliance.type` with a compliance group or local profile key |
|
|
1082
|
+
| `core.regulation` | Use `compliance.type` — compliance group is derived server-side or from pre-built profiles |
|
|
1083
|
+
| `core.signCookies` | Implicit: set `core.cookieSigningKey` to enable signing; no separate flag needed |
|
|
1084
|
+
| `core.privacyPolicyUrl` | Add a link button directly in the profile banner/modal `buttons` array with `action: 'link'` |
|
|
1085
|
+
|
|
878
1086
|
---
|
|
879
1087
|
|
|
880
1088
|
## Contributing
|