@consenti/api 0.3.0 → 0.5.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 +212 -74
- package/dist/chunk-COR72NFQ.js +2 -0
- package/dist/chunk-COR72NFQ.js.map +1 -0
- package/dist/chunk-S2IEYESR.js +2 -0
- package/dist/chunk-S2IEYESR.js.map +1 -0
- package/dist/dashboard/assets/ApiConfig-dmDLJNfI.js +1 -0
- package/dist/dashboard/assets/ArchivedProfileList-ChWp1RHE.js +1 -0
- package/dist/dashboard/assets/AuditLog-3lekUOk8.js +1 -0
- package/dist/dashboard/assets/ChangePassword-BYlF08cE.js +1 -0
- package/dist/dashboard/assets/ConfirmDialog-sMBVcbiI.js +1 -0
- package/dist/dashboard/assets/ConsentList-DLDBSjxO.js +1 -0
- package/dist/dashboard/assets/ConsentTemplateEditor-0iB6G95-.js +1 -0
- package/dist/dashboard/assets/ConsentTemplateList-BnU_nL8h.js +1 -0
- package/dist/dashboard/assets/HeadingTagInput-7owwn9Vj.js +1 -0
- package/dist/dashboard/assets/HowItWorks-B05y-6xX.js +1 -0
- package/dist/dashboard/assets/PermissionGate-D45f3WrO.js +1 -0
- package/dist/dashboard/assets/PreviewPane-Czax39la.js +38 -0
- package/dist/dashboard/assets/ProfileEditor-CuMRNzdM.js +2 -0
- package/dist/dashboard/assets/ProfileList-CFZ_i1zk.js +1 -0
- package/dist/dashboard/assets/ProfileVersionHistory-40HtQSrd.js +3 -0
- package/dist/dashboard/assets/RecordDetailModal-hpJgK02E.js +1 -0
- package/dist/dashboard/assets/Reports-BlvcycZ8.js +1 -0
- package/dist/dashboard/assets/RoleList-CTeQdx6f.js +1 -0
- package/dist/dashboard/assets/Select-CeC6GRyb.js +1 -0
- package/dist/dashboard/assets/Settings-DLsvGtMh.js +1 -0
- package/dist/dashboard/assets/SetupWizard-BVzGrBm2.js +1 -0
- package/dist/dashboard/assets/Table-CcBP7eXS.js +1 -0
- package/dist/dashboard/assets/TenantList-DLgPnlVv.js +1 -0
- package/dist/dashboard/assets/UITemplateEditor-B9EBcvOB.js +2 -0
- package/dist/dashboard/assets/UITemplateList-CGYRRlCh.js +1 -0
- package/dist/dashboard/assets/UserList-BbpiUrVr.js +1 -0
- package/dist/dashboard/assets/VendorList-mlOlW7ht.js +1 -0
- package/dist/dashboard/assets/VisitorList-Dne7xkNx.js +1 -0
- package/dist/dashboard/assets/check-IM5PInrX.js +1 -0
- package/dist/dashboard/assets/copy-FTaGH7-z.js +1 -0
- package/dist/dashboard/assets/de-jwngydrC.js +25 -0
- package/dist/dashboard/assets/dist-GoiMJCTt.js +1 -0
- package/dist/dashboard/assets/es-B6Jp0OPe.js +25 -0
- package/dist/dashboard/assets/external-link-5y29qnhn.js +1 -0
- package/dist/dashboard/assets/fr-CshYcQPW.js +25 -0
- package/dist/dashboard/assets/index-BaYVhd1M.js +26 -0
- package/dist/dashboard/assets/index-Cr5M1Qy_.css +2 -0
- package/dist/dashboard/assets/ja-CgjZNBrc.js +25 -0
- package/dist/dashboard/assets/profiles-D-hvl_fK.js +1 -0
- package/dist/dashboard/assets/roles-DZyZwZDr.js +1 -0
- package/dist/dashboard/assets/rotate-ccw-clock-C5cVu2_f.js +1 -0
- package/dist/dashboard/assets/search-mdMD-WTj.js +1 -0
- package/dist/dashboard/assets/square-pen-BoEcHaSW.js +1 -0
- package/dist/dashboard/assets/templates-CIyMjskA.js +1 -0
- package/dist/dashboard/assets/templates-DLMsZs7d.js +1 -0
- package/dist/dashboard/assets/trash-BVDX09Xe.js +1 -0
- package/dist/dashboard/assets/useHashQuery-wXzkY-tk.js +1 -0
- package/dist/dashboard/assets/users-BLTbK4tk.js +1 -0
- package/dist/dashboard/assets/x-CtCy0YcJ.js +1 -0
- package/dist/dashboard/assets/zh-CN-BnLZkfBC.js +25 -0
- package/dist/dashboard/index.html +2 -4
- package/dist/dist-QJYW5KUK.js +2 -0
- package/dist/index.cjs +69 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +271 -100
- package/dist/index.d.ts +271 -100
- package/dist/index.js +67 -66
- package/dist/index.js.map +1 -1
- package/dist/profiles-FCX454HS.js +2 -0
- package/dist/widget/widget.css +1 -1
- package/dist/widget/widget.js +1 -1
- package/package.json +27 -12
- package/dist/chunk-IY4L6MNB.js +0 -2
- package/dist/chunk-IY4L6MNB.js.map +0 -1
- package/dist/chunk-O5UG4OYI.js +0 -2
- package/dist/chunk-O5UG4OYI.js.map +0 -1
- package/dist/dashboard/assets/ApiConfig-CE-Noelz.js +0 -1
- package/dist/dashboard/assets/ArchivedProfileList-Bw-fx-BD.js +0 -1
- package/dist/dashboard/assets/AuditLog-D6HiTQ1o.js +0 -1
- package/dist/dashboard/assets/ChangePassword-D2rPkzD1.js +0 -1
- package/dist/dashboard/assets/ConfirmDialog-DTtw7VJ-.js +0 -1
- package/dist/dashboard/assets/ConsentList-BWOvPOMG.js +0 -1
- package/dist/dashboard/assets/ConsentTemplateEditor-BKIEnLmA.js +0 -1
- package/dist/dashboard/assets/ConsentTemplateList-DCys2nM3.js +0 -1
- package/dist/dashboard/assets/HeadingTagInput-BTjrdzlS.js +0 -1
- package/dist/dashboard/assets/HowItWorks-AeyjfQGO.js +0 -1
- package/dist/dashboard/assets/PermissionGate-Bhhf7SMh.js +0 -1
- package/dist/dashboard/assets/PreviewPane-Dq5K1h2L.js +0 -35
- package/dist/dashboard/assets/ProfileEditor-BXtaE0tL.js +0 -2
- package/dist/dashboard/assets/ProfileList-CWKSAgu0.js +0 -1
- package/dist/dashboard/assets/ProfileVersionHistory-1h8sUgog.js +0 -3
- package/dist/dashboard/assets/RecordDetailModal-FaFhzQXi.js +0 -1
- package/dist/dashboard/assets/Reports-DWEjQOKh.js +0 -1
- package/dist/dashboard/assets/RoleList-BSGoqlzp.js +0 -1
- package/dist/dashboard/assets/Select-DjJNwlbY.js +0 -1
- package/dist/dashboard/assets/Settings-CVYrlyLm.js +0 -1
- package/dist/dashboard/assets/SetupWizard-4NTb5bZj.js +0 -1
- package/dist/dashboard/assets/Table-DH_jh8_N.js +0 -1
- package/dist/dashboard/assets/TenantList-5AwFtoHf.js +0 -1
- package/dist/dashboard/assets/UITemplateEditor-9A55scBe.js +0 -1
- package/dist/dashboard/assets/UITemplateList-BS7X4u4F.js +0 -1
- package/dist/dashboard/assets/UserList-SPkMyFRy.js +0 -1
- package/dist/dashboard/assets/VendorList-C5fPhr6U.js +0 -1
- package/dist/dashboard/assets/VisitorList-COP1SyNd.js +0 -1
- package/dist/dashboard/assets/check-o5cp1s2J.js +0 -1
- package/dist/dashboard/assets/client-ymc9nwWr.js +0 -1
- package/dist/dashboard/assets/copy-C01KoUUH.js +0 -1
- package/dist/dashboard/assets/createLucideIcon-MdjI3m3u.js +0 -1
- package/dist/dashboard/assets/de-DET92Jb5.js +0 -5
- package/dist/dashboard/assets/dist-DlhLroYC.js +0 -1
- package/dist/dashboard/assets/es-CJNunrnU.js +0 -5
- package/dist/dashboard/assets/external-link-DVe00ZBK.js +0 -1
- package/dist/dashboard/assets/fr-akfTuf5x.js +0 -5
- package/dist/dashboard/assets/history-CHTts_m2.js +0 -1
- package/dist/dashboard/assets/index-DW3qOrry.css +0 -2
- package/dist/dashboard/assets/index-H_Zm0X-u.js +0 -6
- package/dist/dashboard/assets/ja-B0nLWkOd.js +0 -5
- package/dist/dashboard/assets/profiles-DmrdGv7t.js +0 -1
- package/dist/dashboard/assets/roles-C0dJPFl4.js +0 -1
- package/dist/dashboard/assets/search-Bd2RdDRh.js +0 -1
- package/dist/dashboard/assets/templates-B2xiivh4.js +0 -1
- package/dist/dashboard/assets/templates-CnMoLavN.js +0 -1
- package/dist/dashboard/assets/trash-2-BBnkdgm_.js +0 -1
- package/dist/dashboard/assets/trash-Dj1jhzHJ.js +0 -1
- package/dist/dashboard/assets/useHashQuery-B5ytMIMp.js +0 -1
- package/dist/dashboard/assets/users-czK8LRVh.js +0 -1
- package/dist/dashboard/assets/x-BRuyBEQP.js +0 -1
- package/dist/dashboard/assets/zh-CN-Dxg3G-BL.js +0 -5
- package/dist/dist-Z5RVTVBT.js +0 -2
- package/dist/profiles-XTYPWLVC.js +0 -2
- /package/dist/{dist-Z5RVTVBT.js.map → dist-QJYW5KUK.js.map} +0 -0
- /package/dist/{profiles-XTYPWLVC.js.map → profiles-FCX454HS.js.map} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @consenti/api
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
GDPR-style consent management platform backend for Node.js. No required runtime dependencies — SQLite storage and everything else works out of the box; Mongo/MySQL/Postgres storage and spec-correct IAB TCF encoding are opt-in via optional peer dependencies.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@consenti/api)
|
|
6
6
|
[](../../LICENSE)
|
|
@@ -13,8 +13,11 @@ Zero-dependency, GDPR-compliant Consent Management Platform backend for Node.js.
|
|
|
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** — Maintained: GDPR/UK-GDPR, CCPA/CPRA + US state laws, LGPD. Partial: IAB TCF v2.3 (spec-correct encoding needs `tcf.publisherCC` + the optional `@iabtechlabtcf/core` peer dependency) — only relevant if you monetize through programmatic/RTB ad exchanges. In development: DPDPA. Geo-routing and compliance groups extend well beyond that tier (APAC, Middle East, Africa, and more) — see [ECOSYSTEM.md](../../ECOSYSTEM.md) for per-regulation status; contributions to extend or maintain coverage are welcome
|
|
17
|
+
|
|
18
|
+
> **Registration is only needed for programmatic ad monetization.** For first-party analytics/marketing consent, Consenti is fully compliant with zero external registration. Consenti implements the IAB TCF/GPP technical specs but is **not** itself a registered CMP; if you do need TCF/GPP, you must register your own `cmpId` with IAB Europe/MSPA independently before going live — see the [TCF & GPP Registration Guide](https://consenti.dev/docs/compliance/tcf-and-gpp-registration).
|
|
17
19
|
- **Pluggable storage** — SQLite (zero-config, default) · PostgreSQL · MySQL · MongoDB
|
|
20
|
+
- **Companion CLI**: [`@consenti/scanner`](../scanner) crawls a site under none/reject-all/accept-all consent states and reports undeclared third-party trackers
|
|
18
21
|
|
|
19
22
|
---
|
|
20
23
|
|
|
@@ -77,7 +80,7 @@ On Node 20/21, choose between `node-sqlite3-wasm` (pure WASM, no compilation) or
|
|
|
77
80
|
|
|
78
81
|
Consenti ships with four geo resolver options. The default requires zero installation; the others are opt-in.
|
|
79
82
|
|
|
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 }
|
|
83
|
+
**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 }`, plus an optional `complianceGroup?: string` — when set, that group is used directly, skipping the country/region jurisdiction-map lookup entirely (for a provider that already carries legal-grade jurisdiction data and needs to route into an operator-defined custom group).
|
|
81
84
|
|
|
82
85
|
> **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
86
|
|
|
@@ -109,6 +112,7 @@ npm install maxmind
|
|
|
109
112
|
|
|
110
113
|
# OR 5. Custom (optional — only needed when geoDataProvider: CountryResolverFn)
|
|
111
114
|
# Do what it requires, just return `{ country, region, locale }` in supported format
|
|
115
|
+
# (plus an optional `complianceGroup` to skip the jurisdiction-map lookup entirely)
|
|
112
116
|
|
|
113
117
|
```
|
|
114
118
|
---
|
|
@@ -142,7 +146,7 @@ const consenti = createConsenti({
|
|
|
142
146
|
mode: 'local', // 'local' | 'jwt' | 'oidc' | 'saml' | 'custom'
|
|
143
147
|
adminEmail: 'user@consenti.dev',
|
|
144
148
|
adminPassword: process.env.CONSENTI_ADMIN_PASSWORD ?? "Consenti@123",
|
|
145
|
-
|
|
149
|
+
masterSecret: process.env.CONSENTI_ADMIN_MASTER_SECRET, // auto-generated if omitted (sessions expire on restart)
|
|
146
150
|
|
|
147
151
|
// OIDC (Auth0, Keycloak, Google, etc.)
|
|
148
152
|
// mode: 'oidc',
|
|
@@ -150,7 +154,7 @@ const consenti = createConsenti({
|
|
|
150
154
|
// issuer: 'https://your-idp.example.com',
|
|
151
155
|
// clientId: 'your-client-id',
|
|
152
156
|
// clientSecret: process.env.OIDC_SECRET,
|
|
153
|
-
// redirectUri: 'https://app.example.com/consenti/admin/auth/oidc/callback',
|
|
157
|
+
// redirectUri: 'https://app.example.com/consenti/admin/v1/auth/oidc/callback',
|
|
154
158
|
// claimsMapping: { email: 'email', roles: 'consenti_roles' },
|
|
155
159
|
// },
|
|
156
160
|
|
|
@@ -160,7 +164,7 @@ const consenti = createConsenti({
|
|
|
160
164
|
// issuer: 'https://idp.example.com',
|
|
161
165
|
// entryPoint: 'https://idp.example.com/sso/saml',
|
|
162
166
|
// cert: process.env.SAML_CERT!, // IdP signing cert (PEM, no headers)
|
|
163
|
-
// callbackUrl: 'https://app.example.com/consenti/admin/auth/saml/
|
|
167
|
+
// callbackUrl: 'https://app.example.com/consenti/admin/v1/auth/saml/acs',
|
|
164
168
|
// },
|
|
165
169
|
|
|
166
170
|
// Custom — bring your own authentication function
|
|
@@ -187,12 +191,54 @@ const consenti = createConsenti({
|
|
|
187
191
|
},
|
|
188
192
|
|
|
189
193
|
// ── Compliance ───────────────────────────────────────────────────────────────
|
|
194
|
+
// Every compliance-program setting lives here — jurisdiction routing, IAB TCF, consent-record
|
|
195
|
+
// signing, and data retention. (Age gate is per-profile now, not a server-wide setting — see
|
|
196
|
+
// the dashboard's Profile Editor Step 1, and the widget's `profile.ageGate`.)
|
|
190
197
|
compliance: {
|
|
191
198
|
// Geo-based auto routing (recommended)
|
|
192
199
|
type: 'auto', // 'auto' = geo-resolve per visitor | ComplianceGroupId = one fixed group globally
|
|
193
200
|
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
|
-
|
|
195
|
-
//
|
|
201
|
+
complianceMap: 'default', // 'default' = embedded 240-country map | a URL string (fetched, refreshed per Cache-Control/Expires, 24h fallback) | an operator-supplied ComplianceMapData object
|
|
202
|
+
// complianceMap: 'https://consenti.dev/data/v1/compliance-map.json',
|
|
203
|
+
// A region under overriddenRegions can set requiresSensitiveOptIn: true — a per-region carve-out
|
|
204
|
+
// (not a different group) denying cpraCategory:'sensitive' cookies by default for that region only.
|
|
205
|
+
// The embedded map sets this for Colorado's CPA already; only takes effect with a geoip/maxmind
|
|
206
|
+
// geoDataProvider (the timezone/language heuristic never resolves a US state).
|
|
207
|
+
|
|
208
|
+
// IAB TCF v2.3
|
|
209
|
+
tcf: {
|
|
210
|
+
enabled: false,
|
|
211
|
+
cmpId: 123, // your IAB-registered CMP ID (required when enabled)
|
|
212
|
+
cmpVersion: 1, // your CMP software version
|
|
213
|
+
publisherCC: 'DE', // ISO 3166-1 alpha-2 publisher country — required for spec-correct
|
|
214
|
+
// binary TC-string encoding; omitted → simplified fallback string
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
// IAB GPP (US National / "usnat" section only)
|
|
218
|
+
gpp: {
|
|
219
|
+
enabled: false,
|
|
220
|
+
cmpId: 123, // must match the widget's compliance.gpp.cmpId
|
|
221
|
+
cmpVersion: 1,
|
|
222
|
+
mspaCoveredTransaction: false, // whether this deployment falls under MSPA signatory obligations — required, no honest default
|
|
223
|
+
mspaOptOutOptionMode: 0, // 0 = not applicable | 1 = yes | 2 = no
|
|
224
|
+
mspaServiceProviderMode: 0,
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
// Signs server-stored consent records, ownership cookies, and parental-consent tokens, and
|
|
228
|
+
// salts hashed IPs — HMAC-SHA256 over each record's core fields at create/update time,
|
|
229
|
+
// hex-encoded into `signature`, checked on `GET /consent/:visitorId/verify`. Independent of
|
|
230
|
+
// the widget's own cookie-signing (`core.cookieSigningKey`, which protects the visitor's
|
|
231
|
+
// local cookie). Auto-generated in memory if omitted — fine for local dev, but every restart
|
|
232
|
+
// invalidates outstanding ownership cookies, in-flight parental-consent tokens, and (if you
|
|
233
|
+
// compare across restarts) hashed-IP continuity. Set this for production.
|
|
234
|
+
dataSigningHash: process.env.CONSENTI_DATA_SIGNING_HASH,
|
|
235
|
+
parentalConsentTokenTtlDays: 7, // expiry window for parental-consent tokens (only meaningful when dataSigningHash is set)
|
|
236
|
+
|
|
237
|
+
// Data retention
|
|
238
|
+
dataRetention: {
|
|
239
|
+
purgeAfterDays: 365, // delete consent records older than N days; runs nightly
|
|
240
|
+
auditLogPurgeAfterDays: 730, // optional, independent from purgeAfterDays — unset = keep forever
|
|
241
|
+
},
|
|
196
242
|
},
|
|
197
243
|
|
|
198
244
|
// ── S3 profile file sync (optional) ─────────────────────────────────────────
|
|
@@ -221,29 +267,6 @@ const consenti = createConsenti({
|
|
|
221
267
|
// Example: purge CDN edge nodes, update nginx cache keys, etc.
|
|
222
268
|
},
|
|
223
269
|
|
|
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
270
|
// ── Multi-tenant ─────────────────────────────────────────────────────────────
|
|
248
271
|
multiTenant: {
|
|
249
272
|
enabled: false, // scope all data to tenant from X-Tenant-ID header
|
|
@@ -319,7 +342,7 @@ server.listen(3001, () => {
|
|
|
319
342
|
### Express / Connect
|
|
320
343
|
|
|
321
344
|
```ts
|
|
322
|
-
app.use(consenti.router) // as middleware (calls next() for
|
|
345
|
+
app.use(consenti.router) // as middleware (calls next() for requests outside its basePath)
|
|
323
346
|
// — or —
|
|
324
347
|
app.use(consenti.handler) // as a terminal handler (no next())
|
|
325
348
|
```
|
|
@@ -548,7 +571,7 @@ Content-Type: application/json
|
|
|
548
571
|
}
|
|
549
572
|
```
|
|
550
573
|
|
|
551
|
-
`ageVerified`/`parentalConsentToken` are optional — only sent by the widget when `
|
|
574
|
+
`ageVerified`/`parentalConsentToken` are optional — only sent by the widget when the active profile's `ageGate.enabled: true` (a per-profile, per-locale dashboard setting — see the `@consenti/ui` README's "Age Gate" section). Both are stored as-is; this endpoint doesn't validate or enforce them.
|
|
552
575
|
|
|
553
576
|
Response `201`:
|
|
554
577
|
|
|
@@ -582,25 +605,60 @@ GET /consenti/api/v1/consent/visitor-uuid/verify
|
|
|
582
605
|
|
|
583
606
|
`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
607
|
|
|
608
|
+
### Parental consent request/resolve
|
|
609
|
+
|
|
610
|
+
See "Parental consent request/resolve (stateless hook)" under Security below for the full design and caveats.
|
|
611
|
+
|
|
612
|
+
```http
|
|
613
|
+
POST /consenti/api/v1/consent/visitor-uuid/parental-consent-request
|
|
614
|
+
Cookie: consenti_visitor-uuid=...
|
|
615
|
+
|
|
616
|
+
{ "profileId": "my-profile-id" }
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
```json
|
|
620
|
+
{ "token": "pcon_eyJ2aXNpdG9ySWQiOi...ab12cd34..." }
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
```http
|
|
624
|
+
POST /consenti/api/v1/consent/parental-consent-resolve
|
|
625
|
+
|
|
626
|
+
{ "token": "pcon_eyJ2aXNpdG9ySWQiOi...ab12cd34..." }
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
```json
|
|
630
|
+
{ "visitorId": "visitor-uuid", "profileId": "my-profile-id" }
|
|
631
|
+
|
|
632
|
+
// or, on an invalid/expired/tampered token:
|
|
633
|
+
// 403 { "error": "Invalid or expired token (invalid_signature)" }
|
|
634
|
+
```
|
|
635
|
+
|
|
585
636
|
---
|
|
586
637
|
|
|
587
638
|
## Admin REST API
|
|
588
639
|
|
|
589
|
-
Base path: `/consenti/admin
|
|
640
|
+
Base path: `/consenti/admin/v1` (change with `basePath` in config). Every route requires `Authorization: Bearer <jwt>`.
|
|
590
641
|
|
|
591
|
-
Obtain a token via `POST /consenti/admin/auth/login`.
|
|
642
|
+
Obtain a token via `POST /consenti/admin/v1/auth/login`.
|
|
592
643
|
|
|
593
644
|
### Auth
|
|
594
645
|
|
|
595
|
-
| Method | Path
|
|
596
|
-
|
|
597
|
-
| `POST` | `/auth/login`
|
|
598
|
-
| `GET` | `/auth/me`
|
|
599
|
-
| `POST` | `/auth/logout`
|
|
600
|
-
| `POST` | `/auth/refresh`
|
|
646
|
+
| Method | Path | Description |
|
|
647
|
+
|--------|-------------------------|-----------------------------------|
|
|
648
|
+
| `POST` | `/auth/login` | Authenticate (mode `'local'` only) — returns a JWT |
|
|
649
|
+
| `GET` | `/auth/me` | Get current authenticated user (includes `totpEnabled`) |
|
|
650
|
+
| `POST` | `/auth/logout` | Invalidate session |
|
|
651
|
+
| `POST` | `/auth/refresh` | Reissue a fresh token from the current one — extends the session |
|
|
652
|
+
| `GET` | `/auth/oidc/authorize` | Start OIDC authorization (PKCE) — 302-redirects to the IdP (mode `'oidc'` only) |
|
|
653
|
+
| `GET` | `/auth/oidc/callback` | OIDC redirect target — exchanges the code for tokens, verifies the ID token, upserts the admin user from claims, returns a JWT |
|
|
654
|
+
| `GET` | `/auth/saml/metadata` | SAML SP metadata XML for your IdP config (mode `'saml'` only) |
|
|
655
|
+
| `POST` | `/auth/saml/acs` | SAML Assertion Consumer Service — validates the `SAMLResponse`, upserts the admin user, returns a JWT |
|
|
656
|
+
| `POST` | `/auth/totp/setup` | Generate a TOTP secret for the current user and a QR-code URL (`totpEnabled` stays `false` until verified) |
|
|
657
|
+
| `POST` | `/auth/totp/verify` | Verify a TOTP code against the pending secret and set `totpEnabled: true` |
|
|
658
|
+
| `POST` | `/auth/totp/disable` | Clear the TOTP secret and set `totpEnabled: false` |
|
|
601
659
|
|
|
602
660
|
```http
|
|
603
|
-
POST /consenti/admin/auth/login
|
|
661
|
+
POST /consenti/admin/v1/auth/login
|
|
604
662
|
Content-Type: application/json
|
|
605
663
|
|
|
606
664
|
{ "email": "admin@example.com", "password": "your-password" }
|
|
@@ -615,6 +673,19 @@ login — the dashboard calls `POST /auth/refresh` on user activity (mouse/keybo
|
|
|
615
673
|
extend it another 30 minutes, and proactively logs the user out the moment 30 minutes pass with no
|
|
616
674
|
activity at all, rather than leaving the session to silently lapse in the background.
|
|
617
675
|
|
|
676
|
+
**SSO (OIDC / SAML)**: set `auth.mode: 'oidc'` (with `auth.oidc: { issuer, clientId, clientSecret,
|
|
677
|
+
redirectUri, claimsMapping? }`) or `auth.mode: 'saml'` (with `auth.saml: { issuer, entryPoint, cert,
|
|
678
|
+
callbackUrl }`) — see the `auth` block in [Full Configuration](#full-configuration). Both flows
|
|
679
|
+
upsert (create-if-missing) an admin user by email on first successful login and, for OIDC, assign
|
|
680
|
+
roles from `claimsMapping.roles` when the IdP's claim matches an existing role name; there is no
|
|
681
|
+
separate admin-created-user requirement for SSO logins. `POST /auth/login` (local email/password)
|
|
682
|
+
returns `400` when `auth.mode` isn't `'local'`.
|
|
683
|
+
|
|
684
|
+
**TOTP (per-user MFA)**: each admin user can independently enable TOTP via `setup` → scan the QR
|
|
685
|
+
code → `verify`. This is opt-in per user and layered on top of whichever `auth.mode` is active; it
|
|
686
|
+
is not currently enforced as a required second factor on `POST /auth/login` itself — treat it as
|
|
687
|
+
available account-hardening, not a login gate, until wired into the login flow.
|
|
688
|
+
|
|
618
689
|
### Profiles
|
|
619
690
|
|
|
620
691
|
| Method | Path | Description |
|
|
@@ -630,6 +701,7 @@ activity at all, rather than leaving the session to silently lapse in the backgr
|
|
|
630
701
|
| `POST` | `/profiles/:id/deactivate` | Deactivate a profile — removes `${complianceGroup}/` locale files |
|
|
631
702
|
| `GET` | `/profiles/:id/versions` | List every version snapshot on disk, newest first (`{ version, createdAt, locales[] }[]`) |
|
|
632
703
|
| `GET` | `/profiles/:id/versions/:entryId` | Read a specific version's locale file, `:entryId` is its `version` number (query: `?locale=en`) |
|
|
704
|
+
| `GET` | `/profiles/archived` | List profile-id directories on disk with no matching DB row (deleted profiles whose version-snapshot history survives) — `ArchivedProfileSummary[]` |
|
|
633
705
|
| `POST` | `/profiles/validate` | Validate a consent template (`cookies` + `categories`) against a compliance group (no save) |
|
|
634
706
|
| `GET` | `/compliance-coverage` | Active profile (or null) per compliance group — powers coverage panel |
|
|
635
707
|
|
|
@@ -748,17 +820,23 @@ Query params for `GET /consents`: `page`, `limit` (max 500), `profileId`, `from`
|
|
|
748
820
|
|
|
749
821
|
Query params for `GET /visitors`: `page`, `limit` (max 500), `from`, `to`, `q` (**prefix** search across visitorId, country). Response: `{ items, total, page, limit }`.
|
|
750
822
|
|
|
751
|
-
IPs are never stored raw —
|
|
823
|
+
IPs are never stored raw — masked (last IPv4 octet / last 80 bits of IPv6 zeroed), salted with `compliance.dataSigningHash`, and SHA-256 hashed into the `ipHash` field.
|
|
752
824
|
|
|
753
825
|
### Users & Roles
|
|
754
826
|
|
|
755
827
|
| Method | Path | Description |
|
|
756
828
|
|------------|-----------------------------------|------------------------------------------------------|
|
|
757
829
|
| `GET` | `/users` | List admin users |
|
|
830
|
+
| `GET` | `/users/:id` | Get an admin user |
|
|
758
831
|
| `POST` | `/users` | Create an admin user |
|
|
759
832
|
| `PUT` | `/users/:id` | Update an admin user |
|
|
833
|
+
| `DELETE` | `/users/:id` | Delete an admin user |
|
|
834
|
+
| `POST` | `/users/:id/roles` | Assign a role to a user |
|
|
835
|
+
| `DELETE` | `/users/:id/roles/:roleId` | Revoke a role from a user |
|
|
760
836
|
| `GET` | `/roles` | List roles |
|
|
761
837
|
| `POST` | `/roles` | Create a role |
|
|
838
|
+
| `PUT` | `/roles/:id` | Update a role |
|
|
839
|
+
| `DELETE` | `/roles/:id` | Delete a role |
|
|
762
840
|
| `GET` | `/roles/:id/permissions` | Get permissions for a role |
|
|
763
841
|
| `POST` | `/roles/:id/permissions` | Assign a permission to a role |
|
|
764
842
|
| `DELETE` | `/roles/:id/permissions/:permId` | Revoke a permission from a role |
|
|
@@ -805,10 +883,10 @@ Config page (split into two panels, one per API).
|
|
|
805
883
|
| `PATCH` | `/settings` | Update settings (partial — only sends fields being changed) |
|
|
806
884
|
|
|
807
885
|
```ts
|
|
808
|
-
// GET /consenti/admin/settings
|
|
886
|
+
// GET /consenti/admin/v1/settings
|
|
809
887
|
{ "allowedOrigins": ["https://example.com"], "adminAllowedOrigins": ["https://dashboard.example.com"] }
|
|
810
888
|
|
|
811
|
-
// PATCH /consenti/admin/settings
|
|
889
|
+
// PATCH /consenti/admin/v1/settings
|
|
812
890
|
{ "allowedOrigins": ["https://example.com", "https://foo.example.com"] }
|
|
813
891
|
```
|
|
814
892
|
|
|
@@ -818,7 +896,7 @@ when the specific profile being submitted to doesn't set its own `profileJson.al
|
|
|
818
896
|
is its only access gate.
|
|
819
897
|
|
|
820
898
|
**`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`
|
|
899
|
+
browser-originated `/consenti/admin/v1/*` requests (server-to-server callers without an `Origin`
|
|
822
900
|
header are unaffected). Unauthenticated static assets (`widget.js`/`widget.css`) are exempt —
|
|
823
901
|
they must stay embeddable from any origin. **Be careful**: if you configure this, include the
|
|
824
902
|
dashboard's own origin or you'll lock yourself out of the dashboard along with everyone else.
|
|
@@ -872,15 +950,37 @@ Only active when `multiTenant.enabled: true`.
|
|
|
872
950
|
|
|
873
951
|
### IAB TCF
|
|
874
952
|
|
|
875
|
-
| Method | Path
|
|
876
|
-
|
|
877
|
-
| `GET` | `/tcf/vendors`
|
|
878
|
-
| `GET` | `/tcf/purposes`
|
|
953
|
+
| Method | Path | Description |
|
|
954
|
+
|--------|-----------------------------|----------------------------|
|
|
955
|
+
| `GET` | `/tcf/vendors` | List IAB TCF vendors (paginated, `?q=` name filter) |
|
|
956
|
+
| `GET` | `/tcf/purposes` | List IAB TCF purposes |
|
|
957
|
+
| `GET` | `/tcf/registration-status` | Draft/confirmed diff plus a live IAB CMP-List lookup for `cmpId` (`?refresh=true` bypasses the 7-day cache) — powers the dashboard's TCF Registration panel |
|
|
958
|
+
| `POST` | `/tcf/confirm-registration` | `{ acknowledge: true }` — records the confirmation as a hash of `cmpId`/`cmpVersion`/`publisherCC` (never the raw values); `409` if IAB's CMP List shows `cmpId` deregistered, `404` if not found yet |
|
|
879
959
|
|
|
880
960
|
Only active when `tcf.enabled: true`. `cmpId`/`cmpVersion` here must match the widget's own
|
|
881
|
-
`compliance.tcf` config (`@consenti/ui` README, "TCF" section)
|
|
882
|
-
|
|
883
|
-
|
|
961
|
+
`compliance.tcf` config (`@consenti/ui` README, "TCF" section). The widget installs
|
|
962
|
+
`window.__tcfapi` itself; these admin-only routes are for the dashboard's vendor/purpose pickers
|
|
963
|
+
and registration-status panel, not consumed by the public widget.
|
|
964
|
+
|
|
965
|
+
Set `tcf.publisherCC` (your ISO 3166-1 alpha-2 publisher country code) and install the optional
|
|
966
|
+
`@iabtechlabtcf/core` peer dependency to get spec-correct binary TC-string encoding on consent
|
|
967
|
+
records; without both, `tcfString` falls back to the simplified base64url-JSON format the widget
|
|
968
|
+
also uses. See the [TCF & GPP Registration Guide](https://consenti.dev/docs/compliance/tcf-and-gpp-registration) for details.
|
|
969
|
+
|
|
970
|
+
### IAB GPP (US National)
|
|
971
|
+
|
|
972
|
+
| Method | Path | Description |
|
|
973
|
+
|--------|------------------------------|----------------------------|
|
|
974
|
+
| `GET` | `/gpp/registration-status` | Draft/confirmed diff for `cmpId`/`cmpVersion` — powers the dashboard's GPP Registration panel |
|
|
975
|
+
| `POST` | `/gpp/confirm-registration` | `{ acknowledge: true }` — records the confirmation as a hash of `cmpId`/`cmpVersion` |
|
|
976
|
+
|
|
977
|
+
Only active when `gpp.enabled: true`. Unlike TCF, IAB publishes no CMP-List equivalent for GPP, so
|
|
978
|
+
confirmation here is self-attestation only (no external registry to validate `cmpId` against) — the
|
|
979
|
+
checkbox and hash-drift detection (re-arms when `cmpId`/`cmpVersion` changes) are the whole
|
|
980
|
+
mechanism. `cmpId`/`cmpVersion` must match the widget's own `compliance.gpp` config
|
|
981
|
+
(`@consenti/ui` README, "GPP" section). Install the optional `@iabgpp/cmpapi` peer dependency for
|
|
982
|
+
spec-correct binary GPP-string encoding; without it, `gppString` falls back to a base64url-JSON
|
|
983
|
+
payload, same fallback shape as the widget's own stub.
|
|
884
984
|
|
|
885
985
|
---
|
|
886
986
|
|
|
@@ -902,7 +1002,7 @@ Served at `{basePath}/` when `dashboard: true`.
|
|
|
902
1002
|
| Roles | RBAC roles and fine-grained permission assignment |
|
|
903
1003
|
| Sites | Multi-tenant site management **(superadmin only)** |
|
|
904
1004
|
| TCF Vendors | IAB Global Vendor List |
|
|
905
|
-
| Audit Log |
|
|
1005
|
+
| Audit Log | Append-only log of all admin actions, never deleted by Consenti; search, paginate; row click opens a detail modal with old/new data diff |
|
|
906
1006
|
| Settings / API | API keys, branding, OpenAPI docs **(superadmin only)** |
|
|
907
1007
|
| Setup Wizard | One-time first-run welcome / config / default-profiles / confirmation flow — see [First-Run Setup Wizard](#first-run-setup-wizard) |
|
|
908
1008
|
|
|
@@ -1060,8 +1160,8 @@ Only one profile per `complianceGroup` can be active at a time. Activating a new
|
|
|
1060
1160
|
|
|
1061
1161
|
Via API:
|
|
1062
1162
|
```http
|
|
1063
|
-
GET /consenti/admin/profiles/:id/versions
|
|
1064
|
-
GET /consenti/admin/profiles/:id/versions/:entryId?locale=fr-FR
|
|
1163
|
+
GET /consenti/admin/v1/profiles/:id/versions
|
|
1164
|
+
GET /consenti/admin/v1/profiles/:id/versions/:entryId?locale=fr-FR
|
|
1065
1165
|
```
|
|
1066
1166
|
|
|
1067
1167
|
`:id` is the profile's stable id. `:entryId` is one specific version's number, as returned by the `versions` list.
|
|
@@ -1071,7 +1171,7 @@ GET /consenti/admin/profiles/:id/versions/:entryId?locale=fr-FR
|
|
|
1071
1171
|
`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
1172
|
|
|
1073
1173
|
```http
|
|
1074
|
-
GET /consenti/admin/profiles/archived
|
|
1174
|
+
GET /consenti/admin/v1/profiles/archived
|
|
1075
1175
|
```
|
|
1076
1176
|
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
1177
|
|
|
@@ -1092,27 +1192,35 @@ When **changing parameters or categories** on a Consent Template, the backend re
|
|
|
1092
1192
|
| Event | Payload | Description |
|
|
1093
1193
|
|---|---|---|
|
|
1094
1194
|
| `ready` | none | Storage connected and admin user bootstrapped |
|
|
1095
|
-
| `consent
|
|
1096
|
-
| `consent
|
|
1097
|
-
| `consent
|
|
1098
|
-
| `visitor
|
|
1099
|
-
| `profile
|
|
1100
|
-
| `profile
|
|
1101
|
-
| `profile
|
|
1195
|
+
| `consent:created` | `ConsentDbRecord` | New consent record saved |
|
|
1196
|
+
| `consent:updated` | `{ previous, current: ConsentDbRecord }` | Consent record updated |
|
|
1197
|
+
| `consent:erased` | `{ visitorId: string }` | Visitor data erased (GDPR erasure) |
|
|
1198
|
+
| `visitor:created` | `Visitor` | New visitor record created |
|
|
1199
|
+
| `profile:created` | `Profile` | Profile created |
|
|
1200
|
+
| `profile:updated` | `{ previous, current: Profile }` | Profile edited — `current.id` matches `previous.id`; `current.version` is incremented |
|
|
1201
|
+
| `profile:deleted` | `{ id: string, previous: Profile }` | Profile deleted |
|
|
1202
|
+
| `profile.activated` | `Profile` | Profile activated (locale JSON files written to the hot-serve path) |
|
|
1203
|
+
| `profile.deactivated` | `Profile` | Profile deactivated (locale JSON files removed from the hot-serve path) |
|
|
1102
1204
|
| `cache:warm` | `{ paths: string[], profileId: string }` | Fired after locale JSON files are written; paths to warm in CDN/cache |
|
|
1103
1205
|
| `cache:purge` | `{ paths: string[], profileId: string }` | Fired after locale JSON files are removed; paths to purge from CDN/cache |
|
|
1206
|
+
| `consent.parentalConsentRequired` | `{ visitorId, profileId, token, issuedAt }` | Age gate declined with `requireParentalConsent: true` — see [Parental consent request/resolve](#parental-consent-request-resolve-stateless-hook) below |
|
|
1207
|
+
| `consent.parentalConsentGranted` | `{ visitorId, profileId }` | `POST /consent/parental-consent-resolve` succeeded — see the same section |
|
|
1208
|
+
|
|
1209
|
+
Note the inconsistent separator: most events use `:` (`profile:created`), but the activation and
|
|
1210
|
+
parental-consent events use `.` (`profile.activated`, `consent.parentalConsentRequired`) — listen
|
|
1211
|
+
for the exact string shown above, not a normalized form.
|
|
1104
1212
|
|
|
1105
1213
|
`cache:warm` and `cache:purge` mirror the `handleCache` config callback — use whichever suits your integration.
|
|
1106
1214
|
|
|
1107
1215
|
```ts
|
|
1108
1216
|
const { eventBus } = createConsenti({ /* ... */ })
|
|
1109
1217
|
|
|
1110
|
-
eventBus.on('consent
|
|
1218
|
+
eventBus.on('consent:created', (record) => {
|
|
1111
1219
|
// send to analytics, trigger webhook, etc.
|
|
1112
1220
|
console.log('New consent from visitor:', record.visitorId)
|
|
1113
1221
|
})
|
|
1114
1222
|
|
|
1115
|
-
eventBus.on('consent
|
|
1223
|
+
eventBus.on('consent:erased', ({ visitorId }) => {
|
|
1116
1224
|
myDmpClient.deleteVisitor(visitorId)
|
|
1117
1225
|
})
|
|
1118
1226
|
```
|
|
@@ -1121,7 +1229,7 @@ eventBus.on('consent.erased', ({ visitorId }) => {
|
|
|
1121
1229
|
|
|
1122
1230
|
## ConsentAction / CategoryAction
|
|
1123
1231
|
|
|
1124
|
-
Server-side counterparts to `@consenti/ui`'s `ConsentAction`/`CategoryAction` — thin wrappers around `consent
|
|
1232
|
+
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
1233
|
|
|
1126
1234
|
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
1235
|
|
|
@@ -1255,22 +1363,25 @@ await consenti.destroy()
|
|
|
1255
1363
|
|
|
1256
1364
|
| Concern | Approach |
|
|
1257
1365
|
|---------------------------|------------------------------------------------------------------------------|
|
|
1258
|
-
| IP addresses | Never stored raw — SHA-256 hashed
|
|
1366
|
+
| IP addresses | Never stored raw — masked (last IPv4 octet / last 80 bits of IPv6 zeroed), salted with `compliance.dataSigningHash`, then SHA-256 hashed |
|
|
1259
1367
|
| Passwords | `scrypt` via `node:crypto` — no external bcrypt |
|
|
1260
1368
|
| JWTs | `createHmac` via `node:crypto` — no jsonwebtoken package |
|
|
1261
1369
|
| Admin routes | Always require `Authorization: Bearer <jwt>` |
|
|
1262
1370
|
| Stack traces | Suppressed in `NODE_ENV=production` responses |
|
|
1263
1371
|
| CORS | Explicit allowlist — never use `origins: '*'` in production |
|
|
1372
|
+
| Tamper-evident consent | `compliance.dataSigningHash` signs server-stored records — the key never reaches the browser, unlike the widget's own `core.cookieSigningKey`, which is spoofable in standalone (no-backend) mode since it must ship inside the browser bundle |
|
|
1264
1373
|
| Rate limiting | Applied to all public API routes by default |
|
|
1265
|
-
| Signed consent records |
|
|
1374
|
+
| Signed consent records | `compliance.dataSigningHash` — HMAC-SHA256 (`node:crypto` `createHmac`) over each record's core fields, hex-encoded into `signature`. Auto-generated in memory if you don't set it, so this is always active. |
|
|
1266
1375
|
|
|
1267
|
-
### Signed consent records
|
|
1376
|
+
### Signed consent records
|
|
1268
1377
|
|
|
1269
|
-
|
|
1378
|
+
`compliance.dataSigningHash` signs every consent record 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). The same key also signs ownership cookies and parental-consent request/resolve tokens (see "Parental consent" below), and salts hashed IPs. It's auto-generated in memory if you don't set it — every restart then invalidates outstanding ownership cookies and in-flight parental-consent tokens, since the generated value isn't persisted. Set it explicitly (`CONSENTI_DATA_SIGNING_HASH`) for anything beyond local dev.
|
|
1270
1379
|
|
|
1271
1380
|
```ts
|
|
1272
1381
|
createConsenti({
|
|
1273
|
-
|
|
1382
|
+
compliance: {
|
|
1383
|
+
dataSigningHash: process.env.CONSENTI_DATA_SIGNING_HASH,
|
|
1384
|
+
},
|
|
1274
1385
|
})
|
|
1275
1386
|
```
|
|
1276
1387
|
|
|
@@ -1279,6 +1390,32 @@ createConsenti({
|
|
|
1279
1390
|
- `signature` is included in CSV/JSON/XLSX exports (`GET /export/consents`) and shown in the dashboard's Consents detail view.
|
|
1280
1391
|
- Hash-chaining between records (a full tamper-evident log, not just per-record integrity) is not implemented — out of scope for now.
|
|
1281
1392
|
|
|
1393
|
+
### Parental consent request/resolve (stateless hook)
|
|
1394
|
+
|
|
1395
|
+
When a profile's `ageGate.requireParentalConsent` is true and a visitor declines the age gate, the widget submits a deny-all consent and calls `POST /consent/:visitorId/parental-consent-request` to mint a token, then dispatches `consenti:parentalConsentRequired` carrying it. There's no email-sending infra in this package — your own `eventBus` listener owns actually notifying the parent (email, SMS, whatever your product uses):
|
|
1396
|
+
|
|
1397
|
+
```ts
|
|
1398
|
+
const { eventBus } = createConsenti({ /* ... */ })
|
|
1399
|
+
eventBus.on('consent.parentalConsentRequired', ({ visitorId, profileId, token }) => {
|
|
1400
|
+
const resolveUrl = `https://your-site.example/verify-parent?token=${encodeURIComponent(token)}`
|
|
1401
|
+
// send resolveUrl to the parent however your product does that (email, SMS, ...)
|
|
1402
|
+
})
|
|
1403
|
+
```
|
|
1404
|
+
|
|
1405
|
+
The parent's own click/flow eventually calls `POST /consent/parental-consent-resolve` with `{ token }` — either directly, or via the widget's `resolveParentalConsent(token)` export (`@consenti/ui`) if the parent lands on a page with the widget script loaded. On success the server emits `consent.parentalConsentGranted`; your backend listener decides what "granted" actually means for stored consent (this package doesn't update it automatically):
|
|
1406
|
+
|
|
1407
|
+
```ts
|
|
1408
|
+
eventBus.on('consent.parentalConsentGranted', async ({ visitorId, profileId }) => {
|
|
1409
|
+
// e.g. call ConsentService/PUT /consent/:visitorId yourself to update the real consent record
|
|
1410
|
+
})
|
|
1411
|
+
```
|
|
1412
|
+
|
|
1413
|
+
**Signing**: the request/resolve tokens are signed with `compliance.dataSigningHash` — the same key used for consent-record signing above. It's auto-generated in memory if unset, so tokens are always signed — but with an ephemeral, unpersisted key unless you set `dataSigningHash` explicitly, meaning a server restart between issuing and resolving a token invalidates it. Set `dataSigningHash` (`CONSENTI_DATA_SIGNING_HASH`) before using this in production. Token expiry defaults to 7 days, configurable via `compliance.parentalConsentTokenTtlDays`.
|
|
1414
|
+
|
|
1415
|
+
**Important caveats**:
|
|
1416
|
+
- Token **replay isn't prevented** — there's no persistence to mark a token "used," an accepted tradeoff of staying fully stateless (no new DB entity, no `StorageAdapter` changes across all 7 storage adapters for what's meant to be a thin plumbing hook).
|
|
1417
|
+
- This mechanism is **not by itself sufficient verifiable parental consent** under COPPA or similar regimes. Mainstream CMPs (OneTrust, Cookiebot, Usercentrics, TrustArc, Osano) don't build verifiable-parental-consent workflows into the widget either — a CMP has no way to actually verify the clicker is the parent. This is plumbing to let you wire up your own verification method (signed form, ID check, "email plus," video call — whatever your legal counsel requires), not a claim that clicking a link constitutes legally-sufficient VPC.
|
|
1418
|
+
|
|
1282
1419
|
---
|
|
1283
1420
|
|
|
1284
1421
|
## Environment Variables
|
|
@@ -1291,7 +1428,8 @@ All config fields can be set via environment variables. Code config takes preced
|
|
|
1291
1428
|
| `CONSENTI_BASE_PATH` | `basePath` | `'/consenti'` |
|
|
1292
1429
|
| `CONSENTI_ADMIN_EMAIL` | `auth.adminEmail` | `'user@consenti.dev'` |
|
|
1293
1430
|
| `CONSENTI_ADMIN_PASSWORD` | `auth.adminPassword` | `'Consenti@123'` |
|
|
1294
|
-
| `
|
|
1431
|
+
| `CONSENTI_ADMIN_MASTER_SECRET` | `auth.masterSecret` | random per restart |
|
|
1432
|
+
| `CONSENTI_DATA_SIGNING_HASH` | `compliance.dataSigningHash` | random per restart |
|
|
1295
1433
|
| `CONSENTI_DB_DRIVER` | `storage.driver` | `'json'` |
|
|
1296
1434
|
| `CONSENTI_DB_PATH` | `storage.path` | `'./consenti-data'` |
|
|
1297
1435
|
| `CONSENTI_DB_URI` | `storage.uri` | — |
|
|
@@ -1317,7 +1455,7 @@ OpenAPI JSON at `{basePath}/api/openapi.json`.
|
|
|
1317
1455
|
## Demo
|
|
1318
1456
|
For detailed API docs, Guides and Demo, visit [consenti.dev](https://consenti.dev)
|
|
1319
1457
|
|
|
1320
|
-
|
|
1458
|
+
An open-source project by [Santosh Ojha](https://santosh.top)
|
|
1321
1459
|
|
|
1322
1460
|
|
|
1323
1461
|
---
|