@consenti/api 0.1.0 → 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 +917 -207
- package/dist/chunk-IY4L6MNB.js +2 -0
- package/dist/chunk-IY4L6MNB.js.map +1 -0
- package/dist/chunk-O5UG4OYI.js +2 -0
- package/dist/chunk-O5UG4OYI.js.map +1 -0
- package/dist/dashboard/assets/ApiConfig-CE-Noelz.js +1 -0
- package/dist/dashboard/assets/ArchivedProfileList-Bw-fx-BD.js +1 -0
- package/dist/dashboard/assets/AuditLog-D6HiTQ1o.js +1 -0
- package/dist/dashboard/assets/ChangePassword-D2rPkzD1.js +1 -0
- package/dist/dashboard/assets/ConfirmDialog-DTtw7VJ-.js +1 -0
- package/dist/dashboard/assets/ConsentList-BWOvPOMG.js +1 -0
- package/dist/dashboard/assets/ConsentTemplateEditor-BKIEnLmA.js +1 -0
- package/dist/dashboard/assets/ConsentTemplateList-DCys2nM3.js +1 -0
- package/dist/dashboard/assets/HeadingTagInput-BTjrdzlS.js +1 -0
- package/dist/dashboard/assets/HowItWorks-AeyjfQGO.js +1 -0
- package/dist/dashboard/assets/PermissionGate-Bhhf7SMh.js +1 -0
- package/dist/dashboard/assets/PreviewPane-Dq5K1h2L.js +35 -0
- package/dist/dashboard/assets/ProfileEditor-BXtaE0tL.js +2 -0
- package/dist/dashboard/assets/ProfileList-CWKSAgu0.js +1 -0
- package/dist/dashboard/assets/ProfileVersionHistory-1h8sUgog.js +3 -0
- package/dist/dashboard/assets/RecordDetailModal-FaFhzQXi.js +1 -0
- package/dist/dashboard/assets/Reports-DWEjQOKh.js +1 -0
- package/dist/dashboard/assets/RoleList-BSGoqlzp.js +1 -0
- package/dist/dashboard/assets/Select-DjJNwlbY.js +1 -0
- package/dist/dashboard/assets/Settings-CVYrlyLm.js +1 -0
- package/dist/dashboard/assets/SetupWizard-4NTb5bZj.js +1 -0
- package/dist/dashboard/assets/Table-DH_jh8_N.js +1 -0
- package/dist/dashboard/assets/TenantList-5AwFtoHf.js +1 -0
- package/dist/dashboard/assets/UITemplateEditor-9A55scBe.js +1 -0
- package/dist/dashboard/assets/UITemplateList-BS7X4u4F.js +1 -0
- package/dist/dashboard/assets/UserList-SPkMyFRy.js +1 -0
- package/dist/dashboard/assets/VendorList-C5fPhr6U.js +1 -0
- package/dist/dashboard/assets/VisitorList-COP1SyNd.js +1 -0
- package/dist/dashboard/assets/check-o5cp1s2J.js +1 -0
- package/dist/dashboard/assets/client-ymc9nwWr.js +1 -0
- package/dist/dashboard/assets/copy-C01KoUUH.js +1 -0
- package/dist/dashboard/assets/createLucideIcon-MdjI3m3u.js +1 -0
- package/dist/dashboard/assets/de-DET92Jb5.js +5 -0
- package/dist/dashboard/assets/debounce-DlvAAT2V.js +1 -0
- package/dist/dashboard/assets/dist-DlhLroYC.js +1 -0
- package/dist/dashboard/assets/es-CJNunrnU.js +5 -0
- package/dist/dashboard/assets/external-link-DVe00ZBK.js +1 -0
- package/dist/dashboard/assets/fr-akfTuf5x.js +5 -0
- package/dist/dashboard/assets/history-CHTts_m2.js +1 -0
- package/dist/dashboard/assets/index-DW3qOrry.css +2 -0
- package/dist/dashboard/assets/index-H_Zm0X-u.js +6 -0
- package/dist/dashboard/assets/ja-B0nLWkOd.js +5 -0
- package/dist/dashboard/assets/profiles-DmrdGv7t.js +1 -0
- package/dist/dashboard/assets/roles-C0dJPFl4.js +1 -0
- package/dist/dashboard/assets/search-Bd2RdDRh.js +1 -0
- package/dist/dashboard/assets/templates-B2xiivh4.js +1 -0
- package/dist/dashboard/assets/templates-CnMoLavN.js +1 -0
- package/dist/dashboard/assets/trash-2-BBnkdgm_.js +1 -0
- package/dist/dashboard/assets/trash-Dj1jhzHJ.js +1 -0
- package/dist/dashboard/assets/useHashQuery-B5ytMIMp.js +1 -0
- package/dist/dashboard/assets/users-czK8LRVh.js +1 -0
- package/dist/dashboard/assets/x-BRuyBEQP.js +1 -0
- package/dist/dashboard/assets/zh-CN-Dxg3G-BL.js +5 -0
- package/dist/dashboard/index.html +4 -2
- package/dist/dist-Z5RVTVBT.js +2 -0
- package/dist/dist-Z5RVTVBT.js.map +1 -0
- package/dist/index.cjs +129 -9089
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +963 -254
- package/dist/index.d.ts +963 -254
- package/dist/index.js +129 -9043
- package/dist/index.js.map +1 -1
- package/dist/profiles-XTYPWLVC.js +2 -0
- package/dist/profiles-XTYPWLVC.js.map +1 -0
- package/dist/widget/widget.css +1 -0
- package/dist/widget/widget.js +1 -2304
- package/package.json +19 -12
- package/dist/dashboard/assets/index-BBbhHyc_.js +0 -2637
- package/dist/dashboard/assets/index-DYK73WL5.css +0 -1
package/README.md
CHANGED
|
@@ -8,12 +8,12 @@ Zero-dependency, GDPR-compliant Consent Management Platform backend for Node.js.
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
## Why
|
|
11
|
+
## Why Consenti?
|
|
12
12
|
|
|
13
13
|
- **Three SQLite drivers** — `node:sqlite` (Node 22.5+ built-in, zero install), `node-sqlite3-wasm` (WASM, zero compilation, optional peer dep), `better-sqlite3` (native, optional peer dep)
|
|
14
14
|
- **One function to mount** — `createConsenti()` returns a router you drop into any Node.js HTTP framework
|
|
15
15
|
- **Self-contained admin dashboard** — full SPA served at your `basePath`, no separate deploy
|
|
16
|
-
- **Multi-regulation** —
|
|
16
|
+
- **Multi-regulation** — 76 regulations across EU/EEA, UK, US (22 state laws), India, Brazil, China, Canada, 18 APAC countries, 9 Middle East countries, 14 African countries, and more — with automatic geo-routing via 8 compliance groups
|
|
17
17
|
- **Pluggable storage** — SQLite (zero-config, default) · PostgreSQL · MySQL · MongoDB
|
|
18
18
|
|
|
19
19
|
---
|
|
@@ -22,7 +22,45 @@ Zero-dependency, GDPR-compliant Consent Management Platform backend for Node.js.
|
|
|
22
22
|
|
|
23
23
|
- **Node.js ≥ 20.0.0** — Node 22 or 24 LTS recommended for production
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @consenti/api
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Database / Storage
|
|
33
|
+
Consenti by default comes with JSON file based storage, to get started without any installation.
|
|
34
|
+
We have inbuilt SQLite, PostgreSQL, MySQL and MongoDB as storage adaptors, all are optional peer dependencies, only install any one of them that you need. Thats not all, you can create an adaptor for any other database you want to use by using [adapter.interface.ts](apps/api/src/storage/adapter.interface.ts)
|
|
35
|
+
|
|
36
|
+
> **Note:** Default JSON file based storage is only for development and small scale deployments, for production it is recommended to use other database options.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Databases (choose any one )
|
|
40
|
+
|
|
41
|
+
# 1. node:sqlite - Node native SQLite,
|
|
42
|
+
# needs no install on Node 22.5+
|
|
43
|
+
|
|
44
|
+
# OR 2. WASM SQLite — Node 20+, zero compilation (optional, install only if storage.driver: "node-sqlite3-wasm")
|
|
45
|
+
npm install node-sqlite3-wasm
|
|
46
|
+
|
|
47
|
+
# OR 3. native SQLite — Node 20+, faster but needs a C++ toolchain (optional, install only if storage.driver: "better-sqlite3")
|
|
48
|
+
npm install better-sqlite3
|
|
49
|
+
|
|
50
|
+
# OR 4. PostgreSQL (optional, install only if storage.driver: "postgresql")
|
|
51
|
+
npm install pg
|
|
52
|
+
|
|
53
|
+
# OR 5. MySQL (optional, install only if storage.driver: "mysql")
|
|
54
|
+
npm install mysql2
|
|
55
|
+
|
|
56
|
+
# OR 6. MongoDB (optional, install only if storage.driver: "mongodb")
|
|
57
|
+
npm install mongodb
|
|
58
|
+
|
|
59
|
+
# OR 7. Create your own custom database Adaptor using "apps/api/src/storage/adapter.interface.ts"
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Which SQLite driver should I use?
|
|
26
64
|
|
|
27
65
|
| Driver | Config | Node requirement | Extra install? |
|
|
28
66
|
|---|---|---|---|
|
|
@@ -34,29 +72,192 @@ Zero-dependency, GDPR-compliant Consent Management Platform backend for Node.js.
|
|
|
34
72
|
`node:sqlite` is the only driver that requires no extra install — it ships with Node 22.5+.
|
|
35
73
|
On Node 20/21, choose between `node-sqlite3-wasm` (pure WASM, no compilation) or `better-sqlite3` (native binary, faster but may fail on Alpine/musl/ARM).
|
|
36
74
|
|
|
37
|
-
---
|
|
38
75
|
|
|
39
|
-
|
|
76
|
+
### Geo resolvers - compliance routing
|
|
40
77
|
|
|
41
|
-
|
|
42
|
-
npm install @consenti/api
|
|
43
|
-
```
|
|
78
|
+
Consenti ships with four geo resolver options. The default requires zero installation; the others are opt-in.
|
|
44
79
|
|
|
45
|
-
|
|
80
|
+
**Bonus:** You can also supply a custom resolver function via `compliance.geoDataProvider` — it receives `{ ip, timezone, language }` and must return `{ country: string | null, region: string | null, locale: string | null }`.
|
|
81
|
+
|
|
82
|
+
> **Note:** The default (`'default'`) and `'hosted-geoip-lite'` resolvers are suitable for development and lower-traffic deployments. For high-accuracy production use, choose `'geoip'` or `'maxmind'`.
|
|
83
|
+
|
|
84
|
+
### Which geo resolver should I use?
|
|
85
|
+
|
|
86
|
+
| Resolver | Config | Extra install? | Notes |
|
|
87
|
+
|---|---|---|---|
|
|
88
|
+
| `default` | `geoDataProvider: 'default'` | **No** | Timezone + Accept-Language heuristic; zero deps; less accurate |
|
|
89
|
+
| `hosted-geoip-lite` | `geoDataProvider: 'hosted-geoip-lite'` | **No** | Calls `ipinfo.io` via `node:https`; requires outbound internet |
|
|
90
|
+
| `geoip` | `geoDataProvider: 'geoip'` | Yes — `npm install geoip-lite` | Local MaxMind GeoLite2 DB via `geoip-lite`; fast, no outbound traffic |
|
|
91
|
+
| `maxmind` | `geoDataProvider: 'maxmind'` | Yes — `npm install maxmind` | Official MaxMind SDK; most accurate; requires `.mmdb` file |
|
|
92
|
+
| `CountryResolverFn` | `geoDataProvider: myFn` | Depends on your impl | Bring your own resolver — see custom example below |
|
|
46
93
|
|
|
47
94
|
```bash
|
|
48
|
-
#
|
|
49
|
-
npm install node-sqlite3-wasm # WASM SQLite — Node 20+, zero compilation
|
|
50
|
-
npm install better-sqlite3 # native SQLite — Node 20+, faster but needs a C++ toolchain
|
|
95
|
+
# Geo resolvers (choose any one )
|
|
51
96
|
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
97
|
+
# 1. Default - language+timezone based
|
|
98
|
+
# No install needed
|
|
99
|
+
|
|
100
|
+
# OR 2. Hosted Geo-IP (optional — only needed when geoDataProvider: 'hosted-geoip-lite')
|
|
101
|
+
# No install needed — calls ipinfo.io using Node's built-in node:https
|
|
102
|
+
# Requires outbound internet access from your server
|
|
103
|
+
|
|
104
|
+
# OR 3. Geo-IP lite (optional — only needed when geoDataProvider: 'geoip')
|
|
105
|
+
npm install geoip-lite
|
|
106
|
+
|
|
107
|
+
# OR 4. Maxmind (optional — only needed when geoDataProvider: 'maxmind')
|
|
108
|
+
npm install maxmind
|
|
57
109
|
|
|
110
|
+
# OR 5. Custom (optional — only needed when geoDataProvider: CountryResolverFn)
|
|
111
|
+
# Do what it requires, just return `{ country, region, locale }` in supported format
|
|
112
|
+
|
|
113
|
+
```
|
|
58
114
|
---
|
|
59
115
|
|
|
116
|
+
|
|
117
|
+
## Full Configuration
|
|
118
|
+
Selected values are default
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { createConsenti } from '@consenti/api'
|
|
122
|
+
|
|
123
|
+
const consenti = createConsenti({
|
|
124
|
+
// ── Storage ─────────────────────────────────────────────────────────────────
|
|
125
|
+
storage: {
|
|
126
|
+
driver: 'json', // 'json' | 'node:sqlite' | 'node-sqlite3-wasm' | 'better-sqlite3' | 'sqlite' | 'postgresql' | 'mysql' | 'mongodb'
|
|
127
|
+
path: './consenti-data', // base directory — Consenti creates db/, profiles/, logs/ subdirectories automatically
|
|
128
|
+
|
|
129
|
+
// Optional
|
|
130
|
+
uri: 'mongodb://localhost:27017/consenti', // db connection string/uri, only for (driver: 'postgresql' | 'mysql' | 'mongodb')
|
|
131
|
+
database: 'consenti', // database name when not in above uri
|
|
132
|
+
|
|
133
|
+
// Postgres/MySQL only — connection pool tuning
|
|
134
|
+
poolMax: 10, // max pool connections (default: 10, both drivers' own default)
|
|
135
|
+
statementTimeoutMs: 30_000, // abort a query after N ms — unset by default; a forced
|
|
136
|
+
// default could break a legitimate long-running export
|
|
137
|
+
idleInTransactionTimeoutMs: 30_000, // Postgres only: kill a connection idle inside an open transaction after N ms
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
// ── Auth ─────────────────────────────────────────────────────────────────────
|
|
141
|
+
auth: {
|
|
142
|
+
mode: 'local', // 'local' | 'jwt' | 'oidc' | 'saml' | 'custom'
|
|
143
|
+
adminEmail: 'user@consenti.dev',
|
|
144
|
+
adminPassword: process.env.CONSENTI_ADMIN_PASSWORD ?? "Consenti@123",
|
|
145
|
+
jwtSecret: process.env.CONSENTI_ADMIN_JWT_SECRET, // auto-generated if omitted (sessions expire on restart)
|
|
146
|
+
|
|
147
|
+
// OIDC (Auth0, Keycloak, Google, etc.)
|
|
148
|
+
// mode: 'oidc',
|
|
149
|
+
// oidc: {
|
|
150
|
+
// issuer: 'https://your-idp.example.com',
|
|
151
|
+
// clientId: 'your-client-id',
|
|
152
|
+
// clientSecret: process.env.OIDC_SECRET,
|
|
153
|
+
// redirectUri: 'https://app.example.com/consenti/admin/auth/oidc/callback',
|
|
154
|
+
// claimsMapping: { email: 'email', roles: 'consenti_roles' },
|
|
155
|
+
// },
|
|
156
|
+
|
|
157
|
+
// SAML
|
|
158
|
+
// mode: 'saml',
|
|
159
|
+
// saml: {
|
|
160
|
+
// issuer: 'https://idp.example.com',
|
|
161
|
+
// entryPoint: 'https://idp.example.com/sso/saml',
|
|
162
|
+
// cert: process.env.SAML_CERT!, // IdP signing cert (PEM, no headers)
|
|
163
|
+
// callbackUrl: 'https://app.example.com/consenti/admin/auth/saml/callback',
|
|
164
|
+
// },
|
|
165
|
+
|
|
166
|
+
// Custom — bring your own authentication function
|
|
167
|
+
// mode: 'custom',
|
|
168
|
+
// validateUser: async (req: Request) => AdminUser | null,
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
// ── Routing ──────────────────────────────────────────────────────────────────
|
|
172
|
+
basePath: '/consenti', // URL prefix for all routes (default: '/consenti')
|
|
173
|
+
dashboard: true, // serve built-in admin SPA at basePath (default: true)
|
|
174
|
+
|
|
175
|
+
// ── Branding ─────────────────────────────────────────────────────────────────
|
|
176
|
+
branding: {
|
|
177
|
+
appName: 'Consenti', // shown in dashboard header, login page, browser tab
|
|
178
|
+
appLogoPath: './logo-dark.svg', // local file path or https:// URL; auto-served as static asset
|
|
179
|
+
hidePoweredBy: false, // true = hide "Powered by Consenti" badge
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// ── Rate limiting ─────────────────────────────────────────────────────────────
|
|
183
|
+
rateLimit: {
|
|
184
|
+
enabled: true,
|
|
185
|
+
windowMs: 60_000, // rolling window in ms (default: 60 000)
|
|
186
|
+
maxRequests: 60, // max requests per IP per window (default: 60)
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// ── Compliance ───────────────────────────────────────────────────────────────
|
|
190
|
+
compliance: {
|
|
191
|
+
// Geo-based auto routing (recommended)
|
|
192
|
+
type: 'auto', // 'auto' = geo-resolve per visitor | ComplianceGroupId = one fixed group globally
|
|
193
|
+
geoDataProvider: 'default', // 'default' (timezone+language heuristic, zero-dep) | 'hosted-geoip-lite' (no install, calls ipinfo.io) | 'geoip' (optional peer dep) | 'maxmind' (optional peer dep) | CountryResolverFn
|
|
194
|
+
autoComplianceMap: 'default',// 'default' = embedded 240-country map | operator object | 'auto' = fetch from complianceMapUrl
|
|
195
|
+
// complianceMapUrl: 'https://consenti.dev/data/v1/compliance-map.json', // used when autoComplianceMap: 'auto'
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
// ── S3 profile file sync (optional) ─────────────────────────────────────────
|
|
199
|
+
// When enabled, every locale JSON written to disk is also PUT to S3, plus a small
|
|
200
|
+
// pointer.json per compliance group ({ profileId, version }) written on activate —
|
|
201
|
+
// S3 has no symlink equivalent to the local hot-serve swap, so this pointer plays
|
|
202
|
+
// the same role. Consenti's own /resolve-profile route does NOT read from S3 or
|
|
203
|
+
// resolve this pointer — it's a contract for an external CDN/edge function to use
|
|
204
|
+
// if you want reads to bypass the Node process entirely.
|
|
205
|
+
s3Api: {
|
|
206
|
+
enabled: false,
|
|
207
|
+
region: 'us-east-1',
|
|
208
|
+
bucketName: 'consenti-profiles',
|
|
209
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
210
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
211
|
+
// sessionToken: process.env.AWS_SESSION_TOKEN, // for temporary credentials
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
// ── Cache invalidation hook (optional) ──────────────────────────────────────
|
|
215
|
+
// Called after every profile creation, edit, activation, deactivation, or delete.
|
|
216
|
+
// isPurge=false = warm the cache at these paths; isPurge=true = purge.
|
|
217
|
+
handleCache: (paths: string[], profileId: string, isPurge: boolean) => {
|
|
218
|
+
// paths: array of file paths that were written or removed
|
|
219
|
+
// profileId: id of the profile that triggered this write (stable across edits — see Profile Edit History)
|
|
220
|
+
// isPurge: true when files were removed (deactivate/delete); false when written (activate/create)
|
|
221
|
+
// Example: purge CDN edge nodes, update nginx cache keys, etc.
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
// ── IAB TCF v2.2 ─────────────────────────────────────────────────────────────
|
|
225
|
+
tcf: {
|
|
226
|
+
enabled: false,
|
|
227
|
+
cmpId: 123, // your IAB-registered CMP ID (required when enabled)
|
|
228
|
+
cmpVersion: 1, // your CMP software version
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
// ── Age gate ─────────────────────────────────────────────────────────────────
|
|
232
|
+
// The actual gating prompt is widget-side (@consenti/ui README, "Age Gate" section) — this
|
|
233
|
+
// block only needs to exist so `ageVerified`/`parentalConsentToken` land in consent records;
|
|
234
|
+
// there's a matching `compliance.ageGate` block in the widget config with the same shape.
|
|
235
|
+
ageGate: {
|
|
236
|
+
enabled: false,
|
|
237
|
+
minimumAge: 13, // COPPA = 13; GDPR Article 8 = 16
|
|
238
|
+
requireParentalConsent: false,
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// ── Data retention ───────────────────────────────────────────────────────────
|
|
242
|
+
dataRetention: {
|
|
243
|
+
purgeAfterDays: 365, // delete consent records older than N days; runs nightly
|
|
244
|
+
auditLogPurgeAfterDays: 730, // optional, independent from purgeAfterDays — unset = keep forever
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
// ── Multi-tenant ─────────────────────────────────────────────────────────────
|
|
248
|
+
multiTenant: {
|
|
249
|
+
enabled: false, // scope all data to tenant from X-Tenant-ID header
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
// ── Proxies & body ───────────────────────────────────────────────────────────
|
|
253
|
+
trustedProxies: [], // IP / CIDR of reverse proxies (reads X-Forwarded-For)
|
|
254
|
+
maxBodySize: 1_048_576, // request body size limit in bytes (default: 1 MB)
|
|
255
|
+
|
|
256
|
+
// ── Plugins ──────────────────────────────────────────────────────────────────
|
|
257
|
+
plugins: [], // ConsentiServerPlugin instances
|
|
258
|
+
})
|
|
259
|
+
```
|
|
260
|
+
|
|
60
261
|
## Quick Start
|
|
61
262
|
|
|
62
263
|
### Express
|
|
@@ -68,7 +269,7 @@ import { createConsenti } from '@consenti/api'
|
|
|
68
269
|
const app = express()
|
|
69
270
|
|
|
70
271
|
const consenti = createConsenti({
|
|
71
|
-
storage: { driver: 'sqlite', path: './consenti
|
|
272
|
+
storage: { driver: 'node:sqlite', path: './consenti-data' },
|
|
72
273
|
auth: {
|
|
73
274
|
mode: 'local',
|
|
74
275
|
adminEmail: 'admin@example.com',
|
|
@@ -89,10 +290,11 @@ app.listen(3000, () => {
|
|
|
89
290
|
### Standalone (no framework)
|
|
90
291
|
|
|
91
292
|
```ts
|
|
293
|
+
import { createServer } from 'node:http'
|
|
92
294
|
import { createConsenti } from '@consenti/api'
|
|
93
295
|
|
|
94
296
|
const consenti = createConsenti({
|
|
95
|
-
storage: { driver: 'sqlite', path: './consenti
|
|
297
|
+
storage: { driver: 'node:sqlite', path: './consenti-data' },
|
|
96
298
|
auth: {
|
|
97
299
|
mode: 'local',
|
|
98
300
|
adminEmail: 'admin@example.com',
|
|
@@ -101,9 +303,13 @@ const consenti = createConsenti({
|
|
|
101
303
|
dashboard: true,
|
|
102
304
|
})
|
|
103
305
|
|
|
104
|
-
await consenti.
|
|
105
|
-
|
|
106
|
-
|
|
306
|
+
await consenti.ready // wait for DB + bootstrap
|
|
307
|
+
|
|
308
|
+
const server = createServer(consenti.handler)
|
|
309
|
+
server.listen(3001, () => {
|
|
310
|
+
console.log('Admin dashboard → http://localhost:3001/consenti/')
|
|
311
|
+
console.log('REST API → http://localhost:3001/consenti/api/v1/')
|
|
312
|
+
})
|
|
107
313
|
```
|
|
108
314
|
|
|
109
315
|
---
|
|
@@ -122,14 +328,13 @@ app.use(consenti.handler) // as a terminal handler (no next())
|
|
|
122
328
|
|
|
123
329
|
```ts
|
|
124
330
|
import Fastify from 'fastify'
|
|
125
|
-
|
|
331
|
+
import { createConsenti } from '@consenti/api'
|
|
126
332
|
|
|
333
|
+
const fastify = Fastify()
|
|
127
334
|
const consenti = createConsenti({ /* ... */ })
|
|
128
335
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
consenti.handler(req.raw, reply.raw)
|
|
132
|
-
})
|
|
336
|
+
await fastify.register(consenti.fastifyHandler)
|
|
337
|
+
await fastify.listen({ port: 3000 })
|
|
133
338
|
```
|
|
134
339
|
|
|
135
340
|
### Next.js App Router
|
|
@@ -144,7 +349,7 @@ let consenti: Awaited<ReturnType<typeof createConsenti>>
|
|
|
144
349
|
async function getConsenti() {
|
|
145
350
|
if (!consenti) {
|
|
146
351
|
consenti = createConsenti({
|
|
147
|
-
storage: { driver: 'sqlite', path: './consenti
|
|
352
|
+
storage: { driver: 'sqlite', path: './consenti-data' },
|
|
148
353
|
auth: {
|
|
149
354
|
mode: 'local',
|
|
150
355
|
adminEmail: process.env.CONSENTI_ADMIN_EMAIL!,
|
|
@@ -155,12 +360,12 @@ async function getConsenti() {
|
|
|
155
360
|
return consenti
|
|
156
361
|
}
|
|
157
362
|
|
|
158
|
-
|
|
363
|
+
async function consentiHandler(req: NextRequest) {
|
|
159
364
|
const c = await getConsenti()
|
|
160
365
|
return c.handleRequest(req)
|
|
161
366
|
}
|
|
162
367
|
|
|
163
|
-
export {
|
|
368
|
+
export { consentiHandler as POST, consentiHandler as PUT, consentiHandler as DELETE, consentiHandler as PATCH, consentiHandler as GET }
|
|
164
369
|
```
|
|
165
370
|
|
|
166
371
|
### Hono / Fetch-based
|
|
@@ -171,117 +376,90 @@ return consenti.honoApp.fetch(request)
|
|
|
171
376
|
|
|
172
377
|
---
|
|
173
378
|
|
|
174
|
-
##
|
|
379
|
+
## First Boot
|
|
175
380
|
|
|
176
|
-
|
|
177
|
-
import { createConsenti } from '@consenti/api'
|
|
381
|
+
On first start with JSON, Consenti:
|
|
178
382
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
path: './consenti.db', // SQLite only — file path
|
|
184
|
-
|
|
185
|
-
// PostgreSQL
|
|
186
|
-
// driver: 'pg',
|
|
187
|
-
// url: 'postgresql://user:pass@localhost:5432/consenti',
|
|
188
|
-
// pool: { max: 10, idleTimeoutMillis: 30000 },
|
|
189
|
-
|
|
190
|
-
// MongoDB
|
|
191
|
-
// driver: 'mongodb',
|
|
192
|
-
// url: 'mongodb://localhost:27017',
|
|
193
|
-
// dbName: 'consenti',
|
|
194
|
-
|
|
195
|
-
// MySQL
|
|
196
|
-
// driver: 'mysql',
|
|
197
|
-
// url: 'mysql://user:pass@localhost:3306/consenti',
|
|
198
|
-
},
|
|
199
|
-
auth: {
|
|
200
|
-
mode: 'local', // 'local' | 'oauth' | 'saml' | 'none'
|
|
201
|
-
adminEmail: 'admin@example.com',
|
|
202
|
-
adminPassword: process.env.CONSENTI_ADMIN_PASSWORD!,
|
|
203
|
-
jwtSecret: process.env.CONSENTI_ADMIN_JWT_SECRET, // auto-generated if omitted (changes on restart)
|
|
204
|
-
sessionTtl: 86400, // JWT TTL in seconds (default: 24 h)
|
|
383
|
+
1. Creates the storage directory at the configured path (see [Storage Directory Structure](#storage-directory-structure))
|
|
384
|
+
2. Runs all migrations (creates `visitors`, `consent_records`, `consent_history`, `profiles`, `admin_users`, `audit_logs` tables, and DB indexes)
|
|
385
|
+
3. Creates the admin user from `auth.adminEmail` + `auth.adminPassword`
|
|
386
|
+
4. Seeds the default profile (`profileId: '0'`)
|
|
205
387
|
|
|
206
|
-
|
|
207
|
-
// mode: 'oauth',
|
|
208
|
-
// oidc: {
|
|
209
|
-
// issuer: 'https://your-idp.example.com',
|
|
210
|
-
// clientId: 'your-client-id',
|
|
211
|
-
// clientSecret: process.env.OIDC_SECRET,
|
|
212
|
-
// redirectUri: 'https://app.example.com/consenti/admin/auth/callback',
|
|
213
|
-
// },
|
|
388
|
+
Subsequent starts run only pending migrations.
|
|
214
389
|
|
|
215
|
-
|
|
216
|
-
// mode: 'saml',
|
|
217
|
-
// saml: {
|
|
218
|
-
// issuer: 'consenti',
|
|
219
|
-
// entryPoint: 'https://idp.example.com/sso/saml',
|
|
220
|
-
// cert: process.env.SAML_CERT,
|
|
221
|
-
// callbackUrl: 'https://app.example.com/consenti/admin/auth/saml/callback',
|
|
222
|
-
// },
|
|
390
|
+
### Default Profile Seeding
|
|
223
391
|
|
|
224
|
-
|
|
225
|
-
// mode: 'custom',
|
|
226
|
-
// validateUser: async (req) => ({ id, email, roles, permissions } | null),
|
|
227
|
-
},
|
|
392
|
+
`ProfileService` exposes two idempotent helpers for seeding the 8 built-in compliance groups:
|
|
228
393
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
trustProxy: false, // true when behind nginx / Cloudflare
|
|
394
|
+
```ts
|
|
395
|
+
// Seed one group (skips if an active profile already exists for that group)
|
|
396
|
+
await profileService.seedDefaultProfile('opt-in')
|
|
233
397
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
maxRequests: 60, // max requests per IP per window (default: 60)
|
|
238
|
-
},
|
|
398
|
+
// Seed all 8 groups in parallel
|
|
399
|
+
await profileService.seedAllDefaults()
|
|
400
|
+
```
|
|
239
401
|
|
|
240
|
-
|
|
241
|
-
origins: ['https://yoursite.com'], // never use '*' in production
|
|
242
|
-
credentials: true,
|
|
243
|
-
},
|
|
402
|
+
Each seeded profile is built from the English base in `@consenti/utils/profiles` and automatically includes translated text overlays for **German (de), Spanish (es), French (fr), and Japanese (ja)** — 5 locales total per group, 40 locale files across all 8 groups.
|
|
244
403
|
|
|
245
|
-
|
|
246
|
-
gdpr: true, // opt-in enforcement
|
|
247
|
-
ccpa: false, // opt-out model
|
|
248
|
-
gpc: true, // store gpc_detected on records
|
|
249
|
-
tcf: {
|
|
250
|
-
enabled: false, // IAB TCF v2.2
|
|
251
|
-
cmpId: 123, // your registered CMP ID
|
|
252
|
-
},
|
|
253
|
-
},
|
|
404
|
+
Locale coverage per compliance group:
|
|
254
405
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
406
|
+
| Group | en | de | es | fr | ja |
|
|
407
|
+
|-----------------------------|----|----|----|----|----|
|
|
408
|
+
| `opt-in` (GDPR) | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
409
|
+
| `opt-out` (CCPA) | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
410
|
+
| `opt-out-strict` (CPRA) | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
411
|
+
| `opt-in-dpdpa` (India) | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
412
|
+
| `opt-in-china` (PIPL) | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
413
|
+
| `opt-in-brazil` (LGPD) | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
414
|
+
| `general-privacy-consent` | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
415
|
+
| `notice-only` | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
260
416
|
|
|
261
|
-
|
|
262
|
-
purgeAfterDays: 365, // auto-purge old records (0 = disabled)
|
|
263
|
-
},
|
|
417
|
+
All 5 locales — English base plus the de/es/fr/ja overlays — live in `packages/utils/src/profiles/`, the single source of truth for default compliance-profile content shared by profile seeding, the dashboard's "Load Defaults", and `@consenti/ui`'s embedded fallback profile (English only there — see below).
|
|
264
418
|
|
|
265
|
-
|
|
266
|
-
enabled: false, // isolate data per tenant
|
|
267
|
-
},
|
|
419
|
+
### First-Run Setup Wizard
|
|
268
420
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
421
|
+
The dashboard shows a 4-step setup wizard (`#/setup`) once — the first time any admin logs in after install — like a self-hosted app's installer (WordPress, phpMyAdmin, etc.):
|
|
422
|
+
|
|
423
|
+
1. **Welcome** — links to documentation and the in-dashboard "How Consenti Works" page. Skippable.
|
|
424
|
+
2. **Your configuration** — a plain, read-only view of the fully merged `ConsentiServerConfig` (your config + env vars merged over the built-in defaults — exactly what `createConsenti` resolved at boot), with secrets redacted.
|
|
425
|
+
3. **Default compliance profiles** — an accordion of the 8 built-in compliance groups (label, description, and badges for GPC mode / TCF / CPRA categories / DPDPA disclosure), all checked by default. Continuing calls the same idempotent `seedDefaultProfile`/`seedAllDefaults` helpers described above.
|
|
426
|
+
4. **Confirmation** — a summary of what was installed, plus a production-readiness panel that surfaces the JSON-storage-driver and default-credentials warnings `createConsenti` already logs to the server console (otherwise invisible to a dashboard-only admin). CTAs: **Go to Dashboard** / **See How Consenti Works**.
|
|
427
|
+
|
|
428
|
+
Gated by `tenant_settings.setup_completed` (per-tenant in multi-tenant mode) — set once the wizard finishes **or** is skipped, and never reset from the dashboard afterward. There is no "run it again" entry point; it's a one-time first-run flow, not a recurring settings screen: navigating to `#/setup` directly once setup is complete redirects to the dashboard instead of reopening it, and the two mutating routes (`seed-profiles`, `complete`) reject with `409` if called again after completion — enforced both in the router and on the server, not just by hiding the nav entry. Backed by 5 admin routes under `/setup/*` (see [Admin REST API](#admin-rest-api)).
|
|
272
429
|
|
|
273
430
|
---
|
|
274
431
|
|
|
275
|
-
##
|
|
432
|
+
## Storage Directory Structure
|
|
276
433
|
|
|
277
|
-
|
|
434
|
+
`storage.path` is a **directory path**. Consenti creates the following layout automatically:
|
|
278
435
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
436
|
+
```
|
|
437
|
+
${storage.path}/
|
|
438
|
+
db/
|
|
439
|
+
consenti-data.json # JSON adapter
|
|
440
|
+
consenti.db # SQLite adapters
|
|
441
|
+
profiles/
|
|
442
|
+
${tenantId}/
|
|
443
|
+
${profileId}/
|
|
444
|
+
1/ # version 1 (immutable once written)
|
|
445
|
+
en.json
|
|
446
|
+
fr-FR.json
|
|
447
|
+
default.json # copy of defaultLocale content (fallback)
|
|
448
|
+
2/ # version 2 written on next save
|
|
449
|
+
…
|
|
450
|
+
${complianceGroup}/ # → symlink to the active profile's version dir (hot-serve path)
|
|
451
|
+
logs/ # reserved for future structured logging
|
|
452
|
+
```
|
|
283
453
|
|
|
284
|
-
|
|
454
|
+
**Key rules:**
|
|
455
|
+
- Every profile save writes to a new immutable version directory under `${profileId}/${version}/`
|
|
456
|
+
- `${complianceGroup}/` only exists when a profile is **active** for that group — it's a directory symlink (junction on Windows), not a copy
|
|
457
|
+
- On **activate**: `${complianceGroup}/` is atomically repointed at `${profileId}/${version}/` — one filesystem rename, no window where it's missing or a mix of two versions, and no leftover files from a version whose locale set has since shrunk
|
|
458
|
+
- On **deactivate** / **delete**: the `${complianceGroup}/` symlink is removed (the version directory it pointed to is untouched)
|
|
459
|
+
- `default.json` = full resolved profile for `defaultLocale`; used as locale fallback (303 redirect)
|
|
460
|
+
- Windows: symlinks are created as directory **junctions** — no admin rights or Developer Mode required, but the volume must be NTFS and `storage.path` must stay on the same drive
|
|
461
|
+
|
|
462
|
+
**Backward compat:** If `storage.path` has a file extension (`.json`, `.db`), the parent directory is used and a deprecation warning is logged.
|
|
285
463
|
|
|
286
464
|
---
|
|
287
465
|
|
|
@@ -289,15 +467,64 @@ Subsequent starts run only pending migrations.
|
|
|
289
467
|
|
|
290
468
|
Base path: `/consenti/api/v1` (change with `basePath` in config). No authentication required.
|
|
291
469
|
|
|
292
|
-
| Method | Path
|
|
293
|
-
|
|
294
|
-
| `GET` | `/profiles/:
|
|
295
|
-
| `GET` | `/profiles/:
|
|
296
|
-
| `
|
|
297
|
-
| `GET` | `/
|
|
298
|
-
| `
|
|
299
|
-
| `
|
|
300
|
-
| `
|
|
470
|
+
| Method | Path | Description |
|
|
471
|
+
|------------|---------------------------------------------|------------------------------------------------------------------------|
|
|
472
|
+
| `GET` | `/profiles/:tenantId/:complianceGroup/:locale` | **Hot-serve** — serve locale JSON directly from disk (zero DB) |
|
|
473
|
+
| `GET` | `/profiles/:tenantId/:profileId/:version/:locale` | Serve a specific version locale JSON (dashboard preview) |
|
|
474
|
+
| `GET` | `/resolve-profile` | Geo-resolve compliance group, return file path + locale |
|
|
475
|
+
| `GET` | `/profiles/:id` | get profile by ID (resolved, DB-backed, default tenant, default locale)|
|
|
476
|
+
| `GET` | `/profiles/:id/:locale` | get profile by ID & locale (resolved, DB-backed, default tenant) |
|
|
477
|
+
| `GET` | `/profiles/auto/:locale` | Legacy: geo-resolve then return matching active profile |
|
|
478
|
+
| `POST` | `/consent` | Submit a consent record |
|
|
479
|
+
| `GET` | `/consent/:visitorId` | Get current consent for a visitor |
|
|
480
|
+
| `PUT` | `/consent/:visitorId` | Update consent for a visitor |
|
|
481
|
+
| `DELETE` | `/consent/:visitorId` | GDPR right-to-erasure — delete all visitor data |
|
|
482
|
+
| `GET` | `/consent/:visitorId/verify` | Check if consent is still valid |
|
|
483
|
+
|
|
484
|
+
### Static file serving (hot path)
|
|
485
|
+
|
|
486
|
+
```http
|
|
487
|
+
GET /consenti/api/v1/profiles/default/opt-in/en
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Serves `${storage.path}/profiles/default/opt-in/en.json` directly from disk. No DB involved.
|
|
491
|
+
|
|
492
|
+
If the locale file doesn't exist, returns `303 See Other → .../opt-in/default` (which serves `default.json` = `defaultLocale` content).
|
|
493
|
+
|
|
494
|
+
Cache headers: `public, max-age=3600, stale-while-revalidate=60` — suitable for CDN caching.
|
|
495
|
+
|
|
496
|
+
### Resolve profile (for `compliance.type: 'auto'`)
|
|
497
|
+
|
|
498
|
+
```http
|
|
499
|
+
GET /consenti/api/v1/resolve-profile?data=<base64-encoded-GeoHints>
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
For widgets using `compliance.type: 'auto'` — called once on page load to discover which compliance group and static profile file to serve. The widget sends browser geo hints encoded as base64 JSON:
|
|
503
|
+
|
|
504
|
+
```js
|
|
505
|
+
// Widget encodes automatically via encodeGeoHints()
|
|
506
|
+
const data = btoa(JSON.stringify({
|
|
507
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
508
|
+
languages: navigator.languages,
|
|
509
|
+
language: navigator.language,
|
|
510
|
+
locale: 'fr-FR',
|
|
511
|
+
}))
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Response:
|
|
515
|
+
|
|
516
|
+
```json
|
|
517
|
+
{
|
|
518
|
+
"path": "/consenti/api/v1/profiles/default/opt-in/fr-FR",
|
|
519
|
+
"complianceGroup": "opt-in",
|
|
520
|
+
"locale": "fr-FR",
|
|
521
|
+
"found": true
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
When `found` is `false` (no active profile for this tenant/group), `path` is `null` and the widget automatically falls back to the embedded profile for the resolved `complianceGroup`. This means the widget works even before you have seeded profiles for every compliance group.
|
|
526
|
+
|
|
527
|
+
Legacy query params `?tz=`, `?lang=`, `?locale=` are still accepted for backward compatibility — the `data` param takes priority when both are present.
|
|
301
528
|
|
|
302
529
|
### Submit consent
|
|
303
530
|
|
|
@@ -313,13 +540,16 @@ Content-Type: application/json
|
|
|
313
540
|
"marketing": "denied"
|
|
314
541
|
},
|
|
315
542
|
"visitorId": "uuid-v4",
|
|
316
|
-
"profileVersion": 3,
|
|
317
543
|
"locale": "en",
|
|
318
544
|
"gpcDetected": false,
|
|
319
|
-
"source": "banner"
|
|
545
|
+
"source": "banner",
|
|
546
|
+
"ageVerified": true,
|
|
547
|
+
"parentalConsentToken": "pcon_..."
|
|
320
548
|
}
|
|
321
549
|
```
|
|
322
550
|
|
|
551
|
+
`ageVerified`/`parentalConsentToken` are optional — only sent by the widget when `compliance.ageGate.enabled: true` (see the `@consenti/ui` README's "Age Gate" section). Both are stored as-is; this endpoint doesn't validate or enforce them.
|
|
552
|
+
|
|
323
553
|
Response `201`:
|
|
324
554
|
|
|
325
555
|
```json
|
|
@@ -327,7 +557,6 @@ Response `201`:
|
|
|
327
557
|
"id": "record-uuid",
|
|
328
558
|
"visitorId": "visitor-uuid",
|
|
329
559
|
"profileId": "my-profile-id",
|
|
330
|
-
"profileVersion": 3,
|
|
331
560
|
"locale": "en",
|
|
332
561
|
"consentJson": { "necessary": "granted", "analytics": "granted", "marketing": "denied" },
|
|
333
562
|
"gpcDetected": false,
|
|
@@ -347,10 +576,12 @@ GET /consenti/api/v1/consent/visitor-uuid/verify
|
|
|
347
576
|
{ "valid": true }
|
|
348
577
|
|
|
349
578
|
// or, when stale:
|
|
350
|
-
{ "valid": false, "reason": "
|
|
351
|
-
// other reasons: "consent_expired" | "
|
|
579
|
+
{ "valid": false, "reason": "profile_changed" }
|
|
580
|
+
// other reasons: "consent_expired" | "hmac_invalid"
|
|
352
581
|
```
|
|
353
582
|
|
|
583
|
+
`profile_changed` means the profile this consent was collected against is no longer the active profile for its compliance group (a newer edit has superseded it) — every profile edit mints a new id rather than mutating in place, so an id mismatch is what signals staleness now (there is no separate version counter).
|
|
584
|
+
|
|
354
585
|
---
|
|
355
586
|
|
|
356
587
|
## Admin REST API
|
|
@@ -366,6 +597,7 @@ Obtain a token via `POST /consenti/admin/auth/login`.
|
|
|
366
597
|
| `POST` | `/auth/login` | Authenticate — returns a JWT |
|
|
367
598
|
| `GET` | `/auth/me` | Get current authenticated user |
|
|
368
599
|
| `POST` | `/auth/logout` | Invalidate session |
|
|
600
|
+
| `POST` | `/auth/refresh` | Reissue a fresh token from the current one — extends the session |
|
|
369
601
|
|
|
370
602
|
```http
|
|
371
603
|
POST /consenti/admin/auth/login
|
|
@@ -378,15 +610,125 @@ Content-Type: application/json
|
|
|
378
610
|
{ "token": "eyJhbGciOiJIUzI1NiJ9..." }
|
|
379
611
|
```
|
|
380
612
|
|
|
613
|
+
**Session length**: admin tokens are valid for 30 minutes from issuance, not a flat expiry from
|
|
614
|
+
login — the dashboard calls `POST /auth/refresh` on user activity (mouse/keyboard/scroll/touch) to
|
|
615
|
+
extend it another 30 minutes, and proactively logs the user out the moment 30 minutes pass with no
|
|
616
|
+
activity at all, rather than leaving the session to silently lapse in the background.
|
|
617
|
+
|
|
381
618
|
### Profiles
|
|
382
619
|
|
|
383
|
-
| Method | Path
|
|
384
|
-
|
|
385
|
-
| `GET` | `/profiles`
|
|
386
|
-
| `
|
|
387
|
-
| `
|
|
388
|
-
| `
|
|
389
|
-
| `
|
|
620
|
+
| Method | Path | Description |
|
|
621
|
+
|------------|----------------------------------|-------------------------------------------------------------------------|
|
|
622
|
+
| `GET` | `/profiles` | List all profiles |
|
|
623
|
+
| `GET` | `/profiles?summary=1` | List profiles as `ProfileSummary[]` (no blob, with template names) |
|
|
624
|
+
| `POST` | `/profiles` | Create a profile — 422 on missing mandatory content or compliance errors; `{ conflict, requiresChoice: true }` when an active profile exists on the same `complianceGroup` |
|
|
625
|
+
| `GET` | `/profiles/:id` | Get a profile |
|
|
626
|
+
| `PUT` | `/profiles/:id` | Edit a profile — mutates the row **in place**, increments `version`; returned `id` always matches the URL's. Same mandatory-content + compliance validation + conflict detection |
|
|
627
|
+
| `DELETE` | `/profiles/:id` | Delete a profile |
|
|
628
|
+
| `POST` | `/profiles/:id/copy` | Duplicate a profile as a new, always-inactive profile (`version` resets to 1). Optional `{ name }` body, defaults to `Copy of {name}` |
|
|
629
|
+
| `POST` | `/profiles/:id/activate` | Activate a profile — writes locale JSON files to `${complianceGroup}/` |
|
|
630
|
+
| `POST` | `/profiles/:id/deactivate` | Deactivate a profile — removes `${complianceGroup}/` locale files |
|
|
631
|
+
| `GET` | `/profiles/:id/versions` | List every version snapshot on disk, newest first (`{ version, createdAt, locales[] }[]`) |
|
|
632
|
+
| `GET` | `/profiles/:id/versions/:entryId` | Read a specific version's locale file, `:entryId` is its `version` number (query: `?locale=en`) |
|
|
633
|
+
| `POST` | `/profiles/validate` | Validate a consent template (`cookies` + `categories`) against a compliance group (no save) |
|
|
634
|
+
| `GET` | `/compliance-coverage` | Active profile (or null) per compliance group — powers coverage panel |
|
|
635
|
+
|
|
636
|
+
#### Profile Save Conflict Detection
|
|
637
|
+
|
|
638
|
+
Only one profile per `complianceGroup` can be active at a time. If you attempt to create or update a profile with `isActive: true` and another active profile already exists for the same `complianceGroup`, the backend returns **200** (not an error) with:
|
|
639
|
+
|
|
640
|
+
```json
|
|
641
|
+
{
|
|
642
|
+
"conflict": { "id": "existing-profile-uuid", "name": "GDPR Profile v1" },
|
|
643
|
+
"requiresChoice": true
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
Re-submit the original request with a `choice` field to resolve:
|
|
648
|
+
|
|
649
|
+
```json
|
|
650
|
+
{ "choice": "deactivate" } // deactivate the conflicting profile, then save this one as active
|
|
651
|
+
{ "choice": "inactive" } // save this profile as inactive (no conflict)
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
The dashboard profile editor shows a three-button popup when `requiresChoice: true` — "Deactivate existing and activate this one", "Save as inactive", and "Cancel".
|
|
655
|
+
|
|
656
|
+
#### ProfileSummary shape
|
|
657
|
+
|
|
658
|
+
```ts
|
|
659
|
+
{
|
|
660
|
+
id: string
|
|
661
|
+
name: string
|
|
662
|
+
defaultLocale: string
|
|
663
|
+
complianceGroup: ComplianceGroupId | null
|
|
664
|
+
customComplianceGroup: string | null
|
|
665
|
+
isActive: boolean
|
|
666
|
+
consentTemplateName: string | null
|
|
667
|
+
uiTemplateName: string | null
|
|
668
|
+
createdAt: string
|
|
669
|
+
updatedAt: string
|
|
670
|
+
}
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Consent Templates
|
|
674
|
+
|
|
675
|
+
A Consent Template merges what used to be two concerns — the parameter (cookie) list and its categories — into one entity. Categories own legal basis; a parameter's effective legal basis is derived from whichever category lists it (a parameter must belong to exactly one category).
|
|
676
|
+
|
|
677
|
+
| Method | Path | Description |
|
|
678
|
+
|------------|---------------------------------------------|----------------------------------------------------------|
|
|
679
|
+
| `GET` | `/consent-templates` | List consent templates |
|
|
680
|
+
| `GET` | `/consent-templates/:id` | Get a consent template |
|
|
681
|
+
| `POST` | `/consent-templates` | Create a consent template — `{ name, cookies: CookieMap, categories: CategoryMap }` |
|
|
682
|
+
| `PUT` | `/consent-templates/:id` | Update a consent template — 422 if the change breaks compliance for a dependent profile |
|
|
683
|
+
| `DELETE` | `/consent-templates/:id` | Delete a consent template — 422 if any active profiles use it |
|
|
684
|
+
| `POST` | `/consent-templates/:id/copy` | Duplicate a consent template |
|
|
685
|
+
| `GET` | `/consent-templates/:id/profile-usage` | List profiles using this template (`ProfileSummary[]`) |
|
|
686
|
+
|
|
687
|
+
`CookieMap`/`CategoryMap` are keyed by id (`Record<string, Cookie>` / `Record<string, Category>`) — the id is the map key, not a field on the value.
|
|
688
|
+
|
|
689
|
+
#### Consent Template Safety Guards
|
|
690
|
+
|
|
691
|
+
**Deletion guard:** `DELETE /consent-templates/:id` returns `422` with `{ profiles: ProfileSummary[] }` when one or more active profiles reference the template. Deactivate those profiles first, then delete.
|
|
692
|
+
|
|
693
|
+
**Compliance guard:** When updating a template (`PUT`) with new `cookies` and/or `categories`, the backend re-runs compliance validation (using the *new* set) against every profile currently using the template. If any profile's `complianceGroup` rules would be violated, the response is `422` with:
|
|
694
|
+
|
|
695
|
+
```json
|
|
696
|
+
{
|
|
697
|
+
"blockingProfiles": [{ "id": "...", "name": "...", "complianceGroup": "opt-in" }]
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
Profiles listed in `blockingProfiles` must be updated (switch template or change complianceGroup) before the change can be saved.
|
|
702
|
+
|
|
703
|
+
### UI Templates
|
|
704
|
+
|
|
705
|
+
| Method | Path | Description |
|
|
706
|
+
|------------|---------------------------------------------|----------------------------------------------------------|
|
|
707
|
+
| `GET` | `/ui-templates` | List UI templates |
|
|
708
|
+
| `GET` | `/ui-templates/:id` | Get a UI template |
|
|
709
|
+
| `POST` | `/ui-templates` | Create a UI template |
|
|
710
|
+
| `PUT` | `/ui-templates/:id` | Update a UI template |
|
|
711
|
+
| `DELETE` | `/ui-templates/:id` | Delete a UI template |
|
|
712
|
+
| `POST` | `/ui-templates/:id/copy` | Duplicate a UI template |
|
|
713
|
+
| `GET` | `/ui-templates/:id/profile-usage` | List profiles using this template (`ProfileSummary[]`) |
|
|
714
|
+
|
|
715
|
+
### Analytics
|
|
716
|
+
|
|
717
|
+
| Method | Path | Description |
|
|
718
|
+
|--------|-------------------------|----------------------------------------------------|
|
|
719
|
+
| `GET` | `/analytics/opt-in` | Opt-in rate stats aggregated by locale and date |
|
|
720
|
+
|
|
721
|
+
Query params: `tenantId`, `profileId`, `complianceGroup`, `from` (ISO date), `to` (ISO date), `locale`.
|
|
722
|
+
|
|
723
|
+
```json
|
|
724
|
+
{
|
|
725
|
+
"total": 12400,
|
|
726
|
+
"granted": 6800, "denied": 4100, "managed": 1500,
|
|
727
|
+
"grantedPct": 54.8, "deniedPct": 33.1, "managedPct": 12.1,
|
|
728
|
+
"byLocale": { "en": { "total": 8000, "granted": 4400 } },
|
|
729
|
+
"byDate": [{ "date": "2026-07-01", "total": 400, "granted": 220 }]
|
|
730
|
+
}
|
|
731
|
+
```
|
|
390
732
|
|
|
391
733
|
### Consents
|
|
392
734
|
|
|
@@ -396,7 +738,7 @@ Content-Type: application/json
|
|
|
396
738
|
| `GET` | `/consents/:visitorId` | Get consent record for a visitor |
|
|
397
739
|
| `GET` | `/consents/:visitorId/history` | Get consent change history |
|
|
398
740
|
|
|
399
|
-
Query params for `GET /consents`: `page`, `limit
|
|
741
|
+
Query params for `GET /consents`: `page`, `limit` (max 500), `profileId`, `from`, `to`, `q` (**prefix** search across visitorId, profileId, locale, source — matches from the start of the field, not a substring anywhere in it). Response: `{ items, total, page, limit }`. List rows omit `consentJson`/`parentalConsentToken`/`tcfString`/`signature` — fetch `GET /consents/:visitorId` for the full record.
|
|
400
742
|
|
|
401
743
|
### Visitors
|
|
402
744
|
|
|
@@ -404,48 +746,118 @@ Query params for `GET /consents`: `page`, `limit`, `profileId`, `from`, `to`.
|
|
|
404
746
|
|--------|----------------|-------------------------------------|
|
|
405
747
|
| `GET` | `/visitors` | List visitor records (paginated) |
|
|
406
748
|
|
|
749
|
+
Query params for `GET /visitors`: `page`, `limit` (max 500), `from`, `to`, `q` (**prefix** search across visitorId, country). Response: `{ items, total, page, limit }`.
|
|
750
|
+
|
|
407
751
|
IPs are never stored raw — only SHA-256 hashes appear in the `ipHash` field.
|
|
408
752
|
|
|
409
753
|
### Users & Roles
|
|
410
754
|
|
|
411
|
-
| Method | Path | Description
|
|
412
|
-
|
|
413
|
-
| `GET` | `/users` | List admin users
|
|
414
|
-
| `POST` | `/users` | Create an admin user
|
|
415
|
-
| `PUT` | `/users/:id` | Update an admin user
|
|
416
|
-
| `GET` | `/roles` | List roles
|
|
417
|
-
| `POST` | `/roles` | Create a role
|
|
418
|
-
| `GET` | `/roles/:id/permissions` | Get permissions for a role
|
|
419
|
-
| `POST` | `/roles/:id/permissions` | Assign a permission to a role
|
|
420
|
-
| `DELETE` | `/roles/:id/permissions/:permId` | Revoke a permission from a role
|
|
421
|
-
| `GET` | `/permissions` | List all available permissions
|
|
755
|
+
| Method | Path | Description |
|
|
756
|
+
|------------|-----------------------------------|------------------------------------------------------|
|
|
757
|
+
| `GET` | `/users` | List admin users |
|
|
758
|
+
| `POST` | `/users` | Create an admin user |
|
|
759
|
+
| `PUT` | `/users/:id` | Update an admin user |
|
|
760
|
+
| `GET` | `/roles` | List roles |
|
|
761
|
+
| `POST` | `/roles` | Create a role |
|
|
762
|
+
| `GET` | `/roles/:id/permissions` | Get permissions for a role |
|
|
763
|
+
| `POST` | `/roles/:id/permissions` | Assign a permission to a role |
|
|
764
|
+
| `DELETE` | `/roles/:id/permissions/:permId` | Revoke a permission from a role |
|
|
765
|
+
| `GET` | `/permissions` | List all available permissions |
|
|
766
|
+
|
|
767
|
+
#### User `allowedTenants`
|
|
768
|
+
|
|
769
|
+
Admin users can be scoped to specific tenants. When `allowedTenants` is set, the user can only see and manage data for those tenants:
|
|
770
|
+
|
|
771
|
+
```ts
|
|
772
|
+
// POST /admin/users or PUT /admin/users/:id
|
|
773
|
+
{
|
|
774
|
+
"name": "APAC Manager",
|
|
775
|
+
"email": "apac@example.com",
|
|
776
|
+
"password": "...",
|
|
777
|
+
"allowedTenants": ["tenant-uuid-1", "tenant-uuid-2"] // empty = access to all tenants
|
|
778
|
+
}
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
Users with `role: 'superadmin'` always have access to all tenants regardless of `allowedTenants`.
|
|
782
|
+
|
|
783
|
+
**Enforcement:** The auth middleware passes `allowedTenants` to every list query. A user whose `allowedTenants` does not include the tenant being queried receives an empty result set (not a 403), so pagination works normally. A user with an empty `allowedTenants` array has no restrictions. Routes for individual resources (`GET /consents/:visitorId`, `GET /consents/:visitorId/history`) return `403 Forbidden` instead of an empty result when the tenant is not in `allowedTenants`.
|
|
784
|
+
|
|
785
|
+
The **Users** dashboard page includes an **Edit** button on each user row (visible to users with `user:update` permission) that opens a modal for updating `allowedTenants` without changing the user's password or role.
|
|
422
786
|
|
|
423
787
|
### API Keys
|
|
424
788
|
|
|
425
|
-
| Method | Path
|
|
426
|
-
|
|
427
|
-
| `GET` | `/apikeys`
|
|
428
|
-
| `POST` | `/apikeys`
|
|
429
|
-
| `DELETE` | `/apikeys/:id`
|
|
789
|
+
| Method | Path | Description |
|
|
790
|
+
|------------|----------------------------|------------------------------------------------------------------|
|
|
791
|
+
| `GET` | `/apikeys` | List API keys |
|
|
792
|
+
| `POST` | `/apikeys` | Create an API key (raw key returned once — save it!) |
|
|
793
|
+
| `DELETE` | `/apikeys/:id` | Revoke an API key (soft — can be undone with `reactivate`) |
|
|
794
|
+
| `POST` | `/apikeys/:id/reactivate` | Re-enable a revoked key — same hash, no new secret to distribute |
|
|
795
|
+
| `DELETE` | `/apikeys/:id/permanent` | Permanently delete the key row — cannot be undone |
|
|
796
|
+
|
|
797
|
+
### Settings
|
|
798
|
+
|
|
799
|
+
Tenant-wide dashboard settings — the Public and Admin API origin allowlists shown on the API
|
|
800
|
+
Config page (split into two panels, one per API).
|
|
801
|
+
|
|
802
|
+
| Method | Path | Description |
|
|
803
|
+
|----------|-------------|-------------------------------------|
|
|
804
|
+
| `GET` | `/settings` | Get this tenant's settings (`{}` if none set yet) |
|
|
805
|
+
| `PATCH` | `/settings` | Update settings (partial — only sends fields being changed) |
|
|
806
|
+
|
|
807
|
+
```ts
|
|
808
|
+
// GET /consenti/admin/settings
|
|
809
|
+
{ "allowedOrigins": ["https://example.com"], "adminAllowedOrigins": ["https://dashboard.example.com"] }
|
|
810
|
+
|
|
811
|
+
// PATCH /consenti/admin/settings
|
|
812
|
+
{ "allowedOrigins": ["https://example.com", "https://foo.example.com"] }
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
**`allowedOrigins`** is the **fallback** used by `POST /consenti/api/v1/consent`'s origin check
|
|
816
|
+
when the specific profile being submitted to doesn't set its own `profileJson.allowedOrigins`
|
|
817
|
+
(profile-level, if present, always takes precedence). The public API has no auth token, so this
|
|
818
|
+
is its only access gate.
|
|
819
|
+
|
|
820
|
+
**`adminAllowedOrigins`** is an additional CORS-layer check on top of Bearer-token auth for
|
|
821
|
+
browser-originated `/consenti/admin/*` requests (server-to-server callers without an `Origin`
|
|
822
|
+
header are unaffected). Unauthenticated static assets (`widget.js`/`widget.css`) are exempt —
|
|
823
|
+
they must stay embeddable from any origin. **Be careful**: if you configure this, include the
|
|
824
|
+
dashboard's own origin or you'll lock yourself out of the dashboard along with everyone else.
|
|
825
|
+
|
|
826
|
+
Both lists accept full origins (`https://example.com`) or a wildcard subdomain pattern
|
|
827
|
+
(`*.example.com`). Empty/unset means no restriction — every origin is allowed.
|
|
828
|
+
|
|
829
|
+
### Setup Wizard
|
|
830
|
+
|
|
831
|
+
Backs the dashboard's one-time first-run wizard (see [First-Run Setup Wizard](#first-run-setup-wizard)). All routes require `settings:update`, same as Settings above.
|
|
832
|
+
|
|
833
|
+
| Method | Path | Description |
|
|
834
|
+
|--------|-------------------------------|--------------------------------------------------------------------|
|
|
835
|
+
| `GET` | `/setup/status` | `{ completed: boolean }` — whether this tenant has finished/skipped the wizard |
|
|
836
|
+
| `GET` | `/setup/config` | Resolved `ConsentiServerConfig` (secrets redacted) plus `usingJsonStorage`/`usingDefaultCredentials` readiness flags |
|
|
837
|
+
| `GET` | `/setup/compliance-groups` | The 8 built-in compliance groups with label/description/regulation metadata |
|
|
838
|
+
| `POST` | `/setup/seed-profiles` | `{ groups: string[] }` — seeds default profiles for the given groups (subset of the 8 ids; 400 on an unknown id) |
|
|
839
|
+
| `POST` | `/setup/complete` | Marks the wizard complete — called on both finish and skip; never reset from the dashboard afterward |
|
|
430
840
|
|
|
431
841
|
### Stats & Reporting
|
|
432
842
|
|
|
433
|
-
| Method | Path
|
|
434
|
-
|
|
435
|
-
| `GET` | `/stats/overview`
|
|
436
|
-
| `GET` | `/stats/timeline`
|
|
437
|
-
| `GET` | `/stats/categories`
|
|
438
|
-
| `GET` | `/stats/countries`
|
|
439
|
-
| `GET` | `/stats/gpc`
|
|
440
|
-
| `GET` | `/export/consents`
|
|
441
|
-
| `GET` | `/export/consents/xlsx`
|
|
442
|
-
| `GET` | `/export/audit`
|
|
843
|
+
| Method | Path | Description |
|
|
844
|
+
|--------|-----------------------------------|------------------------------------------|
|
|
845
|
+
| `GET` | `/stats/overview` | Total consents, visitors, GPC count |
|
|
846
|
+
| `GET` | `/stats/timeline` | Daily consent counts (`?days=30`) |
|
|
847
|
+
| `GET` | `/stats/categories` | Per-category grant/deny breakdown |
|
|
848
|
+
| `GET` | `/stats/countries` | Consent count by country |
|
|
849
|
+
| `GET` | `/stats/gpc` | GPC detection statistics |
|
|
850
|
+
| `GET` | `/export/consents` | Export consent records (CSV or JSON) |
|
|
851
|
+
| `GET` | `/export/consents/xlsx` | Export as XLSX |
|
|
852
|
+
| `GET` | `/export/audit` | Export audit log (CSV or JSON) |
|
|
853
|
+
| `GET` | `/export/translations/:profileId` | Export all translatable fields as CSV |
|
|
443
854
|
|
|
444
855
|
### Audit Log
|
|
445
856
|
|
|
446
|
-
| Method | Path
|
|
447
|
-
|
|
448
|
-
| `GET` | `/audit`
|
|
857
|
+
| Method | Path | Description |
|
|
858
|
+
|--------|--------------|---------------------------------------------------------|
|
|
859
|
+
| `GET` | `/audit` | Paginated audit log; filter by `action`, `resourceType`, date range, `q` (**prefix** search across action, resourceType, resourceId, userId). Response: `{ items, total, page, limit }`; list rows omit `oldData`/`newData` — fetch `GET /audit/:id` for the full entry. |
|
|
860
|
+
| `GET` | `/audit/:id` | Full audit log entry (`oldData`/`newData`) — list rows omit these for performance; fetch on demand |
|
|
449
861
|
|
|
450
862
|
### Multi-Tenant
|
|
451
863
|
|
|
@@ -465,7 +877,10 @@ Only active when `multiTenant.enabled: true`.
|
|
|
465
877
|
| `GET` | `/tcf/vendors` | List IAB TCF vendors |
|
|
466
878
|
| `GET` | `/tcf/purposes` | List IAB TCF purposes |
|
|
467
879
|
|
|
468
|
-
Only active when `tcf.enabled: true`.
|
|
880
|
+
Only active when `tcf.enabled: true`. `cmpId`/`cmpVersion` here must match the widget's own
|
|
881
|
+
`compliance.tcf` config (`@consenti/ui` README, "TCF" section) — both sides encode the same
|
|
882
|
+
simplified TC string. The widget installs `window.__tcfapi` itself; these two admin-only routes
|
|
883
|
+
are for the dashboard's vendor/purpose pickers, not consumed by the public widget.
|
|
469
884
|
|
|
470
885
|
---
|
|
471
886
|
|
|
@@ -473,45 +888,302 @@ Only active when `tcf.enabled: true`.
|
|
|
473
888
|
|
|
474
889
|
Served at `{basePath}/` when `dashboard: true`.
|
|
475
890
|
|
|
476
|
-
| Section
|
|
477
|
-
|
|
478
|
-
| Dashboard
|
|
479
|
-
|
|
|
480
|
-
|
|
|
481
|
-
|
|
|
482
|
-
|
|
|
483
|
-
|
|
|
484
|
-
|
|
|
485
|
-
|
|
|
486
|
-
|
|
|
487
|
-
|
|
|
488
|
-
|
|
|
489
|
-
|
|
|
891
|
+
| Section | Description |
|
|
892
|
+
|---------------------|--------------------------------------------------------------------------|
|
|
893
|
+
| Dashboard | Consent overview, timeline chart, country breakdown, GPC stats |
|
|
894
|
+
| Reports | Opt-in trend and category/locale breakdowns, date-range filter, requires `stats:view` |
|
|
895
|
+
| Profiles | Create / edit / copy / delete / activate / deactivate consent profiles |
|
|
896
|
+
| Profile History | Edit history viewer — browse every past snapshot in a profile's lineage |
|
|
897
|
+
| Consent Templates | Reusable parameter + category definitions (blank by default + Load Defaults) |
|
|
898
|
+
| UI Templates | Reusable banner + modal layout settings |
|
|
899
|
+
| Consents | Browse, filter, search, paginate, export consent records; per-visitor history; row click opens a detail modal with signature/consent-data breakdown |
|
|
900
|
+
| Visitors | Visitor list with geographic data; search, paginate; row click opens a detail modal including proof-of-notice history |
|
|
901
|
+
| Users | Admin user management with allowed-tenant scoping; search; local-auth password reset (`super_admin` only) |
|
|
902
|
+
| Roles | RBAC roles and fine-grained permission assignment |
|
|
903
|
+
| Sites | Multi-tenant site management **(superadmin only)** |
|
|
904
|
+
| TCF Vendors | IAB Global Vendor List |
|
|
905
|
+
| Audit Log | Immutable log of all admin actions; search, paginate; row click opens a detail modal with old/new data diff |
|
|
906
|
+
| Settings / API | API keys, branding, OpenAPI docs **(superadmin only)** |
|
|
907
|
+
| Setup Wizard | One-time first-run welcome / config / default-profiles / confirmation flow — see [First-Run Setup Wizard](#first-run-setup-wizard) |
|
|
908
|
+
|
|
909
|
+
### Dashboard RBAC
|
|
910
|
+
|
|
911
|
+
- **Sites** and **API** sections are visible only to users with the `super_admin` role
|
|
912
|
+
- **TCF Vendors** is visible to all authenticated users
|
|
913
|
+
- **Reports** requires the `stats:view` permission (granted by default to `super_admin`, `admin`, `compliance_officer`, and `viewer` roles)
|
|
914
|
+
- **Password reset** in the Users edit modal is shown only to `super_admin` users, and only when `auth.mode === 'local'` (JWT/OIDC/SAML/custom auth manage credentials outside Consenti, so there is nothing to reset here)
|
|
490
915
|
|
|
491
916
|
### Profile Creation Wizard
|
|
492
917
|
|
|
493
|
-
**Step 1 —
|
|
918
|
+
**Step 1 — Profile metadata:**
|
|
919
|
+
|
|
920
|
+
| Field | Type | Description |
|
|
921
|
+
|---------------------|------------------------------------------------------|-----------------------------------------------------------------------------|
|
|
922
|
+
| `name` | `string` | Human-readable profile name |
|
|
923
|
+
| `defaultLocale` | `string` (BCP 47) | Locale served when visitor locale is unavailable; written as `default.json` |
|
|
924
|
+
| `complianceGroup` | `ComplianceGroupId` | Regulation group for geo-routing (e.g. `opt-in`, `opt-out-strict`) |
|
|
925
|
+
| `customComplianceGroup` | `string` (lower-kebab-case) | Required when `complianceGroup` is unset ("None / Custom") — it's the identifier the widget's `compliance.type` config uses to fetch this profile (see [Public REST API](#public-rest-api) hot-serve path). Drives activation, deactivation, and "one active profile per group" conflict detection the same way `complianceGroup` does; no `COMPLIANCE_GROUPS` validation rules or GPC defaults apply to it, since none exist for a free-form name. |
|
|
926
|
+
| `gpcMode` | `'ignore' \| 'honor' \| 'strict'` | GPC signal handling. Overrides group default. |
|
|
927
|
+
| `expiryDays` | `number` (default `365`) | Days until stored consent expires and the visitor is asked again. Profile-wide — replaces the old per-parameter expiry field. |
|
|
928
|
+
| `darkMode` | `boolean` | Enable dark mode in the consent banner |
|
|
929
|
+
| `hidePoweredBy` | `boolean` (default `true`) | Hide "Powered by Consenti" branding link. Defaults to checked/hidden in the dashboard, matching the widget's own default for a profile that never sets this field. |
|
|
930
|
+
| `allowReceipt` | `boolean` | Allow visitors to download a PDF consent receipt |
|
|
931
|
+
| `allowedOrigins` | `string[]` | Allowlisted domains for CORS on this profile's consent endpoints |
|
|
932
|
+
| `complianceConfig` | `Record<string, string>` | Per-compliance extra config (e.g. DPDPA data fiduciary name). Only shown when the selected `complianceGroup` requires it; otherwise omitted. |
|
|
933
|
+
| `showFooterMetadata` | `boolean` | Show a metadata strip in the banner and modal footer with: Consent ID (visitor UUID), Consent Date, Profile Version, and a "Privacy Settings" link. |
|
|
934
|
+
| `enhanceAccessibility` | `boolean` | Apply WCAG 2.1 AA enhancements: 44 px min button height, 3 px focus rings. Adds `.consenti--enhanced-a11y` class to the widget root. |
|
|
935
|
+
|
|
936
|
+
**Step 2 — Consent Template:** Define parameters (cookies) and the categories that own their legal basis, edited together. Clicking **Next** at the bottom of Step 2 calls `POST /admin/profiles/validate` with the selected template's `cookies` + `categories` and the chosen `complianceGroup`:
|
|
937
|
+
|
|
938
|
+
- **Compliance errors** (red panel): block advancing to Step 3 until resolved (e.g. a category has no `legalBasis` set, or a marketing-purpose parameter is assigned to a `mandatory` category).
|
|
939
|
+
- **Compliance warnings** (amber panel): show an acknowledgment checkbox; the user must check it before advancing. Warnings do not block save — they surface potential issues (e.g. `preGrant: true` on a strict opt-in profile).
|
|
940
|
+
|
|
941
|
+
Parameter fields:
|
|
494
942
|
|
|
495
|
-
| Field | Type
|
|
496
|
-
|
|
497
|
-
| `id` | `string`
|
|
498
|
-
| `
|
|
499
|
-
| `
|
|
500
|
-
| `
|
|
501
|
-
| `
|
|
943
|
+
| Field | Type | Description |
|
|
944
|
+
|---------------------|-------------------------------------------------|---------------------------------------------------------------------------|
|
|
945
|
+
| `id` | `string` (map key) | Unique identifier referenced in button arrays and category `cookies[]` |
|
|
946
|
+
| `purpose` | `'necessary' \| 'functional' \| 'preferences' \| 'analytics' \| 'marketing'` | What the parameter is for. Required — parameter IDs are free-form, so the purpose is what integrations and compliance checks rely on. Selecting a purpose pre-fills `listenGpc` and `cpraCategory` (still editable). Known Google Consent Mode IDs (`ad_storage`, `analytics_storage`, …) auto-detect their purpose. |
|
|
947
|
+
| `listenGpc` | `boolean` | Auto-denied when GPC signal is active |
|
|
948
|
+
| `preGrant` | `boolean` | Pre-grant this parameter's default consent. Only editable when its category's `legalBasis === 'consent'` — `mandatory`/`legitimate_interest` categories are already effectively pre-granted, so the checkbox is locked checked for those. |
|
|
949
|
+
| `tcfVendorId` / `tcfPurposes` | `number` / `number[]` | IAB TCF vendor + purpose IDs associated with this parameter |
|
|
950
|
+
| `cpraCategory` | `'sale' \| 'sharing' \| 'sensitive'` | CPRA data category |
|
|
502
951
|
|
|
503
|
-
|
|
952
|
+
Category fields (own the legal basis for every parameter listed in `cookies[]` — a parameter must belong to **exactly one** category):
|
|
953
|
+
|
|
954
|
+
| Field | Type | Description |
|
|
955
|
+
|---------------------|--------------------------------------------------------|--------------------------------------------------------------------|
|
|
956
|
+
| `id` | `string` (map key) | Unique identifier |
|
|
957
|
+
| `legalBasis` | `'mandatory' \| 'consent' \| 'legitimate_interest'` | Legal basis for every parameter in `cookies[]` — replaces the old per-parameter `legalBasis` field |
|
|
958
|
+
| `cookies` | `string[]` | Parameter IDs belonging to this category |
|
|
959
|
+
|
|
960
|
+
Purpose defaults (applied to the parameter when a purpose is selected, all overridable; the category's `legalBasis` is authored separately):
|
|
961
|
+
|
|
962
|
+
| `purpose` | Suggested category `legalBasis` | `listenGpc` | `cpraCategory` |
|
|
963
|
+
|---------------|-----------------------------------|-------------|----------------|
|
|
964
|
+
| `necessary` | `mandatory` | `false` | — |
|
|
965
|
+
| `functional` | `legitimate_interest` | `false` | — |
|
|
966
|
+
| `preferences` | `legitimate_interest` | `false` | — |
|
|
967
|
+
| `analytics` | `consent` | `true` | — |
|
|
968
|
+
| `marketing` | `consent` | `true` | `sharing` |
|
|
969
|
+
|
|
970
|
+
**Per-profile parameter overrides:** `ProfileConfig.cookiesOverride?: Record<string, Partial<Cookie>>` lets a profile tune specific fields (e.g. `preGrant`) of a template-authored parameter without forking the whole Consent Template — deltas merge onto the resolved `cookies` map by parameter id at resolve time (`GET /profiles/:tenantId/:complianceGroup/:locale` and friends); a delta for a parameter id not present in the template is ignored. `categoriesOverride`/`uiOverride` exist on the type for a future phase but aren't applied yet.
|
|
971
|
+
|
|
972
|
+
In the Step 2 parameter table, **Pre Grant** is editable per-profile (writes/removes a `cookiesOverride` delta) except for parameters whose category isn't `legalBasis: 'consent'` (locked checked-and-disabled, same rule as template authoring). Selecting a compliance group or Consent Template auto-defaults it: `opt-out`/`opt-out-strict` force it off (with an amber warning), every other group forces it on for parameters the template didn't already pre-grant — only where that differs from the template's authored value.
|
|
973
|
+
|
|
974
|
+
**Step 3 — UI Template:** Configure banner and modal layout. New UI templates start **blank** (no prefilled buttons). Click **Load Defaults** in the amber callout to populate a sensible starter structure.
|
|
504
975
|
|
|
505
976
|
- **Main Banner / GPC Banner:** `position`, `overlayOpacity`, `showClose`, `headingTag`, `buttons`
|
|
506
|
-
- **Preference Modal:** `position`, `overlayOpacity`, `showClose`, `persistent`, `buttons
|
|
977
|
+
- **Preference Modal:** `position`, `overlayOpacity`, `showClose`, `persistent`, `buttons` — categories are no longer edited here; they come from the Consent Template selected in Step 2.
|
|
978
|
+
- Button `id`: a machine identifier (e.g. `accept-all`), **not** display text — UI templates own layout/behavior only. The visitor-facing label for each button is authored per-locale in Step 4, shown there as `id (action)` so authors know which button they're labeling.
|
|
507
979
|
- Button `type`: `primary | secondary | text | reject | submit | manage | close`
|
|
508
980
|
- Button `cookies`: `*` (grant all) · `!` (deny all) · comma-separated IDs
|
|
509
981
|
|
|
510
|
-
|
|
982
|
+
Additional UI template fields:
|
|
983
|
+
|
|
984
|
+
| Field | Scope | Type | Description |
|
|
985
|
+
|-------|-------|------|-------------|
|
|
986
|
+
| `stackButtonsOnBreakpoint` | Main Banner, GPC Banner | `number` | Below this viewport width (px) banner buttons stack vertically and stretch full-width. `0` or absent = disabled. Default when enabled: `576`. |
|
|
987
|
+
| `trapFocus` | Preference Modal | `boolean` | Confine Tab / Shift+Tab keyboard navigation within the modal while it is open. `Escape` closes and restores prior focus. |
|
|
988
|
+
|
|
989
|
+
**Step 4 — Content:** Enter localised copy with live preview:
|
|
511
990
|
|
|
512
991
|
- Main Banner: heading, body HTML
|
|
513
992
|
- GPC Banner: heading, body HTML
|
|
514
|
-
- Preference Modal: heading, subheading, intro HTML, per-category heading + description
|
|
993
|
+
- Preference Modal: heading, subheading, intro HTML, per-category heading + description, plus an optional legitimate-interest description (`legitimateInterestDescription`) shown only for categories with `legalBasis: 'legitimate_interest'` — the GDPR balancing-test justification, localized per profile alongside the rest of the category content
|
|
994
|
+
- Consent receipt checkbox label + description (only shown when **Allow consent receipt** is enabled in Step 1) — `PreferenceModal.receiptLabel`/`receiptDescription`, authored and localized per profile instead of the widget's built-in default text
|
|
995
|
+
|
|
996
|
+
**Adding locales:** Click **+ Add locale** to open a searchable `CountrySelecter` dropdown showing all BCP 47 locale options. Select a locale to add its tab instantly.
|
|
997
|
+
|
|
998
|
+
**Default-locale placeholders:** When editing a non-default locale tab, all heading, subheading, and category-heading inputs show the default locale's value as placeholder text — so translators see the source copy without switching tabs.
|
|
999
|
+
|
|
1000
|
+
**Import / Export:** available above the wizard card on every content step (Main Banner / GPC Banner / Preference Modal) — not scoped to whichever step is active, since each covers the whole profile. `Load Defaults` stays inside each step's own card, scoped to that section only, and is sourced from `packages/utils/src/profiles` for whichever compliance group and active locale tab you're on (a custom, non-preset compliance group falls back to `general-privacy-consent`'s content); category text is matched to your Consent Template's own categories by cookie purpose, not by id.
|
|
1001
|
+
|
|
1002
|
+
| Button | Action |
|
|
1003
|
+
|--------|--------|
|
|
1004
|
+
| **JSON** | Exports every locale the profile has content for as `locales.json`: `{ "en": { "mainBanner.heading": "...", "category.necessary.heading": "...", ... }, "fr-FR": {...} }` — one flat dot-path key/value map per locale. Button columns are keyed by the UI template's button id (e.g. `mainBanner.button.accept-all`); category columns by the consent template's category id. |
|
|
1005
|
+
| **CSV** | Exports `locales.csv` — one row per *every* BCP 47 locale in the `CountrySelecter` list (not just ones this profile already has), so translators never hand-type a locale code. Column A is the locale; the rest are the same dot-path keys as the JSON export, as column headers. Rows for locales the profile already has content for are pre-filled; the rest are blank, ready to fill in. |
|
|
1006
|
+
| **Import** | Accepts either format. The header row (JSON: none; CSV: row 1) is not user data. For CSV, rows where every content column is blank are skipped — so importing the full 70+ row template back doesn't create empty locale entries for locales you didn't fill in. Skipped/invalid locale keys are listed in an amber callout after import. |
|
|
1007
|
+
|
|
1008
|
+
**Readability callouts:** Inline advisory warnings (yellow) appear beneath `heading` and body HTML fields when:
|
|
1009
|
+
- A heading exceeds 80 characters
|
|
1010
|
+
- Average sentence length in the body exceeds 25 words
|
|
1011
|
+
- Total word count in the body exceeds 150 words
|
|
1012
|
+
|
|
1013
|
+
These are informational only — the profile can still be saved.
|
|
1014
|
+
|
|
1015
|
+
### Mandatory Content Validation
|
|
1016
|
+
|
|
1017
|
+
`POST /profiles` and `PUT /profiles/:id` reject (`422`) a save where any locale is missing required
|
|
1018
|
+
content — this is what guarantees a profile can never end up with a blank banner. The dashboard
|
|
1019
|
+
wizard already blocks this client-side (hard stop on Next/Save, jumping to the offending locale
|
|
1020
|
+
tab); the backend check is the enforcement backstop for bulk CSV/JSON import, which bypasses the
|
|
1021
|
+
wizard's step-by-step gating entirely. Both use the same `hasVisibleText` check
|
|
1022
|
+
(`@consenti/utils`) so a rich-text field that *looks* non-empty by string length but renders no
|
|
1023
|
+
actual text (an empty paragraph node, for example) is still correctly caught as blank.
|
|
1024
|
+
|
|
1025
|
+
Mandatory per locale (heading is always optional, everywhere):
|
|
1026
|
+
|
|
1027
|
+
| Section | Field | Rule |
|
|
1028
|
+
|---|---|---|
|
|
1029
|
+
| Main Banner / GPC Banner | Body text (`htmlText`) | Required, non-blank |
|
|
1030
|
+
| Main Banner / GPC Banner | Every button's label | Required, non-blank |
|
|
1031
|
+
| Preference Modal | Heading | Required, non-blank |
|
|
1032
|
+
| Preference Modal | Every button's label | Required, non-blank |
|
|
1033
|
+
| Preference Modal | Every category's heading | Required, non-blank |
|
|
1034
|
+
|
|
1035
|
+
GPC Banner rules only apply to a locale that actually submitted `gpcBanner` content — a profile
|
|
1036
|
+
with `gpcMode: 'ignore'`, or a locale nobody's authored a GPC variant for, simply omits it. A
|
|
1037
|
+
failing save returns:
|
|
1038
|
+
```json
|
|
1039
|
+
{ "error": "Profile content is missing required fields", "details": { "errors": [
|
|
1040
|
+
{ "locale": "hi", "section": "mainBanner", "field": "htmlText", "message": "Body text is required" }
|
|
1041
|
+
] } }
|
|
1042
|
+
```
|
|
1043
|
+
(`details` is only included outside `NODE_ENV=production`, same as the compliance-validation 422 above.)
|
|
1044
|
+
|
|
1045
|
+
### Profile Activation
|
|
1046
|
+
|
|
1047
|
+
Profiles are **inactive** after creation. To serve them via geo-routing:
|
|
1048
|
+
|
|
1049
|
+
1. Click **Activate** in the profile list or use `POST /admin/profiles/:id/activate`
|
|
1050
|
+
2. Consenti atomically repoints `${storage.path}/profiles/${tenantId}/${complianceGroup}/` (a symlink) at the profile's version directory
|
|
1051
|
+
3. The static file route starts serving these files immediately (zero DB on the hot path)
|
|
1052
|
+
|
|
1053
|
+
Only one profile per `complianceGroup` can be active at a time. Activating a new profile automatically deactivates the existing one.
|
|
1054
|
+
|
|
1055
|
+
### Profile Edit History
|
|
1056
|
+
|
|
1057
|
+
`id` is stable across every edit — each save mutates the row in place and increments `version`, and writes a new resolved-JSON snapshot immutably to `${id}/${version}/`. There's no separate DB-level archive; that on-disk snapshot tree is the audit trail. The dashboard history page (`#/banners/profiles/:id/history`) shows:
|
|
1058
|
+
- Left panel: every version, newest first, by date
|
|
1059
|
+
- Right panel: prettified JSON for the selected version; locale switcher dropdown
|
|
1060
|
+
|
|
1061
|
+
Via API:
|
|
1062
|
+
```http
|
|
1063
|
+
GET /consenti/admin/profiles/:id/versions
|
|
1064
|
+
GET /consenti/admin/profiles/:id/versions/:entryId?locale=fr-FR
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
`:id` is the profile's stable id. `:entryId` is one specific version's number, as returned by the `versions` list.
|
|
1068
|
+
|
|
1069
|
+
### Archived Profiles
|
|
1070
|
+
|
|
1071
|
+
`ProfileService.delete()` removes the DB row but never deletes the on-disk version-snapshot tree, so deleted profiles' history is still readable — it's just no longer reachable by browsing the Profiles list. The **Archived Profiles** page (`#/banners/profiles/archived`, linked from the Profiles list) lists every profile-id directory found on disk with no matching DB row, built from a directory listing only — id, version count, last-modified date — no file content is read until a specific version is opened. Clicking one opens the same version-history viewer used for active profiles.
|
|
1072
|
+
|
|
1073
|
+
```http
|
|
1074
|
+
GET /consenti/admin/profiles/archived
|
|
1075
|
+
```
|
|
1076
|
+
Returns `{ id, versionCount, lastModified }[]`. `GET /profiles/:id/versions` and `GET /profiles/:id/versions/:entryId` (above) both work for archived ids too — they read the version-directory tree directly and were never actually gated on the DB row existing.
|
|
1077
|
+
|
|
1078
|
+
### Template Save Safety
|
|
1079
|
+
|
|
1080
|
+
When saving a **Consent Template** or **UI Template** that is used by one or more profiles, a confirmation dialog appears listing the affected profiles. You must confirm before changes are applied. Uses `GET /admin/consent-templates/:id/profile-usage` or `GET /admin/ui-templates/:id/profile-usage` internally.
|
|
1081
|
+
|
|
1082
|
+
When **deleting** a Consent Template used by active profiles, the delete is blocked with a 422 error listing those profiles. Deactivate the affected profiles before deleting the template.
|
|
1083
|
+
|
|
1084
|
+
When **changing parameters or categories** on a Consent Template, the backend re-runs compliance validation against every profile using the template. If any profile's compliance would break, the save is blocked with a 422 listing `blockingProfiles` — see [Consent Template Safety Guards](#consent-template-safety-guards).
|
|
1085
|
+
|
|
1086
|
+
---
|
|
1087
|
+
|
|
1088
|
+
## EventBus
|
|
1089
|
+
|
|
1090
|
+
`createConsenti()` returns an `eventBus` — a standard Node.js `EventEmitter` — that fires events at every data lifecycle step.
|
|
1091
|
+
|
|
1092
|
+
| Event | Payload | Description |
|
|
1093
|
+
|---|---|---|
|
|
1094
|
+
| `ready` | none | Storage connected and admin user bootstrapped |
|
|
1095
|
+
| `consent.created` | `ConsentDbRecord` | New consent record saved |
|
|
1096
|
+
| `consent.updated` | `{ previous, current: ConsentDbRecord }` | Consent record updated |
|
|
1097
|
+
| `consent.erased` | `{ visitorId: string }` | Visitor data erased (GDPR erasure) |
|
|
1098
|
+
| `visitor.created` | `Visitor` | New visitor record created |
|
|
1099
|
+
| `profile.created` | `Profile` | Profile created |
|
|
1100
|
+
| `profile.updated` | `{ previous, current: Profile }` | Profile edited — `current.id` matches `previous.id`; `current.version` is incremented |
|
|
1101
|
+
| `profile.deleted` | `{ id: string, previous: Profile }` | Profile deleted |
|
|
1102
|
+
| `cache:warm` | `{ paths: string[], profileId: string }` | Fired after locale JSON files are written; paths to warm in CDN/cache |
|
|
1103
|
+
| `cache:purge` | `{ paths: string[], profileId: string }` | Fired after locale JSON files are removed; paths to purge from CDN/cache |
|
|
1104
|
+
|
|
1105
|
+
`cache:warm` and `cache:purge` mirror the `handleCache` config callback — use whichever suits your integration.
|
|
1106
|
+
|
|
1107
|
+
```ts
|
|
1108
|
+
const { eventBus } = createConsenti({ /* ... */ })
|
|
1109
|
+
|
|
1110
|
+
eventBus.on('consent.created', (record) => {
|
|
1111
|
+
// send to analytics, trigger webhook, etc.
|
|
1112
|
+
console.log('New consent from visitor:', record.visitorId)
|
|
1113
|
+
})
|
|
1114
|
+
|
|
1115
|
+
eventBus.on('consent.erased', ({ visitorId }) => {
|
|
1116
|
+
myDmpClient.deleteVisitor(visitorId)
|
|
1117
|
+
})
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
---
|
|
1121
|
+
|
|
1122
|
+
## ConsentAction / CategoryAction
|
|
1123
|
+
|
|
1124
|
+
Server-side counterparts to `@consenti/ui`'s `ConsentAction`/`CategoryAction` — thin wrappers around `consent.created`/`consent.updated` that fire `onGrant`/`onDeny` only on an actual status transition (not on every event), across every visitor's submissions. Prefer these over listening to the raw events directly when you only care about one parameter or one category — they handle the previous-vs-current comparison for you.
|
|
1125
|
+
|
|
1126
|
+
There is no server equivalent of the widget's `ConsentScript`/`CategoryScript` — injecting a `<script>` tag only makes sense in a browser DOM the server doesn't have. `services.consent.create()`/`.update()` are already the server-side submit/update methods these hooks react to (see [ConsentService](#services)).
|
|
1127
|
+
|
|
1128
|
+
### `ConsentAction`
|
|
1129
|
+
|
|
1130
|
+
Watches a single cookie parameter.
|
|
1131
|
+
|
|
1132
|
+
```ts
|
|
1133
|
+
import { createConsenti, ConsentAction } from '@consenti/api'
|
|
1134
|
+
|
|
1135
|
+
const { eventBus } = createConsenti({ /* ... */ })
|
|
1136
|
+
|
|
1137
|
+
new ConsentAction({
|
|
1138
|
+
id: 'analytics_storage', // cookie id to watch
|
|
1139
|
+
eventBus,
|
|
1140
|
+
onGrant: ({ visitorId, cookieId, status, record }) => crm.optIn(visitorId),
|
|
1141
|
+
onDeny: ({ visitorId, cookieId, status, record }) => crm.optOut(visitorId),
|
|
1142
|
+
})
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
**Options** (`ConsentActionOptions`):
|
|
1146
|
+
|
|
1147
|
+
| Option | Type | Description |
|
|
1148
|
+
|---|---|---|
|
|
1149
|
+
| `id` | `string` | The consent parameter (cookie) ID to watch |
|
|
1150
|
+
| `eventBus` | `EventEmitter` | The `eventBus` returned by `createConsenti()` |
|
|
1151
|
+
| `onGrant` | `(params: ConsentActionParams) => void` | Called when the parameter transitions to `'granted'` |
|
|
1152
|
+
| `onDeny` | `(params: ConsentActionParams) => void` | Called when the parameter transitions away from `'granted'` (`'denied'` or `'objected'`) |
|
|
1153
|
+
|
|
1154
|
+
**Callback params** (`ConsentActionParams`): `{ visitorId: string, cookieId: string, status: ConsentStatus, record: ConsentDbRecord }`
|
|
1155
|
+
|
|
1156
|
+
### `CategoryAction`
|
|
1157
|
+
|
|
1158
|
+
Watches a whole category's rollup consent state — granted only when *every* cookie in the category is granted, matching the preference modal's tri-state rollup rule.
|
|
1159
|
+
|
|
1160
|
+
```ts
|
|
1161
|
+
import { createConsenti, CategoryAction } from '@consenti/api'
|
|
1162
|
+
|
|
1163
|
+
const { eventBus, services } = createConsenti({ /* ... */ })
|
|
1164
|
+
|
|
1165
|
+
new CategoryAction({
|
|
1166
|
+
categoryId: 'marketing',
|
|
1167
|
+
eventBus,
|
|
1168
|
+
profiles: services.profile, // resolves the category's cookie ids per profile
|
|
1169
|
+
onGrant: ({ visitorId, categoryId, status, record }) => adsPlatform.optIn(visitorId),
|
|
1170
|
+
onDeny: ({ visitorId, categoryId, status, record }) => adsPlatform.optOut(visitorId),
|
|
1171
|
+
})
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
**Options** (`CategoryActionOptions`):
|
|
1175
|
+
|
|
1176
|
+
| Option | Type | Description |
|
|
1177
|
+
|---|---|---|
|
|
1178
|
+
| `categoryId` | `string` | The category ID to watch |
|
|
1179
|
+
| `eventBus` | `EventEmitter` | The `eventBus` returned by `createConsenti()` |
|
|
1180
|
+
| `profiles` | `ProfileService` | The `services.profile` returned by `createConsenti()` — resolves which cookies belong to this category for the record's profile (categories are defined on the consent template) |
|
|
1181
|
+
| `onGrant` | `(params: CategoryActionParams) => void` | Called when every cookie in the category transitions to `'granted'` |
|
|
1182
|
+
| `onDeny` | `(params: CategoryActionParams) => void` | Called when the category transitions away from fully granted |
|
|
1183
|
+
|
|
1184
|
+
**Callback params** (`CategoryActionParams`): `{ visitorId: string, categoryId: string, status: ConsentStatus, record: ConsentDbRecord }`
|
|
1185
|
+
|
|
1186
|
+
Both classes expose `destroy()` to remove their event listeners when the hook is no longer needed.
|
|
515
1187
|
|
|
516
1188
|
---
|
|
517
1189
|
|
|
@@ -590,18 +1262,47 @@ await consenti.destroy()
|
|
|
590
1262
|
| Stack traces | Suppressed in `NODE_ENV=production` responses |
|
|
591
1263
|
| CORS | Explicit allowlist — never use `origins: '*'` in production |
|
|
592
1264
|
| Rate limiting | Applied to all public API routes by default |
|
|
1265
|
+
| Signed consent records | Opt-in via `consentSigningKey` — HMAC-SHA256 (`node:crypto` `createHmac`) over each record's core fields, hex-encoded into `signature`. No-op and no schema burden when unset. |
|
|
1266
|
+
|
|
1267
|
+
### Signed consent records (opt-in)
|
|
1268
|
+
|
|
1269
|
+
Set `consentSigningKey` to have every consent record signed at create/update time — tamper-evidence for server-stored records, independent of the widget's own cookie-signing (`core.cookieSigningKey`, which protects the visitor's local cookie).
|
|
1270
|
+
|
|
1271
|
+
```ts
|
|
1272
|
+
createConsenti({
|
|
1273
|
+
consentSigningKey: process.env.CONSENTI_CONSENT_SIGNING_KEY,
|
|
1274
|
+
})
|
|
1275
|
+
```
|
|
1276
|
+
|
|
1277
|
+
- The signature covers `tenantId`, `visitorId`, `profileId`, `locale`, and the sorted `consentJson` entries — not the DB-generated `id`/`createdAt`/`updatedAt`.
|
|
1278
|
+
- `GET /consent/:visitorId/verify` checks the signature when both a key is configured and the record has one, adding `hmac_invalid` to `reasons` on mismatch. Records signed before the key was configured (or never signed) never fail this check for its absence.
|
|
1279
|
+
- `signature` is included in CSV/JSON/XLSX exports (`GET /export/consents`) and shown in the dashboard's Consents detail view.
|
|
1280
|
+
- Hash-chaining between records (a full tamper-evident log, not just per-record integrity) is not implemented — out of scope for now.
|
|
593
1281
|
|
|
594
1282
|
---
|
|
595
1283
|
|
|
596
1284
|
## Environment Variables
|
|
597
1285
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
|
601
|
-
|
|
602
|
-
| `
|
|
603
|
-
| `
|
|
604
|
-
| `
|
|
1286
|
+
All config fields can be set via environment variables. Code config takes precedence over env vars.
|
|
1287
|
+
|
|
1288
|
+
| Variable | Config field | Default |
|
|
1289
|
+
|-------------------------------------|------------------------------------|----------------------------------|
|
|
1290
|
+
| `NODE_ENV` | — | Suppress stack traces when `'production'` |
|
|
1291
|
+
| `CONSENTI_BASE_PATH` | `basePath` | `'/consenti'` |
|
|
1292
|
+
| `CONSENTI_ADMIN_EMAIL` | `auth.adminEmail` | `'user@consenti.dev'` |
|
|
1293
|
+
| `CONSENTI_ADMIN_PASSWORD` | `auth.adminPassword` | `'Consenti@123'` |
|
|
1294
|
+
| `CONSENTI_ADMIN_JWT_SECRET` | `auth.jwtSecret` | random per restart |
|
|
1295
|
+
| `CONSENTI_DB_DRIVER` | `storage.driver` | `'json'` |
|
|
1296
|
+
| `CONSENTI_DB_PATH` | `storage.path` | `'./consenti-data'` |
|
|
1297
|
+
| `CONSENTI_DB_URI` | `storage.uri` | — |
|
|
1298
|
+
| `CONSENTI_DB_DATABASE` | `storage.database` | — |
|
|
1299
|
+
| `CONSENTI_DB_HOST` | `storage.host` | — |
|
|
1300
|
+
| `CONSENTI_DB_PORT` | `storage.port` | — |
|
|
1301
|
+
| `CONSENTI_DB_USER` | `storage.user` | — |
|
|
1302
|
+
| `CONSENTI_DB_PASSWORD` | `storage.password` | — |
|
|
1303
|
+
| `CONSENTI_RATE_LIMIT_WINDOW_MS` | `rateLimit.windowMs` | `60000` |
|
|
1304
|
+
| `CONSENTI_RATE_LIMIT_MAX_REQUESTS` | `rateLimit.maxRequests` | `60` |
|
|
1305
|
+
| `CONSENTI_MAX_BODY_SIZE` | `maxBodySize` | `1048576` (1 MB) |
|
|
605
1306
|
|
|
606
1307
|
---
|
|
607
1308
|
|
|
@@ -610,6 +1311,15 @@ await consenti.destroy()
|
|
|
610
1311
|
When running, Swagger UI is available at `{basePath}/api/docs`.
|
|
611
1312
|
OpenAPI JSON at `{basePath}/api/openapi.json`.
|
|
612
1313
|
|
|
1314
|
+
|
|
1315
|
+
---
|
|
1316
|
+
|
|
1317
|
+
## Demo
|
|
1318
|
+
For detailed API docs, Guides and Demo, visit [consenti.dev](https://consenti.dev)
|
|
1319
|
+
|
|
1320
|
+
Visit author's profile at [santosh.top](https://santosh.top)
|
|
1321
|
+
|
|
1322
|
+
|
|
613
1323
|
---
|
|
614
1324
|
|
|
615
1325
|
## Contributing
|