@consenti/api 0.3.0 → 0.4.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.
Files changed (113) hide show
  1. package/README.md +212 -74
  2. package/dist/chunk-COR72NFQ.js +2 -0
  3. package/dist/chunk-COR72NFQ.js.map +1 -0
  4. package/dist/chunk-S2IEYESR.js +2 -0
  5. package/dist/chunk-S2IEYESR.js.map +1 -0
  6. package/dist/dashboard/assets/ApiConfig-CZMtYRfK.js +1 -0
  7. package/dist/dashboard/assets/{ArchivedProfileList-Bw-fx-BD.js → ArchivedProfileList-BlQ2gEWz.js} +1 -1
  8. package/dist/dashboard/assets/AuditLog-BpZoAFOe.js +1 -0
  9. package/dist/dashboard/assets/{ChangePassword-D2rPkzD1.js → ChangePassword-BZkP2dXQ.js} +1 -1
  10. package/dist/dashboard/assets/{ConfirmDialog-DTtw7VJ-.js → ConfirmDialog-BDlnfTdx.js} +1 -1
  11. package/dist/dashboard/assets/ConsentList-DgJMrhih.js +1 -0
  12. package/dist/dashboard/assets/ConsentTemplateEditor-DA7B5aI_.js +1 -0
  13. package/dist/dashboard/assets/ConsentTemplateList-DMPd2y5s.js +1 -0
  14. package/dist/dashboard/assets/HeadingTagInput-D2S9xDMJ.js +1 -0
  15. package/dist/dashboard/assets/{HowItWorks-AeyjfQGO.js → HowItWorks-CJCrdr3o.js} +1 -1
  16. package/dist/dashboard/assets/PermissionGate-C69dhwV1.js +1 -0
  17. package/dist/dashboard/assets/PreviewPane-D7mDeBT8.js +38 -0
  18. package/dist/dashboard/assets/ProfileEditor-CSFdOweN.js +2 -0
  19. package/dist/dashboard/assets/ProfileList-C4cIDrM6.js +1 -0
  20. package/dist/dashboard/assets/{ProfileVersionHistory-1h8sUgog.js → ProfileVersionHistory-CX5-Oupy.js} +2 -2
  21. package/dist/dashboard/assets/{RecordDetailModal-FaFhzQXi.js → RecordDetailModal-DIpGxfqA.js} +1 -1
  22. package/dist/dashboard/assets/Reports-B1Ww0ovT.js +1 -0
  23. package/dist/dashboard/assets/RoleList-CQ1GjR0W.js +1 -0
  24. package/dist/dashboard/assets/{Select-DjJNwlbY.js → Select-C_JusbT5.js} +1 -1
  25. package/dist/dashboard/assets/Settings-sIMVWQ0V.js +1 -0
  26. package/dist/dashboard/assets/SetupWizard-ChFBxqo3.js +1 -0
  27. package/dist/dashboard/assets/Table-BUxoz0YR.js +1 -0
  28. package/dist/dashboard/assets/TenantList-DCsT7ghb.js +1 -0
  29. package/dist/dashboard/assets/UITemplateEditor-BAaMnuOU.js +2 -0
  30. package/dist/dashboard/assets/UITemplateList-C3UMbnfs.js +1 -0
  31. package/dist/dashboard/assets/UserList-CHGNnS50.js +1 -0
  32. package/dist/dashboard/assets/VendorList-C-sswM1A.js +1 -0
  33. package/dist/dashboard/assets/VisitorList-DNB9Ia72.js +1 -0
  34. package/dist/dashboard/assets/check-D0g4XvmN.js +1 -0
  35. package/dist/dashboard/assets/client-CYntrO2g.js +1 -0
  36. package/dist/dashboard/assets/{copy-C01KoUUH.js → copy-DEi_W2TM.js} +1 -1
  37. package/dist/dashboard/assets/createLucideIcon-DAiYBrnd.js +1 -0
  38. package/dist/dashboard/assets/de-jwngydrC.js +25 -0
  39. package/dist/dashboard/assets/dist-GoiMJCTt.js +1 -0
  40. package/dist/dashboard/assets/es-B6Jp0OPe.js +25 -0
  41. package/dist/dashboard/assets/{external-link-DVe00ZBK.js → external-link-CFZxxe3W.js} +1 -1
  42. package/dist/dashboard/assets/fr-CshYcQPW.js +25 -0
  43. package/dist/dashboard/assets/index-Cr5M1Qy_.css +2 -0
  44. package/dist/dashboard/assets/index-Tr_BcjkN.js +26 -0
  45. package/dist/dashboard/assets/ja-CgjZNBrc.js +25 -0
  46. package/dist/dashboard/assets/{profiles-DmrdGv7t.js → profiles-C_SuoLJD.js} +1 -1
  47. package/dist/dashboard/assets/{roles-C0dJPFl4.js → roles-D7L936FV.js} +1 -1
  48. package/dist/dashboard/assets/rotate-ccw-clock-6y2NDrJ7.js +1 -0
  49. package/dist/dashboard/assets/search-DKwfLBOv.js +1 -0
  50. package/dist/dashboard/assets/templates-CIyMjskA.js +1 -0
  51. package/dist/dashboard/assets/{templates-B2xiivh4.js → templates-DY68xNys.js} +1 -1
  52. package/dist/dashboard/assets/{trash-2-BBnkdgm_.js → trash-2-eX9s6x8V.js} +1 -1
  53. package/dist/dashboard/assets/{trash-Dj1jhzHJ.js → trash-O30ycMcz.js} +1 -1
  54. package/dist/dashboard/assets/{useHashQuery-B5ytMIMp.js → useHashQuery-BDjUPXDR.js} +1 -1
  55. package/dist/dashboard/assets/{users-czK8LRVh.js → users-BRrNOK0W.js} +1 -1
  56. package/dist/dashboard/assets/x-BhZiH8AL.js +1 -0
  57. package/dist/dashboard/assets/zh-CN-BnLZkfBC.js +25 -0
  58. package/dist/dashboard/index.html +4 -4
  59. package/dist/dist-QJYW5KUK.js +2 -0
  60. package/dist/index.cjs +68 -68
  61. package/dist/index.cjs.map +1 -1
  62. package/dist/index.d.cts +214 -52
  63. package/dist/index.d.ts +214 -52
  64. package/dist/index.js +65 -65
  65. package/dist/index.js.map +1 -1
  66. package/dist/profiles-FCX454HS.js +2 -0
  67. package/dist/widget/widget.css +1 -1
  68. package/dist/widget/widget.js +1 -1
  69. package/package.json +24 -9
  70. package/dist/chunk-IY4L6MNB.js +0 -2
  71. package/dist/chunk-IY4L6MNB.js.map +0 -1
  72. package/dist/chunk-O5UG4OYI.js +0 -2
  73. package/dist/chunk-O5UG4OYI.js.map +0 -1
  74. package/dist/dashboard/assets/ApiConfig-CE-Noelz.js +0 -1
  75. package/dist/dashboard/assets/AuditLog-D6HiTQ1o.js +0 -1
  76. package/dist/dashboard/assets/ConsentList-BWOvPOMG.js +0 -1
  77. package/dist/dashboard/assets/ConsentTemplateEditor-BKIEnLmA.js +0 -1
  78. package/dist/dashboard/assets/ConsentTemplateList-DCys2nM3.js +0 -1
  79. package/dist/dashboard/assets/HeadingTagInput-BTjrdzlS.js +0 -1
  80. package/dist/dashboard/assets/PermissionGate-Bhhf7SMh.js +0 -1
  81. package/dist/dashboard/assets/PreviewPane-Dq5K1h2L.js +0 -35
  82. package/dist/dashboard/assets/ProfileEditor-BXtaE0tL.js +0 -2
  83. package/dist/dashboard/assets/ProfileList-CWKSAgu0.js +0 -1
  84. package/dist/dashboard/assets/Reports-DWEjQOKh.js +0 -1
  85. package/dist/dashboard/assets/RoleList-BSGoqlzp.js +0 -1
  86. package/dist/dashboard/assets/Settings-CVYrlyLm.js +0 -1
  87. package/dist/dashboard/assets/SetupWizard-4NTb5bZj.js +0 -1
  88. package/dist/dashboard/assets/Table-DH_jh8_N.js +0 -1
  89. package/dist/dashboard/assets/TenantList-5AwFtoHf.js +0 -1
  90. package/dist/dashboard/assets/UITemplateEditor-9A55scBe.js +0 -1
  91. package/dist/dashboard/assets/UITemplateList-BS7X4u4F.js +0 -1
  92. package/dist/dashboard/assets/UserList-SPkMyFRy.js +0 -1
  93. package/dist/dashboard/assets/VendorList-C5fPhr6U.js +0 -1
  94. package/dist/dashboard/assets/VisitorList-COP1SyNd.js +0 -1
  95. package/dist/dashboard/assets/check-o5cp1s2J.js +0 -1
  96. package/dist/dashboard/assets/client-ymc9nwWr.js +0 -1
  97. package/dist/dashboard/assets/createLucideIcon-MdjI3m3u.js +0 -1
  98. package/dist/dashboard/assets/de-DET92Jb5.js +0 -5
  99. package/dist/dashboard/assets/dist-DlhLroYC.js +0 -1
  100. package/dist/dashboard/assets/es-CJNunrnU.js +0 -5
  101. package/dist/dashboard/assets/fr-akfTuf5x.js +0 -5
  102. package/dist/dashboard/assets/history-CHTts_m2.js +0 -1
  103. package/dist/dashboard/assets/index-DW3qOrry.css +0 -2
  104. package/dist/dashboard/assets/index-H_Zm0X-u.js +0 -6
  105. package/dist/dashboard/assets/ja-B0nLWkOd.js +0 -5
  106. package/dist/dashboard/assets/search-Bd2RdDRh.js +0 -1
  107. package/dist/dashboard/assets/templates-CnMoLavN.js +0 -1
  108. package/dist/dashboard/assets/x-BRuyBEQP.js +0 -1
  109. package/dist/dashboard/assets/zh-CN-Dxg3G-BL.js +0 -5
  110. package/dist/dist-Z5RVTVBT.js +0 -2
  111. package/dist/profiles-XTYPWLVC.js +0 -2
  112. /package/dist/{dist-Z5RVTVBT.js.map → dist-QJYW5KUK.js.map} +0 -0
  113. /package/dist/{profiles-XTYPWLVC.js.map → profiles-FCX454HS.js.map} +0 -0
package/dist/index.d.ts CHANGED
@@ -13,6 +13,32 @@ type DeepPartial<T> = T extends Function ? T : T extends Array<infer InferredArr
13
13
  [Key in keyof T]?: DeepPartial<T[Key]> | null;
14
14
  } : T;
15
15
 
16
+ /**
17
+ * Single source of truth for all compliance groups, regulations, and country mappings.
18
+ *
19
+ * This is DATA, not types. The `Compliance` and `ComplianceGroupId` literal types
20
+ * derived from the ID arrays below live in @consenti/types (packages/types/src/compliance.ts),
21
+ * via a type-only import — that keeps @consenti/types free of runtime code.
22
+ *
23
+ * ─── HOW TO UPDATE ────────────────────────────────────────────────────────────
24
+ *
25
+ * ADD A NEW REGULATION:
26
+ * 1. Add regulation ID string to `COMPLIANCE_REGULATION_IDS` below
27
+ * (the `Compliance` type in @consenti/types auto-updates from it).
28
+ * 2. Add it to the appropriate group's `compliances` array below.
29
+ * 3. Add / update the country entry in `EMBEDDED_COMPLIANCE_MAP` below.
30
+ * That is all — no other files need touching.
31
+ *
32
+ * ADD A NEW COMPLIANCE GROUP:
33
+ * 1. Add the kebab-case ID to `COMPLIANCE_GROUP_IDS` below.
34
+ * 2. Add a full group entry to `COMPLIANCE_GROUPS` below (label, compliances, flags).
35
+ * 3. Add a validation rule set to `COMPLIANCE_VALIDATION_RULES` below.
36
+ * 4. Map affected countries in `EMBEDDED_COMPLIANCE_MAP` to the new group.
37
+ *
38
+ * ADD OR UPDATE A COUNTRY:
39
+ * Edit the entry in `EMBEDDED_COMPLIANCE_MAP.countries` below. That is the ONLY
40
+ * place that needs changing for country-level jurisdiction updates.
41
+ */
16
42
  declare const COMPLIANCE_GROUP_IDS: readonly ["opt-in", "opt-out", "opt-out-strict", "opt-in-dpdpa", "opt-in-china", "opt-in-brazil", "general-privacy-consent", "notice-only"];
17
43
  declare const COOKIE_PURPOSE_IDS: readonly ["necessary", "functional", "preferences", "analytics", "marketing"];
18
44
 
@@ -42,6 +68,8 @@ interface LocaleContentInput {
42
68
  mainBanner: MainBanner;
43
69
  gpcBanner?: GpcBanner;
44
70
  preferenceModal: PreferenceModal;
71
+ /** Only meaningful when the profile's `ageGate.enabled` is true. */
72
+ ageGateModal?: AgeGateModalContent;
45
73
  }
46
74
  interface StorageConfig {
47
75
  driver: 'sqlite' | 'better-sqlite3' | 'node-sqlite3-wasm' | 'node:sqlite' | 'mongodb' | 'mysql' | 'postgresql' | 'json';
@@ -85,7 +113,7 @@ interface SamlConfig {
85
113
  }
86
114
  interface AuthConfig {
87
115
  mode: 'local' | 'jwt' | 'custom' | 'oidc' | 'saml';
88
- jwtSecret?: string;
116
+ masterSecret?: string;
89
117
  validateUser?: (req: unknown) => Promise<AdminUser | null>;
90
118
  adminEmail?: string;
91
119
  adminPassword?: string;
@@ -101,6 +129,14 @@ type GeoResult = {
101
129
  country: string | null;
102
130
  region: string | null;
103
131
  locale: string | null;
132
+ /** Optional escape hatch: when set, `GeoResolverService` uses this compliance group directly
133
+ * instead of computing one from `country`/`region` against the jurisdiction map — for a
134
+ * provider that already carries legal-grade jurisdiction data (e.g. a paid geo-IP service) and
135
+ * needs to route a visitor into an operator-defined custom group the built-in map can't express.
136
+ * `country`/`region`/`locale` are still used for locale resolution and the `requiresSensitiveOptIn`
137
+ * region carve-out, which are independent of which group applies. Optional — omitting it (or
138
+ * returning `undefined`) preserves the existing country/region-map resolution exactly as before. */
139
+ complianceGroup?: string;
104
140
  };
105
141
  type CountryResolverFn = (ctx: {
106
142
  ip: string;
@@ -178,31 +214,49 @@ interface OptInStats {
178
214
  byLocale: Record<string, OptInByLocale>;
179
215
  byDate: OptInByDate[];
180
216
  }
181
- type ComplianceMapOverride = 'default' | ComplianceMapData | 'auto';
182
217
  interface ComplianceMapData {
183
218
  version: string;
184
219
  countries: Record<string, CountryComplianceEntry>;
185
220
  }
186
221
  interface CountryComplianceEntry {
187
222
  complianceGroup: ComplianceGroupId;
188
- default: ComplianceGroupId;
189
- description: string;
223
+ /** Fallback group used when only the country (not the region) is known. Defaults to
224
+ * `complianceGroup` when omitted — only set this explicitly when a region within this
225
+ * country can be stricter than the country-wide baseline (see `overriddenRegions`). */
226
+ default?: ComplianceGroupId;
227
+ description?: string;
190
228
  overriddenRegions?: Record<string, {
191
229
  complianceGroup: ComplianceGroupId;
192
- description: string;
230
+ description?: string;
193
231
  }>;
194
232
  }
195
233
  interface ComplianceConfig {
196
- /** @deprecated use type */
197
- gdpr?: boolean;
198
- /** @deprecated use type */
199
- ccpa?: boolean;
234
+ /** Server-side GPC enforcement on `POST`/`PUT /consent`: re-applies GPC-driven denial when a
235
+ * write carries `gpcDetected`, independent of the widget's own client-side `gpcMode` handling.
236
+ * `false` disables this defense-in-depth check entirely. Default: enabled. */
200
237
  gpc?: boolean | 'strict';
201
238
  type?: ComplianceGroupId | 'auto';
202
- /** @deprecated use 'default' instead of 'language' or 'timezone'; 'geoip' and 'hosted-geoip-lite' removed */
203
- geoDataProvider?: 'default' | 'maxmind' | 'language' | 'timezone' | 'geoip' | 'hosted-geoip-lite' | CountryResolverFn;
204
- autoComplianceMap?: ComplianceMapOverride;
205
- complianceMapUrl?: string;
239
+ geoDataProvider?: 'default' | 'maxmind' | 'geoip' | 'hosted-geoip-lite' | CountryResolverFn;
240
+ /** `'default'` (embedded map, the default), a URL to fetch a JSON `ComplianceMapData` document
241
+ * from (refreshed in the background per the response's `Cache-Control`/`Expires` header, 24h
242
+ * fallback if absent), or an inline operator-supplied `ComplianceMapData` object. Invalid data
243
+ * from either a URL or an object logs an error and falls back to `'default'`. */
244
+ complianceMap?: 'default' | string | ComplianceMapData;
245
+ tcf?: TcfConfig;
246
+ gpp?: GppConfig;
247
+ /** Signs server-stored consent records (HMAC-SHA256 at create/update time, hex-encoded into
248
+ * `signature`, checked on `GET /consent/:visitorId/verify`), ownership cookies, and
249
+ * parental-consent request/resolve tokens (see `POST /consent/:visitorId/parental-consent-request`),
250
+ * and salts hashed IPs — independent of the widget's own cookie-signing
251
+ * (`core.cookieSigningKey`). Auto-generated in memory when unset, so all of the above are
252
+ * always active — but an auto-generated value isn't persisted, so a restart invalidates
253
+ * outstanding ownership cookies, in-flight parental-consent tokens, and (if compared across
254
+ * restarts) hashed-IP continuity. Set this explicitly for anything beyond local dev. */
255
+ dataSigningHash?: string;
256
+ dataRetention?: DataRetentionConfig;
257
+ /** Expiry window for parental-consent tokens issued by `POST /consent/:visitorId/parental-consent-request`.
258
+ * Default: 7 days. */
259
+ parentalConsentTokenTtlDays?: number;
206
260
  }
207
261
  interface DashboardConfig {
208
262
  enabled?: boolean;
@@ -211,23 +265,35 @@ interface DashboardConfig {
211
265
  interface MultiTenantConfig {
212
266
  enabled: boolean;
213
267
  }
214
- interface AgeGateConfig {
215
- enabled: boolean;
216
- minimumAge: number;
217
- requireParentalConsent?: boolean;
218
- }
219
268
  interface TcfConfig {
220
269
  enabled: boolean;
221
270
  cmpId: number;
222
271
  cmpVersion: number;
272
+ /** ISO 3166-1 alpha-2 country code of the publisher (e.g. 'DE', 'FR'). Required for
273
+ * spec-correct binary TC-string encoding — without it, real encoding is skipped and
274
+ * Consenti falls back to the simplified (non-spec) TC string. No default: there is no
275
+ * honest country to assume on a publisher's behalf. */
276
+ publisherCC?: string;
277
+ }
278
+ /** IAB Global Privacy Platform (GPP), US National ("usnat") section only — the umbrella section
279
+ * most MSPA/programmatic US bidders read. Individual state sections (US-CA, US-CO, etc.) aren't
280
+ * encoded yet. Requires the optional `@iabgpp/cmpapi` peer dependency (see `gpp/gpp-string.ts`) —
281
+ * unlike TCF there's no simplified fallback format, since a non-spec GPP string has no consumer
282
+ * that would accept it: when the dependency isn't installed, `gppString`/`window.__gpp` are
283
+ * simply not produced. */
284
+ interface GppConfig {
285
+ enabled: boolean;
286
+ cmpId: number;
287
+ cmpVersion: number;
288
+ /** MSPA "covered transaction" — whether this deployment's data transactions fall under MSPA
289
+ * signatory obligations. No honest default, so it's required rather than optional. */
290
+ mspaCoveredTransaction: boolean;
291
+ /** IAB's tri-state encoding for both MSPA fields: 0 = not applicable, 1 = yes, 2 = no. */
292
+ mspaOptOutOptionMode: 0 | 1 | 2;
293
+ mspaServiceProviderMode: 0 | 1 | 2;
223
294
  }
224
295
  interface DataRetentionConfig {
225
296
  purgeAfterDays: number;
226
- /** When set, audit log entries older than this are purged on the same daily timer as consent
227
- * record retention. Independent of `purgeAfterDays` since audit trails are often required to
228
- * be kept longer than the consent records they reference for compliance/investigation
229
- * purposes. Unset by default — audit logs are kept indefinitely unless explicitly configured. */
230
- auditLogPurgeAfterDays?: number;
231
297
  }
232
298
  interface BrandingConfig {
233
299
  /** Display name shown in the dashboard (login page, header, browser tab). Default: 'Consenti'. */
@@ -250,18 +316,10 @@ interface ConsentiServerConfig {
250
316
  compliance?: ComplianceConfig;
251
317
  multiTenant?: MultiTenantConfig;
252
318
  plugins?: ConsentiServerPlugin[];
253
- ageGate?: AgeGateConfig;
254
- tcf?: TcfConfig;
255
- dataRetention?: DataRetentionConfig;
256
319
  maxBodySize?: number;
257
320
  trustedProxies?: string[];
258
321
  branding?: BrandingConfig;
259
322
  s3Api?: S3ApiConfig;
260
- /** When set, every consent record is HMAC-SHA256 signed at create/update time (hex-encoded,
261
- * stored in `signature`) and the signature is checked on `GET /consent/:visitorId/verify`.
262
- * No-op and no schema burden when unset — opt-in tamper-evidence for server-stored records,
263
- * independent of the widget's own cookie-signing (`core.cookieSigningKey`). */
264
- consentSigningKey?: string;
265
323
  /** Called after every profile activate/deactivate/delete. isPurge=true means invalidate; false means warm. */
266
324
  handleCache?: (paths: string[], profileId: string, isPurge: boolean) => void;
267
325
  }
@@ -279,13 +337,40 @@ interface TenantSettings {
279
337
  * fallback. The public API has no auth token, so this is its only access gate. */
280
338
  allowedOrigins?: string[];
281
339
  /** Admin API origin allowlist — an additional CORS-layer check on top of Bearer-token auth
282
- * for browser-originated `/consenti/admin/*` requests. Empty/unset means no restriction
340
+ * for browser-originated `/consenti/admin/v1/*` requests. Empty/unset means no restriction
283
341
  * (Bearer token auth alone still applies); only enforced when the request carries an
284
342
  * `Origin` header — server-to-server callers are unaffected. */
285
343
  adminAllowedOrigins?: string[];
286
344
  /** Whether this tenant has completed (or skipped) the first-run setup wizard. Gates the
287
345
  * one-time `#/setup` redirect in the dashboard — never reset once true. Default: false. */
288
346
  setupCompleted?: boolean;
347
+ /** Whether `POST /setup/seed-profiles` has been called at least once for this tenant (an
348
+ * empty `groups: []` call counts). `POST /setup/complete` rejects until this is true, closing
349
+ * the bypass where the wizard's step-1 skip (or a direct API call) could mark setup complete
350
+ * without ever passing through config review or the profile step. */
351
+ profilesSeeded?: boolean;
352
+ /** Last-confirmed TCF `{cmpId, cmpVersion}` registration snapshot, hashed rather than stored
353
+ * in full — the live values always come from the static `compliance.tcf` config, never the DB.
354
+ * A hash mismatch (including never having confirmed) means TCF is treated as unconfirmed and
355
+ * fails closed: no `window.__tcfapi`, no TC string, a logged server error. Re-confirming in the
356
+ * dashboard (against the cached IAB CMP List) writes a fresh hash here. */
357
+ tcfConfirmation?: RegistrationConfirmation;
358
+ /** Same governance shape as `tcfConfirmation`, for the GPP US National section's own
359
+ * `compliance.gpp.{cmpId, cmpVersion}`. Self-attestation only — IAB doesn't publish a
360
+ * CMP-List equivalent for GPP, so there's no external registry to validate against. */
361
+ gppConfirmation?: RegistrationConfirmation;
362
+ }
363
+ /** Hash-based confirmation record — proves a specific `{cmpId, cmpVersion}` config was reviewed
364
+ * and confirmed as registered, without persisting the config values themselves in the DB. */
365
+ interface RegistrationConfirmation {
366
+ /** Hash of the confirmed `{cmpId, cmpVersion}` (and `publisherCC` for TCF). Compared against a
367
+ * fresh hash of the live static config on every check — any drift (including a first-ever
368
+ * check) means unconfirmed. */
369
+ configHash: string;
370
+ confirmedAt: string;
371
+ /** User id of the admin who checked the confirmation box. Undefined for confirmations made
372
+ * before this field existed. */
373
+ confirmedBy?: string;
289
374
  }
290
375
  interface ApiKey {
291
376
  id: string;
@@ -323,9 +408,10 @@ interface ConsentDbRecord {
323
408
  ageVerified?: boolean;
324
409
  parentalConsentToken?: string;
325
410
  tcfString?: string;
326
- /** HMAC-SHA256 signature over the record's core fields, hex-encoded. Only present when
327
- * `consentSigningKey` is configured — opt-in tamper-evidence for server-stored records,
328
- * independent of the widget's own cookie-signing (`core.cookieSigningKey`). */
411
+ gppString?: string;
412
+ /** HMAC-SHA256 signature over the record's core fields, hex-encoded — tamper-evidence for
413
+ * server-stored records via `dataSigningHash` (auto-generated when unset), independent of the
414
+ * widget's own cookie-signing (`core.cookieSigningKey`). */
329
415
  signature?: string;
330
416
  createdAt: string;
331
417
  updatedAt: string;
@@ -405,10 +491,11 @@ interface AuditLog {
405
491
  createdAt: string;
406
492
  }
407
493
  /** List-view shape for consents — omits the fields not shown in a table row (`consentJson`,
408
- * `parentalConsentToken`, `tcfString`, `signature`), so paginated list queries don't have to pull
409
- * those blobs off disk for every row. Fetch the full `ConsentDbRecord` via `getConsent(visitorId)`
410
- * when a single record's full detail is actually needed (e.g. opening a detail modal). */
411
- type ConsentSummary = Omit<ConsentDbRecord, 'consentJson' | 'parentalConsentToken' | 'tcfString' | 'signature'>;
494
+ * `parentalConsentToken`, `tcfString`, `gppString`, `signature`), so paginated list queries don't
495
+ * have to pull those blobs off disk for every row. Fetch the full `ConsentDbRecord` via
496
+ * `getConsent(visitorId)` when a single record's full detail is actually needed (e.g. opening a
497
+ * detail modal). */
498
+ type ConsentSummary = Omit<ConsentDbRecord, 'consentJson' | 'parentalConsentToken' | 'tcfString' | 'gppString' | 'signature'>;
412
499
  /** List-view shape for audit logs — omits `oldData`/`newData` (full before/after snapshots) for
413
500
  * the same reason as `ConsentSummary`. Fetch the full `AuditLog` via `getAuditLogById(id)` when a
414
501
  * single entry's full detail is needed. */
@@ -443,6 +530,7 @@ interface CreateConsentInput {
443
530
  ageVerified?: boolean;
444
531
  parentalConsentToken?: string;
445
532
  tcfString?: string;
533
+ gppString?: string;
446
534
  signature?: string;
447
535
  }
448
536
  interface UpdateConsentInput {
@@ -639,7 +727,6 @@ interface StorageAdapter {
639
727
  deleteApiKey(id: string): Promise<void>;
640
728
  getApiKeys(tenantId: string): Promise<ApiKey[]>;
641
729
  purgeExpiredConsents(olderThanDays: number): Promise<number>;
642
- purgeExpiredAuditLogs(olderThanDays: number): Promise<number>;
643
730
  createConsentTemplate(data: CreateConsentTemplateInput): Promise<ServerConsentTemplate>;
644
731
  updateConsentTemplate(id: string, data: UpdateConsentTemplateInput): Promise<ServerConsentTemplate>;
645
732
  deleteConsentTemplate(id: string): Promise<void>;
@@ -799,6 +886,17 @@ interface PreferenceModal extends Omit<MainBanner, "position"> {
799
886
  receiptLabel?: string;
800
887
  /** Description shown beneath the consent-receipt checkbox. Falls back to a default when omitted. */
801
888
  receiptDescription?: string;
889
+ /**
890
+ * Renders a "Forget me" control in the modal body, below the categories — only shown to
891
+ * visitors who already have a stored consent decision (nothing to erase otherwise). Wired to
892
+ * the same `DELETE /consent/:visitorId` erasure endpoint as `widget.deleteConsent()`, giving
893
+ * visitors self-service access to GDPR Art. 17 / CCPA-CPRA / LGPD Art. 18 / UK GDPR / DPDPA /
894
+ * KVKK / POPIA / PDPA-TH / APPI erasure-equivalent rights without the host wiring a link
895
+ * themselves. See the "Right to erasure" guide for the full compliance mapping.
896
+ */
897
+ showForgetMe?: boolean;
898
+ /** Label for the "Forget me" button (shown when `showForgetMe` is true). Falls back to a default when omitted. */
899
+ forgetMeLabel?: string;
802
900
  }
803
901
  /**
804
902
  * A UI-template button definition. Keyed by its own machine id (e.g. `'accept-all'`) inside
@@ -833,11 +931,16 @@ interface TemplateModalDef extends Omit<TemplateBannerDef, 'position'> {
833
931
  persistent?: boolean;
834
932
  hasSubheading?: boolean;
835
933
  mobileFullScreenBreakpoint?: number;
934
+ /** Renders a "Forget me" control below the categories, for visitors who already have a stored
935
+ * consent decision. See {@link PreferenceModal.showForgetMe}. */
936
+ showForgetMe?: boolean;
836
937
  }
837
938
  interface LocaleTranslations {
838
939
  mainBanner: DeepPartial<MainBanner>;
839
940
  gpcBanner?: DeepPartial<GpcBanner>;
840
941
  preferenceModal: DeepPartial<PreferenceModal>;
942
+ /** Only meaningful when the profile's `ageGate.enabled` is true. */
943
+ ageGateModal?: DeepPartial<AgeGateModalContent>;
841
944
  }
842
945
  interface ProfileTranslations {
843
946
  [locale: string]: LocaleTranslations;
@@ -847,6 +950,29 @@ interface DpdpaConfig {
847
950
  grievanceEmail: string;
848
951
  purposeDescription?: string;
849
952
  }
953
+ /**
954
+ * Per-profile age gate — GDPR Art. 8's consent age varies 13–16 by EU member state and DPDPA has
955
+ * its own child-data age rules, so one global age (the old `ConsentiServerConfig.ageGate`) was
956
+ * wrong for a multi-region deployment. Mirrors `dpdpa` exactly: profile-scoped, not global.
957
+ */
958
+ interface AgeGateConfig {
959
+ enabled: boolean;
960
+ minimumAge: number;
961
+ requireParentalConsent?: boolean;
962
+ }
963
+ /** Per-locale age-gate modal text — translated the same way `mainBanner`/`preferenceModal`
964
+ * content is, unlike the widget's own hardcoded UI chrome (close button, locale switcher). */
965
+ interface AgeGateModalContent {
966
+ heading?: string;
967
+ htmlText: string;
968
+ confirmButtonLabel: string;
969
+ denyButtonLabel: string;
970
+ parentalConsent: {
971
+ heading?: string;
972
+ htmlText: string;
973
+ confirmButtonLabel: string;
974
+ };
975
+ }
850
976
  interface LocaleTextContent {
851
977
  mainBanner: {
852
978
  heading?: string;
@@ -873,7 +999,11 @@ interface LocaleTextContent {
873
999
  }>;
874
1000
  receiptLabel?: string;
875
1001
  receiptDescription?: string;
1002
+ /** Per-locale override of `PreferenceModal.forgetMeLabel`; only meaningful when `showForgetMe` is true. */
1003
+ forgetMeLabel?: string;
876
1004
  };
1005
+ /** Only meaningful when the profile's `ageGate.enabled` is true. */
1006
+ ageGateModal?: AgeGateModalContent;
877
1007
  }
878
1008
  interface ProfileConfig {
879
1009
  id: string;
@@ -888,6 +1018,12 @@ interface ProfileConfig {
888
1018
  darkMode?: boolean;
889
1019
  allowedOrigins?: string[];
890
1020
  dpdpa?: DpdpaConfig;
1021
+ ageGate?: AgeGateConfig;
1022
+ /** Default locale's age-gate modal text — mirrors `mainBanner`/`preferenceModal` (inline,
1023
+ * required-shape-when-present), not `dpdpa` (which has no locale text at all). Every other
1024
+ * locale's `ageGateModal` lives in `localeContents`/`LocaleContentInput` like the rest of
1025
+ * that locale's content. */
1026
+ ageGateModal?: AgeGateModalContent;
891
1027
  regulation?: 'gdpr' | 'ccpa' | 'cpra' | 'dpdpa' | 'uk-gdpr' | 'lgpd' | 'pipeda' | 'popia' | 'pdpa-th' | 'appi' | 'kvkk';
892
1028
  regulations?: string[];
893
1029
  consentTemplateId?: string;
@@ -1005,6 +1141,8 @@ interface PublicProfileResponse {
1005
1141
  allowReceipt?: boolean;
1006
1142
  darkMode?: boolean;
1007
1143
  dpdpa?: DpdpaConfig;
1144
+ ageGate?: AgeGateConfig;
1145
+ ageGateModal?: AgeGateModalContent;
1008
1146
  showFooterMetadata?: boolean;
1009
1147
  enhanceAccessibility?: boolean;
1010
1148
  createdAt?: string;
@@ -1123,6 +1261,20 @@ declare class ProfileService {
1123
1261
  * requested via `getVersionFile()`, which works for these ids too.
1124
1262
  */
1125
1263
  listArchivedProfiles(): Promise<ArchivedProfileSummary[]>;
1264
+ /** Strips display text from an embedded profile's button map — a `UITemplate`'s buttons own
1265
+ * layout/behavior only, never text (button labels are authored per-locale on the profile
1266
+ * instead, see `LocaleTextContent.*.buttonLabels`). */
1267
+ private toTemplateButtonMap;
1268
+ /** Builds a `UITemplate`'s structural definitions (position, buttons, close/locale-switcher
1269
+ * flags) from an embedded profile's English translation — no heading/htmlText/button text,
1270
+ * which stay inline on the seeded profile as per-locale content, never on the template. */
1271
+ private buildSeedUiTemplatePayload;
1272
+ /**
1273
+ * Idempotently gets-or-creates the `ConsentTemplate` + `UITemplate` pair backing one seeded
1274
+ * compliance group's profile, named consistently with the profile itself (`Default — {group}`).
1275
+ * Looked up by name/tenant first so re-running the setup wizard never creates duplicates.
1276
+ */
1277
+ private getOrCreateSeedTemplates;
1126
1278
  /**
1127
1279
  * Seeds a default profile for a compliance group from the embedded English profile in
1128
1280
  * `@consenti/utils`, merged with locale text overlays for de/es/fr/ja. Every locale — including
@@ -1132,6 +1284,14 @@ declare class ProfileService {
1132
1284
  * Only creates the profile if no active profile already exists for the compliance group.
1133
1285
  * Safe to call repeatedly (idempotent).
1134
1286
  *
1287
+ * Also seeds (idempotently) a `ConsentTemplate`/`UITemplate` pair for the group and links the
1288
+ * profile to both via `consentTemplateId`/`uiTemplateId` — same as a dashboard-authored profile
1289
+ * is required to. The profile is a *snapshot*, not a live pointer: `cookies`/`mainBanner`/
1290
+ * `gpcBanner`/`preferenceModal` stay fully resolved inline (DB row + on-disk locale files)
1291
+ * exactly as before; the template ids are additive, purely for traceability and so the editor
1292
+ * knows which template to re-offer on next edit. Editing the template later never silently
1293
+ * changes this profile — only an explicit re-save of the profile itself does.
1294
+ *
1135
1295
  * @param complianceGroup - One of the 8 compliance group IDs.
1136
1296
  */
1137
1297
  seedDefaultProfile(complianceGroup: string): Promise<void>;
@@ -1186,9 +1346,14 @@ declare class ConsentService {
1186
1346
  private eventBus?;
1187
1347
  private tcfConfig?;
1188
1348
  private storage?;
1189
- private consentSigningKey?;
1190
- constructor(consents: ConsentRepo, visitors: VisitorRepo, profiles: ProfileRepo, audit: AuditRepo, tenantId?: string, compliance?: ComplianceConfig | undefined, pluginEngine?: PluginEngine | undefined, eventBus?: EventEmitter | undefined, tcfConfig?: TcfConfig | undefined, storage?: StorageAdapter | undefined, consentSigningKey?: string | undefined);
1349
+ private dataSigningHash?;
1350
+ private gppConfig?;
1351
+ constructor(consents: ConsentRepo, visitors: VisitorRepo, profiles: ProfileRepo, audit: AuditRepo, tenantId?: string, compliance?: ComplianceConfig | undefined, pluginEngine?: PluginEngine | undefined, eventBus?: EventEmitter | undefined, tcfConfig?: TcfConfig | undefined, storage?: StorageAdapter | undefined, dataSigningHash?: string | undefined, gppConfig?: GppConfig | undefined);
1191
1352
  private sign;
1353
+ /** Fails closed: no `storage` to check against means unconfirmed, never assumed confirmed. */
1354
+ private isTcfConfirmed;
1355
+ /** Same fail-closed shape as isTcfConfirmed() — see registration-confirmation.ts. */
1356
+ private isGppConfirmed;
1192
1357
  /**
1193
1358
  * Profiles created via the dashboard wizard reference a consent template (`consentTemplateId`)
1194
1359
  * rather than embedding `cookies`/`preferenceModal.categories` directly on `profileJson` — those
@@ -1210,7 +1375,8 @@ declare class VisitorService {
1210
1375
  private visitors;
1211
1376
  private tenantId;
1212
1377
  private eventBus?;
1213
- constructor(visitors: VisitorRepo, tenantId?: string, eventBus?: EventEmitter | undefined);
1378
+ private dataSigningHash;
1379
+ constructor(visitors: VisitorRepo, tenantId?: string, eventBus?: EventEmitter | undefined, dataSigningHash?: string);
1214
1380
  upsert(data: {
1215
1381
  visitorId: string;
1216
1382
  ip?: string;
@@ -1335,7 +1501,6 @@ declare class BetterSqlite3Adapter implements StorageAdapter {
1335
1501
  getSettings(tenantId: string): Promise<TenantSettings>;
1336
1502
  updateSettings(tenantId: string, data: Partial<TenantSettings>): Promise<TenantSettings>;
1337
1503
  purgeExpiredConsents(olderThanDays: number): Promise<number>;
1338
- purgeExpiredAuditLogs(olderThanDays: number): Promise<number>;
1339
1504
  private mapApiKey;
1340
1505
  createApiKey(data: CreateApiKeyInput): Promise<ApiKey>;
1341
1506
  getApiKeyByHash(keyHash: string): Promise<ApiKey | null>;
@@ -1440,7 +1605,6 @@ declare class MongoDBAdapter implements StorageAdapter {
1440
1605
  getSettings(tenantId: string): Promise<TenantSettings>;
1441
1606
  updateSettings(tenantId: string, data: Partial<TenantSettings>): Promise<TenantSettings>;
1442
1607
  purgeExpiredConsents(olderThanDays: number): Promise<number>;
1443
- purgeExpiredAuditLogs(olderThanDays: number): Promise<number>;
1444
1608
  createConsentTemplate(): Promise<never>;
1445
1609
  updateConsentTemplate(): Promise<never>;
1446
1610
  deleteConsentTemplate(): Promise<void>;
@@ -1533,7 +1697,6 @@ declare class MySQLAdapter implements StorageAdapter {
1533
1697
  updateSettings(tenantId: string, data: Partial<TenantSettings>): Promise<TenantSettings>;
1534
1698
  deleteTenant(id: string): Promise<void>;
1535
1699
  purgeExpiredConsents(olderThanDays: number): Promise<number>;
1536
- purgeExpiredAuditLogs(olderThanDays: number): Promise<number>;
1537
1700
  createConsentTemplate(): Promise<never>;
1538
1701
  updateConsentTemplate(): Promise<never>;
1539
1702
  deleteConsentTemplate(): Promise<void>;
@@ -1625,7 +1788,6 @@ declare class PostgreSQLAdapter implements StorageAdapter {
1625
1788
  getSettings(tenantId: string): Promise<TenantSettings>;
1626
1789
  updateSettings(tenantId: string, data: Partial<TenantSettings>): Promise<TenantSettings>;
1627
1790
  purgeExpiredConsents(olderThanDays: number): Promise<number>;
1628
- purgeExpiredAuditLogs(olderThanDays: number): Promise<number>;
1629
1791
  createConsentTemplate(): Promise<never>;
1630
1792
  updateConsentTemplate(): Promise<never>;
1631
1793
  deleteConsentTemplate(): Promise<void>;
@@ -1649,8 +1811,8 @@ declare class PostgreSQLAdapter implements StorageAdapter {
1649
1811
  *
1650
1812
  * Watches a single consent parameter across *every* visitor's submission (not just one
1651
1813
  * browser's own state) and fires `onGrant`/`onDeny` when that parameter's status actually
1652
- * changes — via the same `eventBus` `createConsenti()` already emits `consent.created`/
1653
- * `consent.updated` on. Useful for server-side integrations (CRM sync, suppression lists,
1814
+ * changes — via the same `eventBus` `createConsenti()` already emits `consent:created`/
1815
+ * `consent:updated` on. Useful for server-side integrations (CRM sync, suppression lists,
1654
1816
  * downstream webhooks) that need to react to consent decisions without polling the database.
1655
1817
  *
1656
1818
  * There is no server equivalent of the widget's `ConsentScript` — injecting a `<script>` tag
@@ -1691,7 +1853,7 @@ interface ConsentActionOptions {
1691
1853
  onDeny?: (params: ConsentActionParams) => void;
1692
1854
  }
1693
1855
  /**
1694
- * Watches a single consent parameter across every incoming `consent.created`/`consent.updated`
1856
+ * Watches a single consent parameter across every incoming `consent:created`/`consent:updated`
1695
1857
  * event and fires `onGrant`/`onDeny` on transitions. Call {@link ConsentAction.destroy} to
1696
1858
  * remove the event listeners when the hook is no longer needed.
1697
1859
  */
@@ -1750,8 +1912,8 @@ interface CategoryActionOptions {
1750
1912
  onDeny?: (params: CategoryActionParams) => void;
1751
1913
  }
1752
1914
  /**
1753
- * Watches a category's rollup consent status across every incoming `consent.created`/
1754
- * `consent.updated` event and fires `onGrant`/`onDeny` on transitions. Call
1915
+ * Watches a category's rollup consent status across every incoming `consent:created`/
1916
+ * `consent:updated` event and fires `onGrant`/`onDeny` on transitions. Call
1755
1917
  * {@link CategoryAction.destroy} to remove the event listeners when no longer needed.
1756
1918
  */
1757
1919
  declare class CategoryAction {