@consenti/api 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 +608 -195
- package/dist/dashboard/assets/de-DwE2aQfW.js +5 -0
- package/dist/dashboard/assets/es-Dq88TWkB.js +5 -0
- package/dist/dashboard/assets/fr-o_Pyg8eR.js +5 -0
- package/dist/dashboard/assets/index-BSsYDdb9.js +40 -0
- package/dist/dashboard/assets/index-bOTGevzl.css +2 -0
- package/dist/dashboard/assets/ja-CwgXYmpD.js +5 -0
- package/dist/dashboard/assets/zh-CN-ety9KoQ0.js +5 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/index.cjs +135 -9084
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +369 -195
- package/dist/index.d.ts +369 -195
- package/dist/index.js +135 -9038
- package/dist/index.js.map +1 -1
- package/dist/widget/widget.css +1 -0
- package/dist/widget/widget.js +1 -2304
- package/package.json +16 -9
- 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,178 @@ 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
|
-
|
|
43
|
-
|
|
78
|
+
Consenti ships with four geo resolver options. The default requires zero installation; the others are opt-in.
|
|
79
|
+
|
|
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?
|
|
44
85
|
|
|
45
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
```
|
|
95
|
+
# Geo resolvers (choose any one )
|
|
96
|
+
|
|
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
|
|
57
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
|
|
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
|
+
|
|
134
|
+
// ── Auth ─────────────────────────────────────────────────────────────────────
|
|
135
|
+
auth: {
|
|
136
|
+
mode: 'local', // 'local' | 'jwt' | 'oidc' | 'saml' | 'custom'
|
|
137
|
+
adminEmail: 'user@consenti.dev',
|
|
138
|
+
adminPassword: process.env.CONSENTI_ADMIN_PASSWORD ?? "Consenti@123",
|
|
139
|
+
jwtSecret: process.env.CONSENTI_ADMIN_JWT_SECRET, // auto-generated if omitted (sessions expire on restart)
|
|
140
|
+
|
|
141
|
+
// OIDC (Auth0, Keycloak, Google, etc.)
|
|
142
|
+
// mode: 'oidc',
|
|
143
|
+
// oidc: {
|
|
144
|
+
// issuer: 'https://your-idp.example.com',
|
|
145
|
+
// clientId: 'your-client-id',
|
|
146
|
+
// clientSecret: process.env.OIDC_SECRET,
|
|
147
|
+
// redirectUri: 'https://app.example.com/consenti/admin/auth/oidc/callback',
|
|
148
|
+
// claimsMapping: { email: 'email', roles: 'consenti_roles' },
|
|
149
|
+
// },
|
|
150
|
+
|
|
151
|
+
// SAML
|
|
152
|
+
// mode: 'saml',
|
|
153
|
+
// saml: {
|
|
154
|
+
// issuer: 'https://idp.example.com',
|
|
155
|
+
// entryPoint: 'https://idp.example.com/sso/saml',
|
|
156
|
+
// cert: process.env.SAML_CERT!, // IdP signing cert (PEM, no headers)
|
|
157
|
+
// callbackUrl: 'https://app.example.com/consenti/admin/auth/saml/callback',
|
|
158
|
+
// },
|
|
159
|
+
|
|
160
|
+
// Custom — bring your own authentication function
|
|
161
|
+
// mode: 'custom',
|
|
162
|
+
// validateUser: async (req: Request) => AdminUser | null,
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
// ── Routing ──────────────────────────────────────────────────────────────────
|
|
166
|
+
basePath: '/consenti', // URL prefix for all routes (default: '/consenti')
|
|
167
|
+
dashboard: true, // serve built-in admin SPA at basePath (default: true)
|
|
168
|
+
|
|
169
|
+
// ── Branding ─────────────────────────────────────────────────────────────────
|
|
170
|
+
branding: {
|
|
171
|
+
appName: 'Consenti', // shown in dashboard header, login page, browser tab
|
|
172
|
+
appLogoPath: './logo-dark.svg', // local file path or https:// URL; auto-served as static asset
|
|
173
|
+
hidePoweredBy: false, // true = hide "Powered by Consenti" badge
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
// ── Rate limiting ─────────────────────────────────────────────────────────────
|
|
177
|
+
rateLimit: {
|
|
178
|
+
enabled: true,
|
|
179
|
+
windowMs: 60_000, // rolling window in ms (default: 60 000)
|
|
180
|
+
maxRequests: 60, // max requests per IP per window (default: 60)
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
// ── Compliance ───────────────────────────────────────────────────────────────
|
|
184
|
+
compliance: {
|
|
185
|
+
// Geo-based auto routing (recommended)
|
|
186
|
+
type: 'auto', // 'auto' = geo-resolve per visitor | ComplianceGroupId = one fixed group globally
|
|
187
|
+
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
|
|
188
|
+
autoComplianceMap: 'default',// 'default' = embedded 240-country map | operator object | 'auto' = fetch from complianceMapUrl
|
|
189
|
+
// complianceMapUrl: 'https://consenti.dev/data/v1/compliance-map.json', // used when autoComplianceMap: 'auto'
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
// ── S3 profile file sync (optional) ─────────────────────────────────────────
|
|
193
|
+
// When enabled, every locale JSON written to disk is also PUT to S3.
|
|
194
|
+
// /resolve-profile returns an s3:// URL so the widget can fetch directly from CDN.
|
|
195
|
+
s3Api: {
|
|
196
|
+
enabled: false,
|
|
197
|
+
region: 'us-east-1',
|
|
198
|
+
bucketName: 'consenti-profiles',
|
|
199
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
200
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
201
|
+
// sessionToken: process.env.AWS_SESSION_TOKEN, // for temporary credentials
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
// ── Cache invalidation hook (optional) ──────────────────────────────────────
|
|
205
|
+
// Called after every profile creation, edit, activation, deactivation, or delete.
|
|
206
|
+
// isPurge=false = warm the cache at these paths; isPurge=true = purge.
|
|
207
|
+
handleCache: (paths: string[], version: number, isPurge: boolean) => {
|
|
208
|
+
// paths: array of file paths that were written or removed
|
|
209
|
+
// version: current profile version
|
|
210
|
+
// isPurge: true when files were removed (deactivate/delete); false when written (activate/create)
|
|
211
|
+
// Example: purge CDN edge nodes, update nginx cache keys, etc.
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
// ── IAB TCF v2.2 ─────────────────────────────────────────────────────────────
|
|
215
|
+
tcf: {
|
|
216
|
+
enabled: false,
|
|
217
|
+
cmpId: 123, // your IAB-registered CMP ID (required when enabled)
|
|
218
|
+
cmpVersion: 1, // your CMP software version
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
// ── Age gate ─────────────────────────────────────────────────────────────────
|
|
222
|
+
ageGate: {
|
|
223
|
+
enabled: false,
|
|
224
|
+
minimumAge: 13, // COPPA = 13; GDPR Article 8 = 16
|
|
225
|
+
requireParentalConsent: false,
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
// ── Data retention ───────────────────────────────────────────────────────────
|
|
229
|
+
dataRetention: {
|
|
230
|
+
purgeAfterDays: 365, // delete consent records older than N days; runs nightly
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
// ── Multi-tenant ─────────────────────────────────────────────────────────────
|
|
234
|
+
multiTenant: {
|
|
235
|
+
enabled: false, // scope all data to tenant from X-Tenant-ID header
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
// ── Proxies & body ───────────────────────────────────────────────────────────
|
|
239
|
+
trustedProxies: [], // IP / CIDR of reverse proxies (reads X-Forwarded-For)
|
|
240
|
+
maxBodySize: 1_048_576, // request body size limit in bytes (default: 1 MB)
|
|
241
|
+
|
|
242
|
+
// ── Plugins ──────────────────────────────────────────────────────────────────
|
|
243
|
+
plugins: [], // ConsentiServerPlugin instances
|
|
244
|
+
})
|
|
245
|
+
```
|
|
246
|
+
|
|
60
247
|
## Quick Start
|
|
61
248
|
|
|
62
249
|
### Express
|
|
@@ -68,7 +255,7 @@ import { createConsenti } from '@consenti/api'
|
|
|
68
255
|
const app = express()
|
|
69
256
|
|
|
70
257
|
const consenti = createConsenti({
|
|
71
|
-
storage: { driver: 'sqlite', path: './consenti
|
|
258
|
+
storage: { driver: 'node:sqlite', path: './consenti-data' },
|
|
72
259
|
auth: {
|
|
73
260
|
mode: 'local',
|
|
74
261
|
adminEmail: 'admin@example.com',
|
|
@@ -89,10 +276,11 @@ app.listen(3000, () => {
|
|
|
89
276
|
### Standalone (no framework)
|
|
90
277
|
|
|
91
278
|
```ts
|
|
279
|
+
import { createServer } from 'node:http'
|
|
92
280
|
import { createConsenti } from '@consenti/api'
|
|
93
281
|
|
|
94
282
|
const consenti = createConsenti({
|
|
95
|
-
storage: { driver: 'sqlite', path: './consenti
|
|
283
|
+
storage: { driver: 'node:sqlite', path: './consenti-data' },
|
|
96
284
|
auth: {
|
|
97
285
|
mode: 'local',
|
|
98
286
|
adminEmail: 'admin@example.com',
|
|
@@ -101,9 +289,13 @@ const consenti = createConsenti({
|
|
|
101
289
|
dashboard: true,
|
|
102
290
|
})
|
|
103
291
|
|
|
104
|
-
await consenti.
|
|
105
|
-
|
|
106
|
-
|
|
292
|
+
await consenti.ready // wait for DB + bootstrap
|
|
293
|
+
|
|
294
|
+
const server = createServer(consenti.handler)
|
|
295
|
+
server.listen(3001, () => {
|
|
296
|
+
console.log('Admin dashboard → http://localhost:3001/consenti/')
|
|
297
|
+
console.log('REST API → http://localhost:3001/consenti/api/v1/')
|
|
298
|
+
})
|
|
107
299
|
```
|
|
108
300
|
|
|
109
301
|
---
|
|
@@ -122,14 +314,13 @@ app.use(consenti.handler) // as a terminal handler (no next())
|
|
|
122
314
|
|
|
123
315
|
```ts
|
|
124
316
|
import Fastify from 'fastify'
|
|
125
|
-
|
|
317
|
+
import { createConsenti } from '@consenti/api'
|
|
126
318
|
|
|
319
|
+
const fastify = Fastify()
|
|
127
320
|
const consenti = createConsenti({ /* ... */ })
|
|
128
321
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
consenti.handler(req.raw, reply.raw)
|
|
132
|
-
})
|
|
322
|
+
await fastify.register(consenti.fastifyHandler)
|
|
323
|
+
await fastify.listen({ port: 3000 })
|
|
133
324
|
```
|
|
134
325
|
|
|
135
326
|
### Next.js App Router
|
|
@@ -144,7 +335,7 @@ let consenti: Awaited<ReturnType<typeof createConsenti>>
|
|
|
144
335
|
async function getConsenti() {
|
|
145
336
|
if (!consenti) {
|
|
146
337
|
consenti = createConsenti({
|
|
147
|
-
storage: { driver: 'sqlite', path: './consenti
|
|
338
|
+
storage: { driver: 'sqlite', path: './consenti-data' },
|
|
148
339
|
auth: {
|
|
149
340
|
mode: 'local',
|
|
150
341
|
adminEmail: process.env.CONSENTI_ADMIN_EMAIL!,
|
|
@@ -155,12 +346,12 @@ async function getConsenti() {
|
|
|
155
346
|
return consenti
|
|
156
347
|
}
|
|
157
348
|
|
|
158
|
-
|
|
349
|
+
async function consentiHandler(req: NextRequest) {
|
|
159
350
|
const c = await getConsenti()
|
|
160
351
|
return c.handleRequest(req)
|
|
161
352
|
}
|
|
162
353
|
|
|
163
|
-
export {
|
|
354
|
+
export { consentiHandler as POST, consentiHandler as PUT, consentiHandler as DELETE, consentiHandler as PATCH, consentiHandler as GET }
|
|
164
355
|
```
|
|
165
356
|
|
|
166
357
|
### Hono / Fetch-based
|
|
@@ -171,133 +362,105 @@ return consenti.honoApp.fetch(request)
|
|
|
171
362
|
|
|
172
363
|
---
|
|
173
364
|
|
|
174
|
-
##
|
|
365
|
+
## First Boot
|
|
175
366
|
|
|
176
|
-
|
|
177
|
-
import { createConsenti } from '@consenti/api'
|
|
367
|
+
On first start with JSON, Consenti:
|
|
178
368
|
|
|
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)
|
|
369
|
+
1. Creates the storage directory at the configured path (see [Storage Directory Structure](#storage-directory-structure))
|
|
370
|
+
2. Runs all migrations (creates `visitors`, `consent_records`, `consent_history`, `profiles`, `admin_users`, `audit_logs` tables, and DB indexes)
|
|
371
|
+
3. Creates the admin user from `auth.adminEmail` + `auth.adminPassword`
|
|
372
|
+
4. Seeds the default profile (`profileId: '0'`)
|
|
205
373
|
|
|
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
|
-
// },
|
|
374
|
+
Subsequent starts run only pending migrations.
|
|
214
375
|
|
|
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
|
-
// },
|
|
376
|
+
---
|
|
223
377
|
|
|
224
|
-
|
|
225
|
-
// mode: 'custom',
|
|
226
|
-
// validateUser: async (req) => ({ id, email, roles, permissions } | null),
|
|
227
|
-
},
|
|
378
|
+
## Storage Directory Structure
|
|
228
379
|
|
|
229
|
-
|
|
230
|
-
basePath: '/consenti', // URL prefix (default: '/consenti')
|
|
231
|
-
dashboard: true, // serve admin SPA (default: false)
|
|
232
|
-
trustProxy: false, // true when behind nginx / Cloudflare
|
|
380
|
+
`storage.path` is a **directory path**. Consenti creates the following layout automatically:
|
|
233
381
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
382
|
+
```
|
|
383
|
+
${storage.path}/
|
|
384
|
+
db/
|
|
385
|
+
consenti-data.json # JSON adapter
|
|
386
|
+
consenti.db # SQLite adapters
|
|
387
|
+
profiles/
|
|
388
|
+
${tenantId}/
|
|
389
|
+
${profileId}/
|
|
390
|
+
1/ # version 1 (immutable once written)
|
|
391
|
+
en.json
|
|
392
|
+
fr-FR.json
|
|
393
|
+
default.json # copy of defaultLocale content (fallback)
|
|
394
|
+
2/ # version 2 written on next save
|
|
395
|
+
…
|
|
396
|
+
${complianceGroup}/ # hot-serve path (active profile only)
|
|
397
|
+
en.json
|
|
398
|
+
fr-FR.json
|
|
399
|
+
default.json
|
|
400
|
+
logs/ # reserved for future structured logging
|
|
401
|
+
```
|
|
239
402
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
403
|
+
**Key rules:**
|
|
404
|
+
- Every profile save writes to a new immutable version directory under `${profileId}/${version}/`
|
|
405
|
+
- `${complianceGroup}/` only exists when a profile is **active** for that group
|
|
406
|
+
- On **activate**: locale files are copied from `${profileId}/${currentVersion}/` → `${complianceGroup}/`
|
|
407
|
+
- On **deactivate** / **delete**: `${complianceGroup}/` files are removed
|
|
408
|
+
- `default.json` = full resolved profile for `defaultLocale`; used as locale fallback (303 redirect)
|
|
244
409
|
|
|
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
|
-
},
|
|
410
|
+
**Backward compat:** If `storage.path` has a file extension (`.json`, `.db`), the parent directory is used and a deprecation warning is logged.
|
|
254
411
|
|
|
255
|
-
|
|
256
|
-
enabled: false,
|
|
257
|
-
minimumAge: 13,
|
|
258
|
-
requireParentalConsent: false,
|
|
259
|
-
},
|
|
412
|
+
---
|
|
260
413
|
|
|
261
|
-
|
|
262
|
-
purgeAfterDays: 365, // auto-purge old records (0 = disabled)
|
|
263
|
-
},
|
|
414
|
+
## Public REST API
|
|
264
415
|
|
|
265
|
-
|
|
266
|
-
enabled: false, // isolate data per tenant
|
|
267
|
-
},
|
|
416
|
+
Base path: `/consenti/api/v1` (change with `basePath` in config). No authentication required.
|
|
268
417
|
|
|
269
|
-
|
|
270
|
-
|
|
418
|
+
| Method | Path | Description |
|
|
419
|
+
|------------|---------------------------------------------|------------------------------------------------------------------------|
|
|
420
|
+
| `GET` | `/profiles/:tenantId/:complianceGroup/:locale` | **Hot-serve** — serve locale JSON directly from disk (zero DB) |
|
|
421
|
+
| `GET` | `/profiles/:tenantId/:profileId/:version/:locale` | Serve a specific version locale JSON (dashboard preview) |
|
|
422
|
+
| `GET` | `/resolve-profile` | Geo-resolve compliance group, return file path + locale |
|
|
423
|
+
| `GET` | `/profiles/:id` | get profile by ID (resolved, DB-backed, default tenant, default locale)|
|
|
424
|
+
| `GET` | `/profiles/:id/:locale` | get profile by ID & locale (resolved, DB-backed, default tenant) |
|
|
425
|
+
| `GET` | `/profiles/auto/:locale` | Legacy: geo-resolve then return matching active profile |
|
|
426
|
+
| `POST` | `/consent` | Submit a consent record |
|
|
427
|
+
| `GET` | `/consent/:visitorId` | Get current consent for a visitor |
|
|
428
|
+
| `PUT` | `/consent/:visitorId` | Update consent for a visitor |
|
|
429
|
+
| `DELETE` | `/consent/:visitorId` | GDPR right-to-erasure — delete all visitor data |
|
|
430
|
+
| `GET` | `/consent/:visitorId/verify` | Check if consent is still valid |
|
|
431
|
+
|
|
432
|
+
### Static file serving (hot path)
|
|
433
|
+
|
|
434
|
+
```http
|
|
435
|
+
GET /consenti/api/v1/profiles/default/opt-in/en
|
|
271
436
|
```
|
|
272
437
|
|
|
273
|
-
|
|
438
|
+
Serves `${storage.path}/profiles/default/opt-in/en.json` directly from disk. No DB involved.
|
|
274
439
|
|
|
275
|
-
|
|
440
|
+
If the locale file doesn't exist, returns `303 See Other → .../opt-in/default` (which serves `default.json` = `defaultLocale` content).
|
|
276
441
|
|
|
277
|
-
|
|
442
|
+
Cache headers: `public, max-age=3600, stale-while-revalidate=60` — suitable for CDN caching.
|
|
278
443
|
|
|
279
|
-
|
|
280
|
-
2. Runs all migrations (creates `visitors`, `consent_records`, `consent_history`, `profiles`, `admin_users`, `audit_logs` tables)
|
|
281
|
-
3. Creates the admin user from `auth.adminEmail` + `auth.adminPassword`
|
|
282
|
-
4. Seeds the default profile (`profileId: '0'`)
|
|
444
|
+
### Resolve profile (for `compliance.type: 'auto'`)
|
|
283
445
|
|
|
284
|
-
|
|
446
|
+
```http
|
|
447
|
+
GET /consenti/api/v1/resolve-profile?tz=Europe/Paris&lang=fr-FR&locale=fr-FR
|
|
448
|
+
```
|
|
285
449
|
|
|
286
|
-
|
|
450
|
+
For widgets using `compliance.type: 'auto'` — call this once on page load to discover which compliance group and locale the visitor should receive:
|
|
287
451
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
452
|
+
```json
|
|
453
|
+
{
|
|
454
|
+
"path": "/consenti/api/v1/profiles/default/opt-in/fr-FR",
|
|
455
|
+
"resolvedLocale": "fr-FR",
|
|
456
|
+
"resolvedComplianceGroup": "opt-in",
|
|
457
|
+
"profileId": "gdpr-profile-uuid",
|
|
458
|
+
"version": 3,
|
|
459
|
+
"warning": "locale_not_found" // only present when locale was unavailable; path points to default
|
|
460
|
+
}
|
|
461
|
+
```
|
|
291
462
|
|
|
292
|
-
|
|
293
|
-
|------------|-------------------------------|-------------------------------------------------|
|
|
294
|
-
| `GET` | `/profiles/:id` | Get profile resolved in its default locale |
|
|
295
|
-
| `GET` | `/profiles/:id/:locale` | Get profile resolved for a specific locale |
|
|
296
|
-
| `POST` | `/consent` | Submit a consent record |
|
|
297
|
-
| `GET` | `/consent/:visitorId` | Get current consent for a visitor |
|
|
298
|
-
| `PUT` | `/consent/:visitorId` | Update consent for a visitor |
|
|
299
|
-
| `DELETE` | `/consent/:visitorId` | GDPR right-to-erasure — delete all visitor data |
|
|
300
|
-
| `GET` | `/consent/:visitorId/verify` | Check if consent is still valid |
|
|
463
|
+
Query params: `tz` (IANA timezone), `lang` (Accept-Language value), `locale` (preferred BCP 47 locale).
|
|
301
464
|
|
|
302
465
|
### Submit consent
|
|
303
466
|
|
|
@@ -380,13 +543,114 @@ Content-Type: application/json
|
|
|
380
543
|
|
|
381
544
|
### Profiles
|
|
382
545
|
|
|
383
|
-
| Method | Path
|
|
384
|
-
|
|
385
|
-
| `GET` | `/profiles`
|
|
386
|
-
| `
|
|
387
|
-
| `
|
|
388
|
-
| `
|
|
389
|
-
| `
|
|
546
|
+
| Method | Path | Description |
|
|
547
|
+
|------------|----------------------------------|-------------------------------------------------------------------------|
|
|
548
|
+
| `GET` | `/profiles` | List all profiles |
|
|
549
|
+
| `GET` | `/profiles?summary=1` | List profiles as `ProfileSummary[]` (no blob, with template names) |
|
|
550
|
+
| `POST` | `/profiles` | Create a profile — 422 on compliance errors; `{ conflict, requiresChoice: true }` when an active profile exists on the same `complianceGroup` |
|
|
551
|
+
| `GET` | `/profiles/:id` | Get a profile |
|
|
552
|
+
| `PUT` | `/profiles/:id` | Update a profile (auto-bumps version) — same compliance validation + conflict detection |
|
|
553
|
+
| `DELETE` | `/profiles/:id` | Delete a profile |
|
|
554
|
+
| `POST` | `/profiles/:id/activate` | Activate a profile — writes locale JSON files to `${complianceGroup}/` |
|
|
555
|
+
| `POST` | `/profiles/:id/deactivate` | Deactivate a profile — removes `${complianceGroup}/` locale files |
|
|
556
|
+
| `GET` | `/profiles/:id/versions` | List version history entries (`{ version, createdAt, locales[] }[]`) |
|
|
557
|
+
| `GET` | `/profiles/:id/versions/:n` | Read a specific version locale file (query: `?locale=en`) |
|
|
558
|
+
| `POST` | `/profiles/validate` | Validate cookie template against a compliance group (no save) |
|
|
559
|
+
| `GET` | `/compliance-coverage` | Active profile (or null) per compliance group — powers coverage panel |
|
|
560
|
+
|
|
561
|
+
#### Profile Save Conflict Detection
|
|
562
|
+
|
|
563
|
+
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:
|
|
564
|
+
|
|
565
|
+
```json
|
|
566
|
+
{
|
|
567
|
+
"conflict": { "id": "existing-profile-uuid", "name": "GDPR Profile v1" },
|
|
568
|
+
"requiresChoice": true
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
Re-submit the original request with a `choice` field to resolve:
|
|
573
|
+
|
|
574
|
+
```json
|
|
575
|
+
{ "choice": "deactivate" } // deactivate the conflicting profile, then save this one as active
|
|
576
|
+
{ "choice": "inactive" } // save this profile as inactive (no conflict)
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
The dashboard profile editor shows a three-button popup when `requiresChoice: true` — "Deactivate existing and activate this one", "Save as inactive", and "Cancel".
|
|
580
|
+
|
|
581
|
+
#### ProfileSummary shape
|
|
582
|
+
|
|
583
|
+
```ts
|
|
584
|
+
{
|
|
585
|
+
id: string
|
|
586
|
+
name: string
|
|
587
|
+
defaultLocale: string
|
|
588
|
+
complianceGroup: ComplianceGroupId | null
|
|
589
|
+
version: number
|
|
590
|
+
isActive: boolean
|
|
591
|
+
cookieTemplateName: string | null
|
|
592
|
+
uiTemplateName: string | null
|
|
593
|
+
createdAt: string
|
|
594
|
+
updatedAt: string
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### Cookie Templates
|
|
599
|
+
|
|
600
|
+
| Method | Path | Description |
|
|
601
|
+
|------------|---------------------------------------------|----------------------------------------------------------|
|
|
602
|
+
| `GET` | `/cookie-templates` | List cookie templates |
|
|
603
|
+
| `GET` | `/cookie-templates/:id` | Get a cookie template |
|
|
604
|
+
| `POST` | `/cookie-templates` | Create a cookie template |
|
|
605
|
+
| `PUT` | `/cookie-templates/:id` | Update a cookie template — 422 if removing cookies breaks compliance for a dependent profile |
|
|
606
|
+
| `DELETE` | `/cookie-templates/:id` | Delete a cookie template — 422 if any active profiles use it |
|
|
607
|
+
| `POST` | `/cookie-templates/:id/copy` | Duplicate a cookie template |
|
|
608
|
+
| `GET` | `/cookie-templates/:id/profile-usage` | List profiles using this template (`ProfileSummary[]`) |
|
|
609
|
+
|
|
610
|
+
#### Cookie Template Safety Guards
|
|
611
|
+
|
|
612
|
+
**Deletion guard:** `DELETE /cookie-templates/:id` returns `422` with `{ activeProfiles: ProfileSummary[] }` when one or more active profiles reference the template. Deactivate those profiles first, then delete.
|
|
613
|
+
|
|
614
|
+
**Cookie removal guard:** When updating a template (`PUT`), if cookies are removed, the backend checks all profiles using the template and runs compliance validation with the new cookie set. If any profile's `complianceGroup` requires a cookie that was removed, the response is `422` with:
|
|
615
|
+
|
|
616
|
+
```json
|
|
617
|
+
{
|
|
618
|
+
"blockingProfiles": [{ "id": "...", "name": "...", "complianceGroup": "opt-in" }],
|
|
619
|
+
"removedCookieIds": ["analytics", "marketing"]
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
Profiles listed in `blockingProfiles` must be updated (switch template or change complianceGroup) before the cookie removal can proceed.
|
|
624
|
+
|
|
625
|
+
### UI Templates
|
|
626
|
+
|
|
627
|
+
| Method | Path | Description |
|
|
628
|
+
|------------|---------------------------------------------|----------------------------------------------------------|
|
|
629
|
+
| `GET` | `/ui-templates` | List UI templates |
|
|
630
|
+
| `GET` | `/ui-templates/:id` | Get a UI template |
|
|
631
|
+
| `POST` | `/ui-templates` | Create a UI template |
|
|
632
|
+
| `PUT` | `/ui-templates/:id` | Update a UI template |
|
|
633
|
+
| `DELETE` | `/ui-templates/:id` | Delete a UI template |
|
|
634
|
+
| `POST` | `/ui-templates/:id/copy` | Duplicate a UI template |
|
|
635
|
+
| `GET` | `/ui-templates/:id/profile-usage` | List profiles using this template (`ProfileSummary[]`) |
|
|
636
|
+
|
|
637
|
+
### Analytics
|
|
638
|
+
|
|
639
|
+
| Method | Path | Description |
|
|
640
|
+
|--------|-------------------------|----------------------------------------------------|
|
|
641
|
+
| `GET` | `/analytics/opt-in` | Opt-in rate stats aggregated by locale and date |
|
|
642
|
+
|
|
643
|
+
Query params: `tenantId`, `profileId`, `complianceGroup`, `from` (ISO date), `to` (ISO date), `locale`.
|
|
644
|
+
|
|
645
|
+
```json
|
|
646
|
+
{
|
|
647
|
+
"total": 12400,
|
|
648
|
+
"granted": 6800, "denied": 4100, "managed": 1500,
|
|
649
|
+
"grantedPct": 54.8, "deniedPct": 33.1, "managedPct": 12.1,
|
|
650
|
+
"byLocale": { "en": { "total": 8000, "granted": 4400 } },
|
|
651
|
+
"byDate": [{ "date": "2026-07-01", "total": 400, "granted": 220 }]
|
|
652
|
+
}
|
|
653
|
+
```
|
|
390
654
|
|
|
391
655
|
### Consents
|
|
392
656
|
|
|
@@ -408,17 +672,37 @@ IPs are never stored raw — only SHA-256 hashes appear in the `ipHash` field.
|
|
|
408
672
|
|
|
409
673
|
### Users & Roles
|
|
410
674
|
|
|
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
|
|
675
|
+
| Method | Path | Description |
|
|
676
|
+
|------------|-----------------------------------|------------------------------------------------------|
|
|
677
|
+
| `GET` | `/users` | List admin users |
|
|
678
|
+
| `POST` | `/users` | Create an admin user |
|
|
679
|
+
| `PUT` | `/users/:id` | Update an admin user |
|
|
680
|
+
| `GET` | `/roles` | List roles |
|
|
681
|
+
| `POST` | `/roles` | Create a role |
|
|
682
|
+
| `GET` | `/roles/:id/permissions` | Get permissions for a role |
|
|
683
|
+
| `POST` | `/roles/:id/permissions` | Assign a permission to a role |
|
|
684
|
+
| `DELETE` | `/roles/:id/permissions/:permId` | Revoke a permission from a role |
|
|
685
|
+
| `GET` | `/permissions` | List all available permissions |
|
|
686
|
+
|
|
687
|
+
#### User `allowedTenants`
|
|
688
|
+
|
|
689
|
+
Admin users can be scoped to specific tenants. When `allowedTenants` is set, the user can only see and manage data for those tenants:
|
|
690
|
+
|
|
691
|
+
```ts
|
|
692
|
+
// POST /admin/users or PUT /admin/users/:id
|
|
693
|
+
{
|
|
694
|
+
"name": "APAC Manager",
|
|
695
|
+
"email": "apac@example.com",
|
|
696
|
+
"password": "...",
|
|
697
|
+
"allowedTenants": ["tenant-uuid-1", "tenant-uuid-2"] // empty = access to all tenants
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
Users with `role: 'superadmin'` always have access to all tenants regardless of `allowedTenants`.
|
|
702
|
+
|
|
703
|
+
**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`.
|
|
704
|
+
|
|
705
|
+
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
706
|
|
|
423
707
|
### API Keys
|
|
424
708
|
|
|
@@ -430,16 +714,17 @@ IPs are never stored raw — only SHA-256 hashes appear in the `ipHash` field.
|
|
|
430
714
|
|
|
431
715
|
### Stats & Reporting
|
|
432
716
|
|
|
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`
|
|
717
|
+
| Method | Path | Description |
|
|
718
|
+
|--------|-----------------------------------|------------------------------------------|
|
|
719
|
+
| `GET` | `/stats/overview` | Total consents, visitors, GPC count |
|
|
720
|
+
| `GET` | `/stats/timeline` | Daily consent counts (`?days=30`) |
|
|
721
|
+
| `GET` | `/stats/categories` | Per-category grant/deny breakdown |
|
|
722
|
+
| `GET` | `/stats/countries` | Consent count by country |
|
|
723
|
+
| `GET` | `/stats/gpc` | GPC detection statistics |
|
|
724
|
+
| `GET` | `/export/consents` | Export consent records (CSV or JSON) |
|
|
725
|
+
| `GET` | `/export/consents/xlsx` | Export as XLSX |
|
|
726
|
+
| `GET` | `/export/audit` | Export audit log (CSV or JSON) |
|
|
727
|
+
| `GET` | `/export/translations/:profileId` | Export all translatable fields as CSV |
|
|
443
728
|
|
|
444
729
|
### Audit Log
|
|
445
730
|
|
|
@@ -473,46 +758,152 @@ Only active when `tcf.enabled: true`.
|
|
|
473
758
|
|
|
474
759
|
Served at `{basePath}/` when `dashboard: true`.
|
|
475
760
|
|
|
476
|
-
| Section
|
|
477
|
-
|
|
478
|
-
| Dashboard
|
|
479
|
-
| Profiles
|
|
480
|
-
|
|
|
481
|
-
|
|
|
482
|
-
|
|
|
483
|
-
|
|
|
484
|
-
|
|
|
485
|
-
|
|
|
486
|
-
|
|
|
487
|
-
|
|
|
488
|
-
|
|
|
489
|
-
|
|
|
761
|
+
| Section | Description |
|
|
762
|
+
|---------------------|--------------------------------------------------------------------------|
|
|
763
|
+
| Dashboard | Consent overview, timeline chart, country breakdown, GPC stats |
|
|
764
|
+
| Profiles | Create / edit / copy / delete / activate / deactivate consent profiles |
|
|
765
|
+
| Profile History | Version history viewer — compare locale JSON per version |
|
|
766
|
+
| Cookie Templates | Reusable cookie/purpose definitions (blank by default + Load Defaults) |
|
|
767
|
+
| UI Templates | Reusable banner + modal layout settings |
|
|
768
|
+
| Consents | Browse, filter, export consent records; per-visitor history |
|
|
769
|
+
| Visitors | Visitor list with geographic data |
|
|
770
|
+
| Users | Admin user management with allowed-tenant scoping |
|
|
771
|
+
| Roles | RBAC roles and fine-grained permission assignment |
|
|
772
|
+
| Sites | Multi-tenant site management **(superadmin only)** |
|
|
773
|
+
| TCF Vendors | IAB Global Vendor List |
|
|
774
|
+
| Audit Log | Immutable log of all admin actions |
|
|
775
|
+
| Settings / API | API keys, branding, OpenAPI docs **(superadmin only)** |
|
|
776
|
+
|
|
777
|
+
### Dashboard RBAC
|
|
778
|
+
|
|
779
|
+
- **Sites** and **API** sections are visible only to users with `role: 'superadmin'`
|
|
780
|
+
- **TCF Vendors** is visible to all authenticated users
|
|
490
781
|
|
|
491
782
|
### Profile Creation Wizard
|
|
492
783
|
|
|
493
|
-
**Step 1 —
|
|
784
|
+
**Step 1 — Profile metadata:**
|
|
785
|
+
|
|
786
|
+
| Field | Type | Description |
|
|
787
|
+
|---------------------|------------------------------------------------------|-----------------------------------------------------------------------------|
|
|
788
|
+
| `name` | `string` | Human-readable profile name |
|
|
789
|
+
| `defaultLocale` | `string` (BCP 47) | Locale served when visitor locale is unavailable; written as `default.json` |
|
|
790
|
+
| `complianceGroup` | `ComplianceGroupId` | Regulation group for geo-routing (e.g. `opt-in`, `opt-out-strict`) |
|
|
791
|
+
| `gpcMode` | `'ignore' \| 'honor' \| 'strict'` | GPC signal handling. Overrides group default. |
|
|
792
|
+
| `darkMode` | `boolean` | Enable dark mode in the consent banner |
|
|
793
|
+
| `hidePoweredBy` | `boolean` | Hide "Powered by Consenti" branding link |
|
|
794
|
+
| `allowReceipt` | `boolean` | Allow visitors to download a PDF consent receipt |
|
|
795
|
+
| `allowedOrigins` | `string[]` | Allowlisted domains for CORS on this profile's consent endpoints |
|
|
796
|
+
| `complianceConfig` | `Record<string, string>` | Per-compliance extra config (e.g. DPDPA data fiduciary name). Only shown when the selected `complianceGroup` requires it; otherwise omitted. |
|
|
797
|
+
|
|
798
|
+
**Step 2 — Cookie Template:** Define cookie IDs and their properties. Clicking **Next** at the bottom of Step 2 calls `POST /admin/profiles/validate` with the selected template's cookies and the chosen `complianceGroup`:
|
|
494
799
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
| `id` | `string` | Unique identifier referenced in button arrays |
|
|
498
|
-
| `type` | `'consent' \| 'legitimate_interest'` | Legal basis |
|
|
499
|
-
| `mandatory` | `boolean` | Always granted; toggle disabled in modal |
|
|
500
|
-
| `listenGpc` | `boolean` | Auto-denied when GPC signal is active |
|
|
501
|
-
| `expiry` | `number` | Days until re-consent is required (0 = session) |
|
|
800
|
+
- **Compliance errors** (red panel): block advancing to Step 3 until resolved (e.g. template has no cookie with `listenGpc: true` but the group requires GPC support).
|
|
801
|
+
- **Compliance warnings** (amber panel): show an acknowledgment checkbox; the user must check it before advancing. Warnings do not block save — they surface potential issues.
|
|
502
802
|
|
|
503
|
-
|
|
803
|
+
| Field | Type | Description |
|
|
804
|
+
|---------------------|-------------------------------------------------|---------------------------------------------------------------------------|
|
|
805
|
+
| `id` | `string` | Unique identifier referenced in button arrays |
|
|
806
|
+
| `legalBasis` | `'mandatory' \| 'consent' \| 'legitimate_interest'` | Legal basis (replaces deprecated `mandatory`+`type` fields) |
|
|
807
|
+
| `listenGpc` | `boolean` | Auto-denied when GPC signal is active |
|
|
808
|
+
| `expiry` | `number` | Days until re-consent is required (0 = session) |
|
|
809
|
+
| `tcfSpecialFeatures`| `number[]` | IAB TCF special feature IDs (SF1, SF2) associated with this cookie |
|
|
810
|
+
|
|
811
|
+
**Step 3 — UI Template:** Configure banner and modal layout. New UI templates start **blank** (no prefilled buttons or categories). Click **Load Defaults** in the amber callout to populate a sensible starter structure.
|
|
504
812
|
|
|
505
813
|
- **Main Banner / GPC Banner:** `position`, `overlayOpacity`, `showClose`, `headingTag`, `buttons`
|
|
506
814
|
- **Preference Modal:** `position`, `overlayOpacity`, `showClose`, `persistent`, `buttons`, `categories`
|
|
507
815
|
- Button `type`: `primary | secondary | text | reject | submit | manage | close`
|
|
508
816
|
- Button `cookies`: `*` (grant all) · `!` (deny all) · comma-separated IDs
|
|
509
817
|
|
|
510
|
-
**Step
|
|
818
|
+
**Step 4 — Content:** Enter localised copy with live preview:
|
|
511
819
|
|
|
512
820
|
- Main Banner: heading, body HTML
|
|
513
821
|
- GPC Banner: heading, body HTML
|
|
514
822
|
- Preference Modal: heading, subheading, intro HTML, per-category heading + description
|
|
515
823
|
|
|
824
|
+
**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.
|
|
825
|
+
|
|
826
|
+
**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.
|
|
827
|
+
|
|
828
|
+
**Import / Export:**
|
|
829
|
+
|
|
830
|
+
| Button | Action |
|
|
831
|
+
|--------|--------|
|
|
832
|
+
| **JSON** | Exports *all* locale tabs as a single `locales.json` file (`{ "en": {...}, "fr-FR": {...} }`) |
|
|
833
|
+
| **CSV** | Exports the *current* locale tab as a `locale-{locale}.csv` file |
|
|
834
|
+
| **Import** | Accepts either format — multi-locale JSON auto-creates missing locale tabs; skipped locales (invalid BCP 47 key) are listed in an amber callout after import |
|
|
835
|
+
|
|
836
|
+
**Readability callouts:** Inline advisory warnings (yellow) appear beneath `heading` and body HTML fields when:
|
|
837
|
+
- A heading exceeds 80 characters
|
|
838
|
+
- Average sentence length in the body exceeds 25 words
|
|
839
|
+
- Total word count in the body exceeds 150 words
|
|
840
|
+
|
|
841
|
+
These are informational only — the profile can still be saved.
|
|
842
|
+
|
|
843
|
+
### Profile Activation
|
|
844
|
+
|
|
845
|
+
Profiles are **inactive** after creation. To serve them via geo-routing:
|
|
846
|
+
|
|
847
|
+
1. Click **Activate** in the profile list or use `POST /admin/profiles/:id/activate`
|
|
848
|
+
2. Consenti writes locale JSON files to `${storage.path}/profiles/${tenantId}/${complianceGroup}/`
|
|
849
|
+
3. The static file route starts serving these files immediately (zero DB on the hot path)
|
|
850
|
+
|
|
851
|
+
Only one profile per `complianceGroup` can be active at a time. Activating a new profile automatically deactivates the existing one.
|
|
852
|
+
|
|
853
|
+
### Profile Version History
|
|
854
|
+
|
|
855
|
+
Every profile save creates an immutable snapshot at `${profileId}/${version}/`. The dashboard version history page (`#/banners/profiles/:id/history`) shows:
|
|
856
|
+
- Left panel: list of all versions with dates
|
|
857
|
+
- Right panel: prettified JSON for the selected version; locale switcher dropdown
|
|
858
|
+
|
|
859
|
+
Via API:
|
|
860
|
+
```http
|
|
861
|
+
GET /consenti/admin/profiles/:id/versions
|
|
862
|
+
GET /consenti/admin/profiles/:id/versions/3?locale=fr-FR
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
### Template Save Safety
|
|
866
|
+
|
|
867
|
+
When saving a **Cookie 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/cookie-templates/:id/profile-usage` or `GET /admin/ui-templates/:id/profile-usage` internally.
|
|
868
|
+
|
|
869
|
+
When **deleting** a Cookie Template used by active profiles, the delete is blocked with a 422 error listing those profiles. Deactivate the affected profiles before deleting the template.
|
|
870
|
+
|
|
871
|
+
When **removing cookies** from a Cookie Template, the backend checks whether the reduced cookie set satisfies the `complianceGroup` rules for every profile using the template. If any profile's compliance would break, the save is blocked with a 422 listing the `blockingProfiles` and `removedCookieIds` — see [Cookie Template Safety Guards](#cookie-template-safety-guards).
|
|
872
|
+
|
|
873
|
+
---
|
|
874
|
+
|
|
875
|
+
## EventBus
|
|
876
|
+
|
|
877
|
+
`createConsenti()` returns an `eventBus` — a standard Node.js `EventEmitter` — that fires events at every data lifecycle step.
|
|
878
|
+
|
|
879
|
+
| Event | Payload | Description |
|
|
880
|
+
|---|---|---|
|
|
881
|
+
| `ready` | none | Storage connected and admin user bootstrapped |
|
|
882
|
+
| `consent.created` | `ConsentDbRecord` | New consent record saved |
|
|
883
|
+
| `consent.updated` | `{ previous, current: ConsentDbRecord }` | Consent record updated |
|
|
884
|
+
| `consent.erased` | `{ visitorId: string }` | Visitor data erased (GDPR erasure) |
|
|
885
|
+
| `visitor.created` | `Visitor` | New visitor record created |
|
|
886
|
+
| `profile.created` | `Profile` | Profile created |
|
|
887
|
+
| `profile.updated` | `{ previous, current: Profile }` | Profile updated (version bumped) |
|
|
888
|
+
| `profile.deleted` | `{ id: string, previous: Profile }` | Profile deleted |
|
|
889
|
+
| `cache:warm` | `{ paths: string[], version: number }` | Fired after locale JSON files are written; paths to warm in CDN/cache |
|
|
890
|
+
| `cache:purge` | `{ paths: string[], version: number }` | Fired after locale JSON files are removed; paths to purge from CDN/cache |
|
|
891
|
+
|
|
892
|
+
`cache:warm` and `cache:purge` mirror the `handleCache` config callback — use whichever suits your integration.
|
|
893
|
+
|
|
894
|
+
```ts
|
|
895
|
+
const { eventBus } = createConsenti({ /* ... */ })
|
|
896
|
+
|
|
897
|
+
eventBus.on('consent.created', (record) => {
|
|
898
|
+
// send to analytics, trigger webhook, etc.
|
|
899
|
+
console.log('New consent from visitor:', record.visitorId)
|
|
900
|
+
})
|
|
901
|
+
|
|
902
|
+
eventBus.on('consent.erased', ({ visitorId }) => {
|
|
903
|
+
myDmpClient.deleteVisitor(visitorId)
|
|
904
|
+
})
|
|
905
|
+
```
|
|
906
|
+
|
|
516
907
|
---
|
|
517
908
|
|
|
518
909
|
## Plugins (Server-side)
|
|
@@ -595,13 +986,26 @@ await consenti.destroy()
|
|
|
595
986
|
|
|
596
987
|
## Environment Variables
|
|
597
988
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
|
601
|
-
|
|
602
|
-
| `
|
|
603
|
-
| `
|
|
604
|
-
| `
|
|
989
|
+
All config fields can be set via environment variables. Code config takes precedence over env vars.
|
|
990
|
+
|
|
991
|
+
| Variable | Config field | Default |
|
|
992
|
+
|-------------------------------------|------------------------------------|----------------------------------|
|
|
993
|
+
| `NODE_ENV` | — | Suppress stack traces when `'production'` |
|
|
994
|
+
| `CONSENTI_BASE_PATH` | `basePath` | `'/consenti'` |
|
|
995
|
+
| `CONSENTI_ADMIN_EMAIL` | `auth.adminEmail` | `'user@consenti.dev'` |
|
|
996
|
+
| `CONSENTI_ADMIN_PASSWORD` | `auth.adminPassword` | `'Consenti@123'` |
|
|
997
|
+
| `CONSENTI_ADMIN_JWT_SECRET` | `auth.jwtSecret` | random per restart |
|
|
998
|
+
| `CONSENTI_DB_DRIVER` | `storage.driver` | `'json'` |
|
|
999
|
+
| `CONSENTI_DB_PATH` | `storage.path` | `'./consenti-data'` |
|
|
1000
|
+
| `CONSENTI_DB_URI` | `storage.uri` | — |
|
|
1001
|
+
| `CONSENTI_DB_DATABASE` | `storage.database` | — |
|
|
1002
|
+
| `CONSENTI_DB_HOST` | `storage.host` | — |
|
|
1003
|
+
| `CONSENTI_DB_PORT` | `storage.port` | — |
|
|
1004
|
+
| `CONSENTI_DB_USER` | `storage.user` | — |
|
|
1005
|
+
| `CONSENTI_DB_PASSWORD` | `storage.password` | — |
|
|
1006
|
+
| `CONSENTI_RATE_LIMIT_WINDOW_MS` | `rateLimit.windowMs` | `60000` |
|
|
1007
|
+
| `CONSENTI_RATE_LIMIT_MAX_REQUESTS` | `rateLimit.maxRequests` | `60` |
|
|
1008
|
+
| `CONSENTI_MAX_BODY_SIZE` | `maxBodySize` | `1048576` (1 MB) |
|
|
605
1009
|
|
|
606
1010
|
---
|
|
607
1011
|
|
|
@@ -610,6 +1014,15 @@ await consenti.destroy()
|
|
|
610
1014
|
When running, Swagger UI is available at `{basePath}/api/docs`.
|
|
611
1015
|
OpenAPI JSON at `{basePath}/api/openapi.json`.
|
|
612
1016
|
|
|
1017
|
+
|
|
1018
|
+
---
|
|
1019
|
+
|
|
1020
|
+
## Demo
|
|
1021
|
+
For detailed API docs, Guides and Demo, visit [consenti.dev](https://consenti.dev)
|
|
1022
|
+
|
|
1023
|
+
Visit author's profile at [santosh.top](https://santosh.top)
|
|
1024
|
+
|
|
1025
|
+
|
|
613
1026
|
---
|
|
614
1027
|
|
|
615
1028
|
## Contributing
|