@consenti/api 0.2.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 (79) hide show
  1. package/README.md +578 -143
  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-BlQ2gEWz.js +1 -0
  8. package/dist/dashboard/assets/AuditLog-BpZoAFOe.js +1 -0
  9. package/dist/dashboard/assets/ChangePassword-BZkP2dXQ.js +1 -0
  10. package/dist/dashboard/assets/ConfirmDialog-BDlnfTdx.js +1 -0
  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-CJCrdr3o.js +1 -0
  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-CX5-Oupy.js +3 -0
  21. package/dist/dashboard/assets/RecordDetailModal-DIpGxfqA.js +1 -0
  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-C_JusbT5.js +1 -0
  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-DEi_W2TM.js +1 -0
  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/debounce-DlvAAT2V.js +1 -0
  40. package/dist/dashboard/assets/dist-GoiMJCTt.js +1 -0
  41. package/dist/dashboard/assets/es-B6Jp0OPe.js +25 -0
  42. package/dist/dashboard/assets/external-link-CFZxxe3W.js +1 -0
  43. package/dist/dashboard/assets/fr-CshYcQPW.js +25 -0
  44. package/dist/dashboard/assets/index-Cr5M1Qy_.css +2 -0
  45. package/dist/dashboard/assets/index-Tr_BcjkN.js +26 -0
  46. package/dist/dashboard/assets/ja-CgjZNBrc.js +25 -0
  47. package/dist/dashboard/assets/profiles-C_SuoLJD.js +1 -0
  48. package/dist/dashboard/assets/roles-D7L936FV.js +1 -0
  49. package/dist/dashboard/assets/rotate-ccw-clock-6y2NDrJ7.js +1 -0
  50. package/dist/dashboard/assets/search-DKwfLBOv.js +1 -0
  51. package/dist/dashboard/assets/templates-CIyMjskA.js +1 -0
  52. package/dist/dashboard/assets/templates-DY68xNys.js +1 -0
  53. package/dist/dashboard/assets/trash-2-eX9s6x8V.js +1 -0
  54. package/dist/dashboard/assets/trash-O30ycMcz.js +1 -0
  55. package/dist/dashboard/assets/useHashQuery-BDjUPXDR.js +1 -0
  56. package/dist/dashboard/assets/users-BRrNOK0W.js +1 -0
  57. package/dist/dashboard/assets/x-BhZiH8AL.js +1 -0
  58. package/dist/dashboard/assets/zh-CN-BnLZkfBC.js +25 -0
  59. package/dist/dashboard/index.html +4 -2
  60. package/dist/dist-QJYW5KUK.js +2 -0
  61. package/dist/dist-QJYW5KUK.js.map +1 -0
  62. package/dist/index.cjs +90 -101
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +847 -150
  65. package/dist/index.d.ts +847 -150
  66. package/dist/index.js +90 -101
  67. package/dist/index.js.map +1 -1
  68. package/dist/profiles-FCX454HS.js +2 -0
  69. package/dist/profiles-FCX454HS.js.map +1 -0
  70. package/dist/widget/widget.css +1 -1
  71. package/dist/widget/widget.js +1 -1
  72. package/package.json +32 -17
  73. package/dist/dashboard/assets/de-DwE2aQfW.js +0 -5
  74. package/dist/dashboard/assets/es-Dq88TWkB.js +0 -5
  75. package/dist/dashboard/assets/fr-o_Pyg8eR.js +0 -5
  76. package/dist/dashboard/assets/index-BSsYDdb9.js +0 -40
  77. package/dist/dashboard/assets/index-bOTGevzl.css +0 -2
  78. package/dist/dashboard/assets/ja-CwgXYmpD.js +0 -5
  79. package/dist/dashboard/assets/zh-CN-ety9KoQ0.js +0 -5
package/dist/index.d.cts CHANGED
@@ -1,7 +1,46 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { IncomingMessage, ServerResponse } from 'node:http';
3
3
 
4
+ /**
5
+ * Deep-partial with JSON Merge Patch (RFC 7396) delete semantics: any key may be
6
+ * set to `null` instead of its usual type to signal "delete this key" when the
7
+ * partial is applied with `deepMerge()` — `undefined`/omitted keeps the base
8
+ * value, `null` removes it. Only meaningful for object/map keys (e.g. removing
9
+ * a `CategoryMap`/`CookieMap` entry via `profileOverride`); array elements are
10
+ * merged positionally and don't support deletion this way.
11
+ */
12
+ type DeepPartial<T> = T extends Function ? T : T extends Array<infer InferredArrayType> ? Array<DeepPartial<InferredArrayType>> : T extends ReadonlyArray<infer InferredReadonlyArrayType> ? ReadonlyArray<DeepPartial<InferredReadonlyArrayType>> : T extends object ? {
13
+ [Key in keyof T]?: DeepPartial<T[Key]> | null;
14
+ } : T;
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
+ */
4
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"];
43
+ declare const COOKIE_PURPOSE_IDS: readonly ["necessary", "functional", "preferences", "analytics", "marketing"];
5
44
 
6
45
  /**
7
46
  * Compliance-related literal types, derived from the regulation/group ID data
@@ -18,6 +57,20 @@ declare const COMPLIANCE_GROUP_IDS: readonly ["opt-in", "opt-out", "opt-out-stri
18
57
 
19
58
  type ComplianceGroupId = typeof COMPLIANCE_GROUP_IDS[number];
20
59
 
60
+ /** Resolved content for one non-default locale — template shape merged with that locale's
61
+ * author text, computed client-side (dashboard) the same way the default locale's content on
62
+ * `StoredProfileJson` is. Never persisted in the DB row; `ProfileService` writes it straight to
63
+ * that locale's on-disk version file. Keyed by locale code on `CreateProfileInput`/
64
+ * `UpdateProfileInput.localeContent` — only locales actually touched this save need an entry;
65
+ * any other locale already in `profileJson.locales` is carried forward unchanged from the
66
+ * previous version. */
67
+ interface LocaleContentInput {
68
+ mainBanner: MainBanner;
69
+ gpcBanner?: GpcBanner;
70
+ preferenceModal: PreferenceModal;
71
+ /** Only meaningful when the profile's `ageGate.enabled` is true. */
72
+ ageGateModal?: AgeGateModalContent;
73
+ }
21
74
  interface StorageConfig {
22
75
  driver: 'sqlite' | 'better-sqlite3' | 'node-sqlite3-wasm' | 'node:sqlite' | 'mongodb' | 'mysql' | 'postgresql' | 'json';
23
76
  path?: string;
@@ -27,6 +80,20 @@ interface StorageConfig {
27
80
  port?: number;
28
81
  user?: string;
29
82
  password?: string;
83
+ /** Postgres/MySQL only. Max pool connections. Default: 10 (both drivers' own default — set
84
+ * explicitly here so it's a documented, tunable value rather than an implicit library default). */
85
+ poolMax?: number;
86
+ /**
87
+ * Postgres/MySQL only. Milliseconds before an in-flight query is aborted, so one slow/runaway
88
+ * query can't hold a pool connection indefinitely. Unset by default: a forced default could
89
+ * silently break legitimate long-running operations (e.g. exporting millions of records) that
90
+ * this project has no way to load-test — opt in once you've measured your own worst-case query.
91
+ */
92
+ statementTimeoutMs?: number;
93
+ /** Postgres only. Milliseconds a client may sit idle inside an open transaction before being
94
+ * terminated — protects against a `BEGIN` with no matching `COMMIT`/`ROLLBACK` holding a
95
+ * connection forever. Unset by default, same reasoning as `statementTimeoutMs`. */
96
+ idleInTransactionTimeoutMs?: number;
30
97
  }
31
98
  interface OidcConfig {
32
99
  issuer: string;
@@ -46,7 +113,7 @@ interface SamlConfig {
46
113
  }
47
114
  interface AuthConfig {
48
115
  mode: 'local' | 'jwt' | 'custom' | 'oidc' | 'saml';
49
- jwtSecret?: string;
116
+ masterSecret?: string;
50
117
  validateUser?: (req: unknown) => Promise<AdminUser | null>;
51
118
  adminEmail?: string;
52
119
  adminPassword?: string;
@@ -62,6 +129,14 @@ type GeoResult = {
62
129
  country: string | null;
63
130
  region: string | null;
64
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;
65
140
  };
66
141
  type CountryResolverFn = (ctx: {
67
142
  ip: string;
@@ -81,18 +156,33 @@ interface ProfileSummary {
81
156
  name: string;
82
157
  defaultLocale: string;
83
158
  complianceGroup: ComplianceGroupId | null;
84
- version: number;
159
+ customComplianceGroup: string | null;
85
160
  isActive: boolean;
86
- cookieTemplateName: string | null;
161
+ consentTemplateName: string | null;
87
162
  uiTemplateName: string | null;
88
163
  createdAt: string;
89
164
  updatedAt: string;
90
165
  }
166
+ /**
167
+ * One snapshot in a profile's edit history. `Profile.id` is stable across edits; each save
168
+ * increments `Profile.version` in place and writes a new snapshot under that version number.
169
+ */
91
170
  interface ProfileVersionEntry {
92
171
  version: number;
93
172
  createdAt: string;
94
173
  locales: string[];
95
174
  }
175
+ /**
176
+ * A profile-id directory found on disk with no matching DB row — the profile was deleted
177
+ * (or predates the current versioning model) but its version-snapshot history wasn't. Built
178
+ * from a directory listing only (id, version count, last-modified) — no file content is read
179
+ * until a specific version is opened via the normal version-history endpoints.
180
+ */
181
+ interface ArchivedProfileSummary {
182
+ id: string;
183
+ versionCount: number;
184
+ lastModified: string;
185
+ }
96
186
  interface OptInFilters {
97
187
  profileId?: string;
98
188
  complianceGroup?: string;
@@ -124,31 +214,49 @@ interface OptInStats {
124
214
  byLocale: Record<string, OptInByLocale>;
125
215
  byDate: OptInByDate[];
126
216
  }
127
- type ComplianceMapOverride = 'default' | ComplianceMapData | 'auto';
128
217
  interface ComplianceMapData {
129
218
  version: string;
130
219
  countries: Record<string, CountryComplianceEntry>;
131
220
  }
132
221
  interface CountryComplianceEntry {
133
222
  complianceGroup: ComplianceGroupId;
134
- default: ComplianceGroupId;
135
- 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;
136
228
  overriddenRegions?: Record<string, {
137
229
  complianceGroup: ComplianceGroupId;
138
- description: string;
230
+ description?: string;
139
231
  }>;
140
232
  }
141
233
  interface ComplianceConfig {
142
- /** @deprecated use type */
143
- gdpr?: boolean;
144
- /** @deprecated use type */
145
- 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. */
146
237
  gpc?: boolean | 'strict';
147
238
  type?: ComplianceGroupId | 'auto';
148
- /** @deprecated use 'default' instead of 'language' or 'timezone'; 'geoip' and 'hosted-geoip-lite' removed */
149
- geoDataProvider?: 'default' | 'maxmind' | 'language' | 'timezone' | 'geoip' | 'hosted-geoip-lite' | CountryResolverFn;
150
- autoComplianceMap?: ComplianceMapOverride;
151
- 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;
152
260
  }
153
261
  interface DashboardConfig {
154
262
  enabled?: boolean;
@@ -157,15 +265,32 @@ interface DashboardConfig {
157
265
  interface MultiTenantConfig {
158
266
  enabled: boolean;
159
267
  }
160
- interface AgeGateConfig {
161
- enabled: boolean;
162
- minimumAge: number;
163
- requireParentalConsent?: boolean;
164
- }
165
268
  interface TcfConfig {
166
269
  enabled: boolean;
167
270
  cmpId: number;
168
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;
169
294
  }
170
295
  interface DataRetentionConfig {
171
296
  purgeAfterDays: number;
@@ -191,15 +316,12 @@ interface ConsentiServerConfig {
191
316
  compliance?: ComplianceConfig;
192
317
  multiTenant?: MultiTenantConfig;
193
318
  plugins?: ConsentiServerPlugin[];
194
- ageGate?: AgeGateConfig;
195
- tcf?: TcfConfig;
196
- dataRetention?: DataRetentionConfig;
197
319
  maxBodySize?: number;
198
320
  trustedProxies?: string[];
199
321
  branding?: BrandingConfig;
200
322
  s3Api?: S3ApiConfig;
201
323
  /** Called after every profile activate/deactivate/delete. isPurge=true means invalidate; false means warm. */
202
- handleCache?: (paths: string[], currentVersion: number, isPurge: boolean) => void;
324
+ handleCache?: (paths: string[], profileId: string, isPurge: boolean) => void;
203
325
  }
204
326
  interface Tenant {
205
327
  id: string;
@@ -208,21 +330,69 @@ interface Tenant {
208
330
  createdAt: string;
209
331
  updatedAt: string;
210
332
  }
333
+ /** Tenant-wide dashboard-managed defaults, shown on the API Config page. */
334
+ interface TenantSettings {
335
+ /** Public API origin allowlist. Distinct from `ProfileConfig.allowedOrigins`, which the
336
+ * origin check on `POST /consent` prefers when a profile sets its own list; this is the
337
+ * fallback. The public API has no auth token, so this is its only access gate. */
338
+ allowedOrigins?: string[];
339
+ /** Admin API origin allowlist — an additional CORS-layer check on top of Bearer-token auth
340
+ * for browser-originated `/consenti/admin/v1/*` requests. Empty/unset means no restriction
341
+ * (Bearer token auth alone still applies); only enforced when the request carries an
342
+ * `Origin` header — server-to-server callers are unaffected. */
343
+ adminAllowedOrigins?: string[];
344
+ /** Whether this tenant has completed (or skipped) the first-run setup wizard. Gates the
345
+ * one-time `#/setup` redirect in the dashboard — never reset once true. Default: false. */
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;
374
+ }
211
375
  interface ApiKey {
212
376
  id: string;
213
377
  tenantId: string;
214
378
  keyHash: string;
215
379
  name: string;
216
380
  isActive: boolean;
381
+ /** User id of the admin who created this key. Undefined for keys created before this field existed. */
382
+ createdBy?: string;
383
+ /** Optional expiry set at creation. Checked lazily at auth time and on list — no background sweep. */
384
+ expireBy?: string;
217
385
  createdAt: string;
386
+ updatedAt?: string;
218
387
  }
219
388
  interface Profile {
220
389
  id: string;
221
390
  tenantId: string;
222
391
  name: string;
223
392
  defaultLocale: string;
393
+ /** Starts at 1, incremented in place on every save. The row itself is mutated, never replaced — see {@link ProfileVersionEntry} for the on-disk snapshot history this produces. */
224
394
  version: number;
225
- profileJson: ProfileConfig;
395
+ profileJson: StoredProfileJson;
226
396
  createdAt: string;
227
397
  updatedAt: string;
228
398
  }
@@ -231,7 +401,6 @@ interface ConsentDbRecord {
231
401
  tenantId: string;
232
402
  visitorId: string;
233
403
  profileId: string;
234
- profileVersion: number;
235
404
  locale: string;
236
405
  consentJson: ConsentValue;
237
406
  gpcDetected: boolean;
@@ -239,6 +408,11 @@ interface ConsentDbRecord {
239
408
  ageVerified?: boolean;
240
409
  parentalConsentToken?: string;
241
410
  tcfString?: string;
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`). */
415
+ signature?: string;
242
416
  createdAt: string;
243
417
  updatedAt: string;
244
418
  }
@@ -289,6 +463,22 @@ interface Permission {
289
463
  name: string;
290
464
  description?: string;
291
465
  }
466
+ /** Proof that the consent banner was rendered to a visitor — recorded even if they never
467
+ * interact with it, distinct from (and never merged into) an actual consent decision. */
468
+ interface NoticeShownRecord {
469
+ id: string;
470
+ tenantId: string;
471
+ visitorId: string;
472
+ profileId: string;
473
+ locale: string;
474
+ createdAt: string;
475
+ }
476
+ interface CreateNoticeShownInput {
477
+ tenantId: string;
478
+ visitorId: string;
479
+ profileId: string;
480
+ locale: string;
481
+ }
292
482
  interface AuditLog {
293
483
  id: string;
294
484
  tenantId: string;
@@ -300,22 +490,39 @@ interface AuditLog {
300
490
  newData?: unknown;
301
491
  createdAt: string;
302
492
  }
493
+ /** List-view shape for consents — omits the fields not shown in a table row (`consentJson`,
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'>;
499
+ /** List-view shape for audit logs — omits `oldData`/`newData` (full before/after snapshots) for
500
+ * the same reason as `ConsentSummary`. Fetch the full `AuditLog` via `getAuditLogById(id)` when a
501
+ * single entry's full detail is needed. */
502
+ type AuditLogSummary = Omit<AuditLog, 'oldData' | 'newData'>;
303
503
  interface CreateProfileInput {
304
504
  tenantId: string;
305
505
  name: string;
306
506
  defaultLocale: string;
307
- profileJson: ProfileConfig;
507
+ profileJson: StoredProfileJson;
508
+ /** Non-default locales' resolved content for this save — see {@link LocaleContentInput}. */
509
+ localeContent?: Record<string, LocaleContentInput>;
308
510
  }
309
511
  interface UpdateProfileInput {
310
512
  name?: string;
311
513
  defaultLocale?: string;
312
- profileJson?: ProfileConfig;
514
+ profileJson?: StoredProfileJson;
515
+ /** Explicit new version number — set by `ProfileService.update()` to `old.version + 1` for a content edit. Omitted for isActive-only flips (activate/deactivate), which leave the version unchanged. */
516
+ version?: number;
517
+ /** Non-default locales' resolved content for this save — see {@link LocaleContentInput}. Any
518
+ * locale in `profileJson.locales` not present here is carried forward unchanged from the
519
+ * previous version's on-disk file. */
520
+ localeContent?: Record<string, LocaleContentInput>;
313
521
  }
314
522
  interface CreateConsentInput {
315
523
  tenantId: string;
316
524
  visitorId: string;
317
525
  profileId: string;
318
- profileVersion: number;
319
526
  locale: string;
320
527
  consentJson: ConsentValue;
321
528
  gpcDetected: boolean;
@@ -323,11 +530,14 @@ interface CreateConsentInput {
323
530
  ageVerified?: boolean;
324
531
  parentalConsentToken?: string;
325
532
  tcfString?: string;
533
+ gppString?: string;
534
+ signature?: string;
326
535
  }
327
536
  interface UpdateConsentInput {
328
537
  consentJson: ConsentValue;
329
538
  locale?: string;
330
539
  gpcDetected?: boolean;
540
+ signature?: string;
331
541
  }
332
542
  interface CreateVisitorInput {
333
543
  tenantId: string;
@@ -376,6 +586,8 @@ interface ConsentFilters {
376
586
  to?: string;
377
587
  page?: number;
378
588
  limit?: number;
589
+ /** Free-text search across visitorId, profileId, locale, source. */
590
+ q?: string;
379
591
  }
380
592
  interface VisitorFilters {
381
593
  tenantId: string;
@@ -383,6 +595,8 @@ interface VisitorFilters {
383
595
  to?: string;
384
596
  page?: number;
385
597
  limit?: number;
598
+ /** Free-text search across visitorId, country. */
599
+ q?: string;
386
600
  }
387
601
  interface AuditFilters {
388
602
  tenantId: string;
@@ -392,12 +606,24 @@ interface AuditFilters {
392
606
  to?: string;
393
607
  page?: number;
394
608
  limit?: number;
609
+ /** Free-text search across action, resourceType, resourceId, userId. */
610
+ q?: string;
611
+ }
612
+ /** Page of results plus the total row count matching the same filters (ignoring page/limit) —
613
+ * lets callers compute total page count for numbered pagination without a second round trip. */
614
+ interface PagedResult<T> {
615
+ items: T[];
616
+ total: number;
617
+ page: number;
618
+ limit: number;
395
619
  }
396
620
  interface ConsentVerifyResult {
397
621
  valid: boolean;
398
- reasons: Array<'profile_version_mismatch' | 'consent_expired' | 'hmac_invalid'>;
399
- currentProfileVersion?: number;
400
- consentProfileVersion?: number;
622
+ reasons: Array<'profile_changed' | 'consent_expired' | 'hmac_invalid'>;
623
+ /** The id of the profile currently active for this consent's compliance group, if any. */
624
+ currentProfileId?: string;
625
+ /** The id of the profile this consent was originally collected against. */
626
+ consentProfileId?: string;
401
627
  }
402
628
  type SafeAdminUser = Omit<AdminUser, 'passwordHash'>;
403
629
  interface CreateRoleInput {
@@ -438,6 +664,8 @@ interface CreateApiKeyInput {
438
664
  tenantId: string;
439
665
  name: string;
440
666
  keyHash: string;
667
+ createdBy?: string;
668
+ expireBy?: string;
441
669
  }
442
670
  interface CreateTenantInput {
443
671
  name: string;
@@ -459,22 +687,24 @@ interface StorageAdapter {
459
687
  findActiveProfileByComplianceGroup(tenantId: string, complianceGroup: string): Promise<Profile | null>;
460
688
  /** Lightweight list — no profile_json blob; single JOIN for template names. */
461
689
  listProfilesSummary(tenantId: string): Promise<ProfileSummary[]>;
462
- findProfilesUsingCookieTemplate(templateId: string): Promise<ProfileSummary[]>;
690
+ findProfilesUsingConsentTemplate(templateId: string): Promise<ProfileSummary[]>;
463
691
  findProfilesUsingUITemplate(templateId: string): Promise<ProfileSummary[]>;
464
692
  getOptInStats(tenantId: string, filters: OptInFilters): Promise<OptInStats>;
465
693
  createConsent(data: CreateConsentInput): Promise<ConsentDbRecord>;
466
694
  updateConsent(visitorId: string, data: UpdateConsentInput): Promise<ConsentDbRecord>;
467
695
  deleteConsent(visitorId: string): Promise<void>;
468
696
  getConsent(visitorId: string): Promise<ConsentDbRecord | null>;
469
- getConsents(filters: ConsentFilters): Promise<ConsentDbRecord[]>;
697
+ getConsents(filters: ConsentFilters): Promise<PagedResult<ConsentSummary>>;
470
698
  streamConsents(filters: ConsentFilters): AsyncIterable<ConsentDbRecord>;
471
699
  createVisitor(data: CreateVisitorInput): Promise<Visitor>;
472
700
  updateVisitor(visitorId: string, data: UpdateVisitorInput): Promise<Visitor>;
473
701
  deleteVisitor(visitorId: string): Promise<void>;
474
702
  getVisitor(visitorId: string): Promise<Visitor | null>;
475
- getVisitors(filters: VisitorFilters): Promise<Visitor[]>;
703
+ getVisitors(filters: VisitorFilters): Promise<PagedResult<Visitor>>;
476
704
  getConsentHistory(visitorId: string): Promise<ConsentHistoryEntry[]>;
477
705
  streamAuditLogs(filters: AuditFilters): AsyncIterable<AuditLog>;
706
+ createNoticeShown(data: CreateNoticeShownInput): Promise<NoticeShownRecord>;
707
+ getNoticeShownForVisitor(visitorId: string): Promise<NoticeShownRecord[]>;
478
708
  getOverviewStats(tenantId: string): Promise<OverviewStats>;
479
709
  getCategoryStats(tenantId: string): Promise<CategoryStats>;
480
710
  getTimeline(tenantId: string, days?: number): Promise<TimelineEntry[]>;
@@ -484,17 +714,25 @@ interface StorageAdapter {
484
714
  createTenant(data: CreateTenantInput): Promise<Tenant>;
485
715
  updateTenant(id: string, data: UpdateTenantInput): Promise<Tenant>;
486
716
  deleteTenant(id: string): Promise<void>;
717
+ /** Returns `{}` (all fields unset) if no settings row exists yet for this tenant. */
718
+ getSettings(tenantId: string): Promise<TenantSettings>;
719
+ updateSettings(tenantId: string, data: Partial<TenantSettings>): Promise<TenantSettings>;
487
720
  createApiKey(data: CreateApiKeyInput): Promise<ApiKey>;
488
721
  getApiKeyByHash(keyHash: string): Promise<ApiKey | null>;
489
722
  revokeApiKey(id: string): Promise<void>;
723
+ /** Re-enables a previously revoked key (same hash, no new secret). Clears `expireBy` if it's
724
+ * in the past, so a lazily-expired key doesn't immediately re-expire on the next check. */
725
+ reactivateApiKey(id: string): Promise<void>;
726
+ /** Permanently removes the key row — unlike `revokeApiKey`, this cannot be undone. */
727
+ deleteApiKey(id: string): Promise<void>;
490
728
  getApiKeys(tenantId: string): Promise<ApiKey[]>;
491
729
  purgeExpiredConsents(olderThanDays: number): Promise<number>;
492
- createCookieTemplate(data: CreateCookieTemplateInput): Promise<ServerCookieTemplate>;
493
- updateCookieTemplate(id: string, data: UpdateCookieTemplateInput): Promise<ServerCookieTemplate>;
494
- deleteCookieTemplate(id: string): Promise<void>;
495
- getCookieTemplate(id: string): Promise<ServerCookieTemplate | null>;
496
- getCookieTemplates(tenantId: string): Promise<ServerCookieTemplate[]>;
497
- copyCookieTemplate(id: string, newName: string): Promise<ServerCookieTemplate>;
730
+ createConsentTemplate(data: CreateConsentTemplateInput): Promise<ServerConsentTemplate>;
731
+ updateConsentTemplate(id: string, data: UpdateConsentTemplateInput): Promise<ServerConsentTemplate>;
732
+ deleteConsentTemplate(id: string): Promise<void>;
733
+ getConsentTemplate(id: string): Promise<ServerConsentTemplate | null>;
734
+ getConsentTemplates(tenantId: string): Promise<ServerConsentTemplate[]>;
735
+ copyConsentTemplate(id: string, newName: string): Promise<ServerConsentTemplate>;
498
736
  createUITemplate(data: CreateUITemplateInput): Promise<ServerUITemplate>;
499
737
  updateUITemplate(id: string, data: UpdateUITemplateInput): Promise<ServerUITemplate>;
500
738
  deleteUITemplate(id: string): Promise<void>;
@@ -520,7 +758,8 @@ interface StorageAdapter {
520
758
  assignPermissionToRole(roleId: string, permissionId: string): Promise<void>;
521
759
  revokePermissionFromRole(roleId: string, permissionId: string): Promise<void>;
522
760
  createLog(data: CreateAuditLogInput): Promise<void>;
523
- getLogs(filters: AuditFilters): Promise<AuditLog[]>;
761
+ getLogs(filters: AuditFilters): Promise<PagedResult<AuditLogSummary>>;
762
+ getAuditLogById(id: string): Promise<AuditLog | null>;
524
763
  }
525
764
  interface PluginContext {
526
765
  storage: StorageAdapter;
@@ -539,23 +778,54 @@ declare abstract class ConsentiServerPlugin {
539
778
  beforeUserCreate?(data: CreateUserInput): Promise<CreateUserInput>;
540
779
  afterUserCreate?(user: AdminUser): Promise<void>;
541
780
  }
781
+ /**
782
+ * The server's stored shape of `Profile.profileJson` — extends the shared `ProfileConfig`
783
+ * (`./ui`, also used by `apps/ui`'s local `ConsentiProfile`/`registerProfile()` authoring, which
784
+ * genuinely needs the full multi-locale `translations`/`localeContents` blob for its own
785
+ * no-backend use case and must stay untouched).
786
+ *
787
+ * On the server, only `defaultLocale`'s own resolved content is ever stored here —
788
+ * `mainBanner`/`gpcBanner`/`preferenceModal` (inherited from `ProfileConfig` as-is: template shape
789
+ * merged with the author's text, optional fields simply omitted rather than blank — never a
790
+ * multi-locale map). Every other locale's content lives only in its own per-version JSON file on
791
+ * disk (`profiles/{tenant}/{id}/{version}/{locale}.json`, see `ProfileService`), never in the DB
792
+ * row — `translations`/`localeContents` (both full multi-locale blobs) are dropped entirely.
793
+ */
794
+ interface StoredProfileJson extends Omit<ProfileConfig, 'translations' | 'localeContents'> {
795
+ /** Every locale this profile has content for — drives the dashboard's tab list. Content for any
796
+ * locale other than `defaultLocale` lives only in that locale's on-disk version file, not here. */
797
+ locales: string[];
798
+ }
542
799
 
543
800
  type ConsentStatus = 'granted' | 'denied' | 'objected';
544
801
  type ConsentValue = Record<string, ConsentStatus>;
802
+ type GpcMode = 'ignore' | 'honor' | 'strict';
803
+ type LegalBasis = 'mandatory' | 'consent' | 'legitimate_interest';
804
+ type CookiePurpose = typeof COOKIE_PURPOSE_IDS[number];
805
+ /**
806
+ * A single consent parameter (tracker/cookie definition).
807
+ * Keyed by its own id inside `Record<string, Cookie>` — the map key IS the id,
808
+ * there is no `id` field on the value itself.
809
+ */
545
810
  interface Cookie {
546
- id: string;
547
- legalBasis?: 'mandatory' | 'consent' | 'legitimate_interest';
548
- /** @deprecated use legalBasis: 'mandatory' */
549
- mandatory?: boolean;
550
- /** @deprecated use legalBasis */
551
- type?: 'consent' | 'legitimate_interest';
811
+ purpose?: CookiePurpose;
552
812
  listenGpc?: boolean;
553
- expiry?: number;
813
+ /**
814
+ * Pre-grant this parameter's default consent to 'granted' (instead of the
815
+ * compliance-group-driven default) when no stored decision exists yet.
816
+ * Only meaningful when the owning category's `legalBasis === 'consent'` —
817
+ * `mandatory`/`legitimate_interest` categories are already effectively
818
+ * pre-granted regardless of this flag. Never overrides an active GPC signal.
819
+ * Default: false.
820
+ */
821
+ preGrant?: boolean;
554
822
  tcfVendorId?: number;
555
823
  tcfPurposes?: number[];
556
824
  tcfSpecialFeatures?: number[];
557
825
  cpraCategory?: 'sale' | 'sharing' | 'sensitive';
558
826
  }
827
+ /** `Cookie` keyed by its own id — the standard shape used everywhere a parameter list is passed around. */
828
+ type CookieMap = Record<string, Cookie>;
559
829
  type ButtonStyle = 'primary' | 'secondary' | 'text' | 'accent';
560
830
  type ButtonAction = 'submit' | 'manage' | 'close' | 'custom' | 'link';
561
831
  interface Button {
@@ -564,8 +834,11 @@ interface Button {
564
834
  action: ButtonAction;
565
835
  cookies?: string[] | '*' | '!';
566
836
  url?: string;
567
- type?: string;
568
837
  }
838
+ /** `Button` keyed by its own id — e.g. `'accept-all'`. The map key IS the id, rendered as the DOM
839
+ * `id` (`consenti-btn-{id}`) so integrators can target specific buttons, and is what
840
+ * `profileOverride`'s `deepMerge` deletes by (`{ 'reject-optional': null }` removes one button). */
841
+ type ButtonMap = Record<string, Button>;
569
842
  interface MainBanner {
570
843
  position: 'top' | 'bottom' | 'middle' | 'left-bottom' | 'right-bottom';
571
844
  overlayOpacity?: number;
@@ -574,34 +847,100 @@ interface MainBanner {
574
847
  heading?: string;
575
848
  headingTag?: string;
576
849
  htmlText: string;
577
- buttons: Button[];
850
+ buttons: ButtonMap;
851
+ /** Stack buttons vertically below this viewport width in px. 0 or undefined = disabled. Default: 576. */
852
+ stackButtonsOnBreakpoint?: number;
853
+ /** Trap keyboard Tab focus within the consent root while this banner is visible. */
854
+ trapFocus?: boolean;
578
855
  }
579
856
  type GpcBanner = MainBanner;
857
+ /**
858
+ * A consent category — the single source of legal basis for every parameter
859
+ * it lists in `cookies`. Keyed by its own id inside `Record<string, Category>` —
860
+ * the map key IS the id, there is no `id` field on the value itself.
861
+ *
862
+ * A parameter must belong to exactly one category (enforced at authoring time,
863
+ * not by this type) — that relationship is what `getCookieLegalBasis`/
864
+ * `isMandatoryCookie` (`@consenti/utils`) resolve at runtime.
865
+ */
580
866
  interface Category {
581
- id: string;
582
867
  heading: string;
583
868
  headingTag?: string;
584
869
  htmlText: string;
585
- mandatory?: boolean;
586
- type?: 'consent' | 'legitimate_interest';
587
- legitimateInterest?: {
588
- enabled?: boolean;
589
- description?: string;
590
- };
870
+ legalBasis: LegalBasis;
871
+ /** Only meaningful when legalBasis === 'legitimate_interest' — the GDPR balancing-test justification shown to visitors. */
872
+ legitimateInterestDescription?: string;
873
+ /** IDs of the parameters (from the profile's `cookies` map) that belong to this category. */
591
874
  cookies: string[];
592
875
  }
876
+ /** `Category` keyed by its own id — the standard shape used everywhere a category list is passed around. */
877
+ type CategoryMap = Record<string, Category>;
593
878
  interface PreferenceModal extends Omit<MainBanner, "position"> {
594
879
  position?: 'left' | 'right' | 'center';
595
880
  subheading?: string;
596
- categories: Category[];
881
+ categories: CategoryMap;
597
882
  persistent?: boolean;
598
883
  /** Screen width in px below which the modal expands to full screen. Default: 576. Set to 0 to disable. */
599
884
  mobileFullScreenBreakpoint?: number;
885
+ /** Label for the optional consent-receipt download checkbox (shown when `allowReceipt` is true). Falls back to a default when omitted. */
886
+ receiptLabel?: string;
887
+ /** Description shown beneath the consent-receipt checkbox. Falls back to a default when omitted. */
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;
900
+ }
901
+ /**
902
+ * A UI-template button definition. Keyed by its own machine id (e.g. `'accept-all'`) inside
903
+ * `TemplateButtonMap` — the map key IS the id, not display text. UI templates own layout/behavior
904
+ * only, never text. The visitor-facing label is authored per-locale on the profile instead
905
+ * (`LocaleTextContent.*.buttonLabels`, keyed by the same button id) and composed into a resolved
906
+ * `Button.text` at profile-build time. Distinct from `Button` (the resolved-profile shape), which
907
+ * always carries real `text`.
908
+ */
909
+ interface TemplateButtonDef {
910
+ type?: ButtonStyle;
911
+ action: ButtonAction;
912
+ cookies?: string[] | '*' | '!';
913
+ url?: string;
914
+ }
915
+ /** `TemplateButtonDef` keyed by its own id — the standard shape everywhere a UI template's
916
+ * button set is passed around. */
917
+ type TemplateButtonMap = Record<string, TemplateButtonDef>;
918
+ interface TemplateBannerDef {
919
+ position: 'top' | 'bottom' | 'middle' | 'left-bottom' | 'right-bottom';
920
+ overlayOpacity?: number;
921
+ showClose?: boolean;
922
+ showLocaleSwitcher?: boolean;
923
+ headingTag?: string;
924
+ buttons: TemplateButtonMap;
925
+ stackButtonsOnBreakpoint?: number;
926
+ trapFocus?: boolean;
927
+ }
928
+ type TemplateGpcBannerDef = TemplateBannerDef;
929
+ interface TemplateModalDef extends Omit<TemplateBannerDef, 'position'> {
930
+ position?: 'left' | 'right' | 'center';
931
+ persistent?: boolean;
932
+ hasSubheading?: boolean;
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;
600
937
  }
601
938
  interface LocaleTranslations {
602
- mainBanner: MainBanner;
603
- gpcBanner?: GpcBanner;
604
- preferenceModal: PreferenceModal;
939
+ mainBanner: DeepPartial<MainBanner>;
940
+ gpcBanner?: DeepPartial<GpcBanner>;
941
+ preferenceModal: DeepPartial<PreferenceModal>;
942
+ /** Only meaningful when the profile's `ageGate.enabled` is true. */
943
+ ageGateModal?: DeepPartial<AgeGateModalContent>;
605
944
  }
606
945
  interface ProfileTranslations {
607
946
  [locale: string]: LocaleTranslations;
@@ -611,113 +950,201 @@ interface DpdpaConfig {
611
950
  grievanceEmail: string;
612
951
  purposeDescription?: string;
613
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
+ }
614
976
  interface LocaleTextContent {
615
977
  mainBanner: {
616
978
  heading?: string;
617
979
  htmlText: string;
618
- buttonLabels?: string[];
980
+ /** Keyed by the linked UI template's button id — was a positional array matched to
981
+ * `buttons[i]` by index; now matched by key, robust against button reordering. */
982
+ buttonLabels?: Record<string, string>;
619
983
  };
620
984
  gpcBanner: {
621
985
  heading?: string;
622
986
  htmlText: string;
623
- buttonLabels?: string[];
987
+ buttonLabels?: Record<string, string>;
624
988
  };
625
989
  preferenceModal: {
626
990
  heading: string;
627
991
  subheading?: string;
628
992
  htmlText?: string;
629
- buttonLabels?: string[];
630
- categories: {
631
- id: string;
993
+ buttonLabels?: Record<string, string>;
994
+ /** `legitimateInterestDescription` is only meaningful for categories whose legalBasis is 'legitimate_interest'; optional GDPR balancing-test text, translated per locale like heading/htmlText. */
995
+ categories: Record<string, {
632
996
  heading: string;
633
997
  htmlText: string;
634
- }[];
998
+ legitimateInterestDescription?: string;
999
+ }>;
1000
+ receiptLabel?: string;
1001
+ receiptDescription?: string;
1002
+ /** Per-locale override of `PreferenceModal.forgetMeLabel`; only meaningful when `showForgetMe` is true. */
1003
+ forgetMeLabel?: string;
635
1004
  };
1005
+ /** Only meaningful when the profile's `ageGate.enabled` is true. */
1006
+ ageGateModal?: AgeGateModalContent;
636
1007
  }
637
1008
  interface ProfileConfig {
638
- cookies?: Cookie[];
1009
+ id: string;
1010
+ cookies?: CookieMap;
639
1011
  defaultLocale: string;
1012
+ /** Days until consent expires and the visitor is asked again (profile-wide). Default: 365. */
1013
+ expiryDays?: number;
640
1014
  translations?: ProfileTranslations;
641
- gpcBanner?: boolean;
1015
+ mainBanner: MainBanner;
1016
+ gpcBanner?: GpcBanner;
1017
+ preferenceModal: PreferenceModal;
642
1018
  darkMode?: boolean;
643
1019
  allowedOrigins?: string[];
644
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;
645
1027
  regulation?: 'gdpr' | 'ccpa' | 'cpra' | 'dpdpa' | 'uk-gdpr' | 'lgpd' | 'pipeda' | 'popia' | 'pdpa-th' | 'appi' | 'kvkk';
646
1028
  regulations?: string[];
647
- cookieTemplateId?: string;
1029
+ consentTemplateId?: string;
648
1030
  uiTemplateId?: string;
649
1031
  localeContents?: Record<string, LocaleTextContent>;
650
- complianceGroup?: string;
651
- /** @deprecated use gpcMode */
652
- gpcCompliance?: 'strict' | 'honor' | 'ignore';
653
- gpcMode?: 'ignore' | 'honor' | 'strict';
1032
+ complianceGroup?: ComplianceGroupId;
1033
+ /**
1034
+ * Free-form identifier (lower-kebab-case) for profiles that don't map to one of the
1035
+ * built-in `complianceGroup` values — the identifier the widget's `compliance.type`
1036
+ * config targets to fetch this profile. Participates in activation, deactivation, and
1037
+ * "one active profile per group" conflict detection the same way `complianceGroup` does;
1038
+ * no `COMPLIANCE_GROUPS` validation rules or GPC defaults apply to it, since none exist
1039
+ * for a free-form name.
1040
+ */
1041
+ customComplianceGroup?: string;
1042
+ /**
1043
+ * Per-profile deltas applied on top of the consent template's authored `Cookie` values
1044
+ * (e.g. an overridden `preGrant`) when resolving this profile — see the dashboard's Step 2
1045
+ * pre-grant override UX. Keyed by cookie id; only the overridden fields need to be present.
1046
+ */
1047
+ cookiesOverride?: Record<string, Partial<Cookie>>;
1048
+ /**
1049
+ * Reserved for a future phase — stored but not yet applied when resolving a profile.
1050
+ * Intended to mirror `cookiesOverride` for per-category deltas.
1051
+ */
1052
+ categoriesOverride?: Record<string, Partial<Category>>;
1053
+ /**
1054
+ * Reserved for a future phase — stored but not yet applied when resolving a profile.
1055
+ * Intended to hold per-profile UI-template deltas.
1056
+ */
1057
+ uiOverride?: Record<string, unknown>;
1058
+ /**
1059
+ * When registered via `registerProfile()` and `complianceGroup` matches one of
1060
+ * Consenti's built-in groups: `false` (default) fully replaces the built-in
1061
+ * embedded profile for that group; `true` deep-merges this config onto the
1062
+ * built-in profile instead (only meaningful together with `complianceGroup`).
1063
+ */
1064
+ deepMerge?: boolean;
1065
+ gpcMode?: GpcMode;
654
1066
  isActive?: boolean;
655
1067
  hidePoweredBy?: boolean;
656
1068
  allowReceipt?: boolean;
657
1069
  /** Per-compliance extra config (e.g. DPDPA data fiduciary name). */
658
1070
  complianceConfig?: Record<string, string>;
1071
+ /** Show a metadata footer strip in the banner/modal with Consent ID, Date, Version, Privacy Settings link. */
1072
+ showFooterMetadata?: boolean;
1073
+ /** Apply WCAG 2.1 AA button sizing (44px min-height), visible focus rings, and screen-reader labels. */
1074
+ enhanceAccessibility?: boolean;
659
1075
  }
660
1076
  interface ServerUITemplate {
661
1077
  id: string;
662
1078
  tenantId: string;
663
1079
  name: string;
664
- mainBanner: MainBanner;
665
- gpcBanner: GpcBanner;
666
- preferenceModal: PreferenceModal;
1080
+ mainBanner: TemplateBannerDef;
1081
+ gpcBanner: TemplateGpcBannerDef;
1082
+ preferenceModal: TemplateModalDef;
667
1083
  createdAt: string;
668
1084
  updatedAt: string;
669
1085
  }
670
- interface ServerCookieTemplate {
1086
+ interface ServerConsentTemplate {
671
1087
  id: string;
672
1088
  tenantId: string;
673
1089
  name: string;
674
- cookies: Cookie[];
1090
+ cookies: CookieMap;
1091
+ categories: CategoryMap;
675
1092
  createdAt: string;
676
1093
  updatedAt: string;
677
1094
  }
678
- interface CreateCookieTemplateInput {
1095
+ interface CreateConsentTemplateInput {
679
1096
  tenantId: string;
680
1097
  name: string;
681
- cookies: Cookie[];
1098
+ cookies: CookieMap;
1099
+ categories: CategoryMap;
682
1100
  }
683
- interface UpdateCookieTemplateInput {
1101
+ interface UpdateConsentTemplateInput {
684
1102
  name?: string;
685
- cookies?: Cookie[];
1103
+ cookies?: CookieMap;
1104
+ categories?: CategoryMap;
686
1105
  }
687
1106
  interface CreateUITemplateInput {
688
1107
  tenantId: string;
689
1108
  name: string;
690
- mainBanner: MainBanner;
691
- gpcBanner: MainBanner;
692
- preferenceModal: PreferenceModal;
1109
+ mainBanner: TemplateBannerDef;
1110
+ gpcBanner: TemplateGpcBannerDef;
1111
+ preferenceModal: TemplateModalDef;
693
1112
  }
694
1113
  interface UpdateUITemplateInput {
695
1114
  name?: string;
696
- mainBanner?: MainBanner;
697
- gpcBanner?: MainBanner;
698
- preferenceModal?: PreferenceModal;
1115
+ mainBanner?: TemplateBannerDef;
1116
+ gpcBanner?: TemplateGpcBannerDef;
1117
+ preferenceModal?: TemplateModalDef;
699
1118
  }
700
1119
  interface PublicProfileResponse {
701
1120
  id: string;
1121
+ /** Incremented in place on every save of this profile. */
1122
+ version: number;
702
1123
  name?: string;
703
1124
  tenantId?: string;
704
- version: number;
705
1125
  defaultLocale: string;
706
1126
  currentLocale: string;
707
1127
  locales: string[];
708
- cookies: Cookie[];
1128
+ cookies: CookieMap;
1129
+ /** Days until consent expires and the visitor is asked again (profile-wide). */
1130
+ expiryDays?: number;
709
1131
  mainBanner: MainBanner;
710
1132
  gpcBanner?: GpcBanner;
711
1133
  preferenceModal: PreferenceModal;
712
1134
  resolvedComplianceGroup?: string;
713
- complianceGroup?: string;
1135
+ complianceGroup?: ComplianceGroupId;
714
1136
  compliances?: string[];
715
1137
  complianceConfig?: Record<string, string>;
716
1138
  allowedOrigins?: string[];
717
- gpcMode?: boolean | 'ignore' | 'honor' | 'strict';
1139
+ gpcMode?: GpcMode;
718
1140
  hidePoweredBy?: boolean;
719
1141
  allowReceipt?: boolean;
720
1142
  darkMode?: boolean;
1143
+ dpdpa?: DpdpaConfig;
1144
+ ageGate?: AgeGateConfig;
1145
+ ageGateModal?: AgeGateModalContent;
1146
+ showFooterMetadata?: boolean;
1147
+ enhanceAccessibility?: boolean;
721
1148
  createdAt?: string;
722
1149
  updatedAt?: string;
723
1150
  }
@@ -737,7 +1164,8 @@ declare class AuditRepo {
737
1164
  private adapter;
738
1165
  constructor(adapter: StorageAdapter);
739
1166
  log(data: CreateAuditLogInput): Promise<void>;
740
- list(filters: AuditFilters): Promise<AuditLog[]>;
1167
+ list(filters: AuditFilters): Promise<PagedResult<AuditLogSummary>>;
1168
+ get(id: string): Promise<AuditLog | null>;
741
1169
  }
742
1170
 
743
1171
  declare class LocaleJsonCacheService {
@@ -760,23 +1188,118 @@ declare class ProfileService {
760
1188
  private profilesDir?;
761
1189
  private handleCache?;
762
1190
  private s3Config?;
763
- constructor(profiles: ProfileRepo, audit: AuditRepo, tenantId?: string, storage?: StorageAdapter | undefined, eventBus?: EventEmitter | undefined, localeCache?: LocaleJsonCacheService | undefined, profilesDir?: string | undefined, handleCache?: ((paths: string[], version: number, isPurge: boolean) => void) | undefined, s3Config?: S3ApiConfig | undefined);
764
- private profileDir;
1191
+ constructor(profiles: ProfileRepo, audit: AuditRepo, tenantId?: string, storage?: StorageAdapter | undefined, eventBus?: EventEmitter | undefined, localeCache?: LocaleJsonCacheService | undefined, profilesDir?: string | undefined, handleCache?: ((paths: string[], profileId: string, isPurge: boolean) => void) | undefined, s3Config?: S3ApiConfig | undefined);
1192
+ /** Root directory holding every version snapshot of one logical profile, keyed by its stable id. */
1193
+ private versionsRootDir;
765
1194
  private complianceGroupDir;
1195
+ /**
1196
+ * The directory key used for activation/hot-serve writes and the "one active profile
1197
+ * per group" conflict check — the real `complianceGroup` when set, otherwise the
1198
+ * free-form `customComplianceGroup` (which the widget's `compliance.type` targets the
1199
+ * same way). Compliance *validation* still only ever runs against a real `complianceGroup`.
1200
+ */
1201
+ private groupKey;
766
1202
  private profileFilePaths;
1203
+ /** Resolves this profile's cookie map (consent-template lookup + `cookiesOverride` applied) —
1204
+ * the one piece of content still resolved live rather than baked at save time, since cookies
1205
+ * are small and never the source of the row-size problem `mainBanner`/`preferenceModal` were. */
1206
+ private resolveCookies;
1207
+ /**
1208
+ * Writes every locale's on-disk version file for one save (create or an edit) in a single
1209
+ * atomic worker call — default locale content comes from `profile.profileJson` itself, every
1210
+ * other touched locale from `localeContent`. A locale already in `profileJson.locales` but not
1211
+ * present in `localeContent` (a tab the user never opened this session) is carried forward
1212
+ * unchanged from `previousVersion` by the worker — never silently dropped.
1213
+ */
1214
+ private writeLocaleFiles;
767
1215
  create(input: Omit<CreateProfileInput, 'tenantId'>): Promise<Profile>;
1216
+ /**
1217
+ * Duplicates a profile as a brand new, always-inactive profile (own `id`, `version` starts at
1218
+ * 1 again). Delegates to {@link create} rather than copying on-disk files directly — the
1219
+ * resolved-JSON snapshot embeds its own `id`/`version`, so it has to be regenerated for the
1220
+ * new profile rather than byte-copied from the source's. Non-default locale content is read
1221
+ * back off the source's current version files so the copy carries every locale forward too.
1222
+ */
1223
+ copy(id: string, name?: string): Promise<Profile>;
1224
+ /**
1225
+ * Mutates the existing row in place — `id` is stable across every edit, `version` is
1226
+ * incremented. The prior state isn't kept as a separate DB row; its resolved-JSON snapshot
1227
+ * on disk (see {@link listVersions}) is the audit trail for that edit.
1228
+ */
768
1229
  update(id: string, input: UpdateProfileInput): Promise<Profile>;
769
1230
  delete(id: string): Promise<void>;
770
1231
  activate(id: string): Promise<Profile>;
771
1232
  deactivate(id: string): Promise<Profile>;
772
- private runWrite;
773
- get(id: string, locale?: string): Promise<Profile | null>;
1233
+ private runDeactivate;
1234
+ get(id: string): Promise<Profile | null>;
1235
+ /**
1236
+ * Resolves one locale's full public response. The default locale's content lives directly on
1237
+ * the DB row (`profileJson.mainBanner`/`gpcBanner`/`preferenceModal`) — no template lookups, no
1238
+ * live resolution. Any other locale's content lives only in that locale's on-disk version file;
1239
+ * this reads it directly, falling back to the default locale's content if the file is missing
1240
+ * (e.g. a locale listed but never actually authored yet). This method is not a hot path — the
1241
+ * public `/profiles/:tenant/:group/:locale` route serves the static file directly and only
1242
+ * falls back to this for previewing an inactive/versioned profile.
1243
+ */
774
1244
  getResolved(id: string, locale?: string): Promise<PublicProfileResponse | null>;
775
1245
  findActiveByComplianceGroup(complianceGroup: string): Promise<Profile | null>;
776
1246
  list(): Promise<Profile[]>;
777
1247
  listSummary(): Promise<ProfileSummary[]>;
778
- listVersions(profileId: string): ProfileVersionEntry[];
779
- getVersionFile(profileId: string, version: number, locale: string): string | null;
1248
+ /**
1249
+ * Lists every version snapshot of `profileId` on disk (newest first), read straight from
1250
+ * the version-directory tree — no DB query, `Profile.version` is just the current pointer.
1251
+ * Works for archived profiles too (DB row deleted, files still on disk) — the directory
1252
+ * tree is the only thing this reads.
1253
+ */
1254
+ listVersions(profileId: string): Promise<ProfileVersionEntry[]>;
1255
+ /** Works for archived profiles too — see `listVersions()`. */
1256
+ getVersionFile(profileId: string, version: string, locale: string): Promise<string | null>;
1257
+ /**
1258
+ * Profile-id directories on disk with no matching DB row — deleted profiles whose version
1259
+ * snapshots `delete()` never removes. Directory-only read (id, version count, last-modified
1260
+ * mtime) — no file content is opened here; that only happens once a specific version is
1261
+ * requested via `getVersionFile()`, which works for these ids too.
1262
+ */
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;
1278
+ /**
1279
+ * Seeds a default profile for a compliance group from the embedded English profile in
1280
+ * `@consenti/utils`, merged with locale text overlays for de/es/fr/ja. Every locale — including
1281
+ * non-English ones — is passed through `create()`'s normal `localeContent` path, the same as an
1282
+ * author-submitted profile; there is no separate "seeded profile" code path for locale content
1283
+ * anymore, which is what previously let a locale silently never make it into the served file.
1284
+ * Only creates the profile if no active profile already exists for the compliance group.
1285
+ * Safe to call repeatedly (idempotent).
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
+ *
1295
+ * @param complianceGroup - One of the 8 compliance group IDs.
1296
+ */
1297
+ seedDefaultProfile(complianceGroup: string): Promise<void>;
1298
+ /**
1299
+ * Seeds default profiles for all 8 compliance groups.
1300
+ * Skips any group that already has an active profile (idempotent).
1301
+ */
1302
+ seedAllDefaults(): Promise<void>;
780
1303
  }
781
1304
 
782
1305
  declare class ConsentRepo {
@@ -786,7 +1309,7 @@ declare class ConsentRepo {
786
1309
  update(visitorId: string, data: UpdateConsentInput): Promise<ConsentDbRecord>;
787
1310
  delete(visitorId: string): Promise<void>;
788
1311
  get(visitorId: string): Promise<ConsentDbRecord | null>;
789
- list(filters: ConsentFilters): Promise<ConsentDbRecord[]>;
1312
+ list(filters: ConsentFilters): Promise<PagedResult<ConsentSummary>>;
790
1313
  }
791
1314
 
792
1315
  declare class VisitorRepo {
@@ -822,7 +1345,25 @@ declare class ConsentService {
822
1345
  private pluginEngine?;
823
1346
  private eventBus?;
824
1347
  private tcfConfig?;
825
- constructor(consents: ConsentRepo, visitors: VisitorRepo, profiles: ProfileRepo, audit: AuditRepo, tenantId?: string, compliance?: ComplianceConfig | undefined, pluginEngine?: PluginEngine | undefined, eventBus?: EventEmitter | undefined, tcfConfig?: TcfConfig | undefined);
1348
+ private storage?;
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);
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;
1357
+ /**
1358
+ * Profiles created via the dashboard wizard reference a consent template (`consentTemplateId`)
1359
+ * rather than embedding `cookies`/`preferenceModal.categories` directly on `profileJson` — those
1360
+ * fields are only populated for the embedded/seeded default profiles (see
1361
+ * `ProfileService.seedDefaultProfile`). Cookie/category membership and `legalBasis` are
1362
+ * locale-independent, so the raw consent template's data is sufficient here — no need to
1363
+ * resolve per-locale text via `ProfileService.getResolved()`. Mirrors the same fallback
1364
+ * `ProfileService.getResolved()` and the admin route validators already apply.
1365
+ */
1366
+ private resolveTemplateData;
826
1367
  create(input: Omit<CreateConsentInput, 'tenantId'>): Promise<ConsentDbRecord>;
827
1368
  update(visitorId: string, input: UpdateConsentInput): Promise<ConsentDbRecord>;
828
1369
  get(visitorId: string): Promise<ConsentDbRecord | null>;
@@ -834,7 +1375,8 @@ declare class VisitorService {
834
1375
  private visitors;
835
1376
  private tenantId;
836
1377
  private eventBus?;
837
- constructor(visitors: VisitorRepo, tenantId?: string, eventBus?: EventEmitter | undefined);
1378
+ private dataSigningHash;
1379
+ constructor(visitors: VisitorRepo, tenantId?: string, eventBus?: EventEmitter | undefined, dataSigningHash?: string);
838
1380
  upsert(data: {
839
1381
  visitorId: string;
840
1382
  ip?: string;
@@ -904,19 +1446,23 @@ declare class BetterSqlite3Adapter implements StorageAdapter {
904
1446
  getProfiles(tenantId: string): Promise<Profile[]>;
905
1447
  findActiveProfileByComplianceGroup(tenantId: string, complianceGroup: string): Promise<Profile | null>;
906
1448
  private mapConsent;
1449
+ private static readonly CONSENT_SUMMARY_COLS;
1450
+ private mapConsentSummary;
907
1451
  createConsent(data: CreateConsentInput): Promise<ConsentDbRecord>;
908
1452
  updateConsent(visitorId: string, data: UpdateConsentInput): Promise<ConsentDbRecord>;
909
1453
  deleteConsent(visitorId: string): Promise<void>;
910
1454
  getConsent(visitorId: string): Promise<ConsentDbRecord | null>;
911
- getConsents(filters: ConsentFilters): Promise<ConsentDbRecord[]>;
1455
+ getConsents(filters: ConsentFilters): Promise<PagedResult<ConsentSummary>>;
912
1456
  streamConsents(filters: ConsentFilters): AsyncIterable<ConsentDbRecord>;
913
1457
  getConsentHistory(visitorId: string): Promise<ConsentHistoryEntry[]>;
1458
+ createNoticeShown(data: CreateNoticeShownInput): Promise<NoticeShownRecord>;
1459
+ getNoticeShownForVisitor(visitorId: string): Promise<NoticeShownRecord[]>;
914
1460
  private mapVisitor;
915
1461
  createVisitor(data: CreateVisitorInput): Promise<Visitor>;
916
1462
  updateVisitor(visitorId: string, data: UpdateVisitorInput): Promise<Visitor>;
917
1463
  deleteVisitor(visitorId: string): Promise<void>;
918
1464
  getVisitor(visitorId: string): Promise<Visitor | null>;
919
- getVisitors(filters: VisitorFilters): Promise<Visitor[]>;
1465
+ getVisitors(filters: VisitorFilters): Promise<PagedResult<Visitor>>;
920
1466
  private mapUser;
921
1467
  createUser(data: CreateUserInput): Promise<AdminUser>;
922
1468
  updateUser(id: string, data: UpdateUserInput): Promise<AdminUser>;
@@ -937,7 +1483,10 @@ declare class BetterSqlite3Adapter implements StorageAdapter {
937
1483
  assignRole(userId: string, roleId: string, tenantId?: string): Promise<void>;
938
1484
  revokeRole(userId: string, roleId: string, tenantId?: string): Promise<void>;
939
1485
  createLog(data: CreateAuditLogInput): Promise<void>;
940
- getLogs(filters: AuditFilters): Promise<AuditLog[]>;
1486
+ getLogs(filters: AuditFilters): Promise<PagedResult<AuditLogSummary>>;
1487
+ getAuditLogById(id: string): Promise<AuditLog | null>;
1488
+ private static readonly AUDIT_SUMMARY_COLS;
1489
+ private mapAuditSummary;
941
1490
  private mapAuditLog;
942
1491
  streamAuditLogs(filters: AuditFilters): AsyncIterable<AuditLog>;
943
1492
  getOverviewStats(tenantId: string): Promise<OverviewStats>;
@@ -949,19 +1498,23 @@ declare class BetterSqlite3Adapter implements StorageAdapter {
949
1498
  createTenant(data: CreateTenantInput): Promise<Tenant>;
950
1499
  updateTenant(id: string, data: UpdateTenantInput): Promise<Tenant>;
951
1500
  deleteTenant(id: string): Promise<void>;
1501
+ getSettings(tenantId: string): Promise<TenantSettings>;
1502
+ updateSettings(tenantId: string, data: Partial<TenantSettings>): Promise<TenantSettings>;
952
1503
  purgeExpiredConsents(olderThanDays: number): Promise<number>;
953
1504
  private mapApiKey;
954
1505
  createApiKey(data: CreateApiKeyInput): Promise<ApiKey>;
955
1506
  getApiKeyByHash(keyHash: string): Promise<ApiKey | null>;
956
1507
  revokeApiKey(id: string): Promise<void>;
1508
+ reactivateApiKey(id: string): Promise<void>;
1509
+ deleteApiKey(id: string): Promise<void>;
957
1510
  getApiKeys(tenantId: string): Promise<ApiKey[]>;
958
- private mapCookieTemplate;
959
- createCookieTemplate(data: CreateCookieTemplateInput): Promise<ServerCookieTemplate>;
960
- updateCookieTemplate(id: string, data: UpdateCookieTemplateInput): Promise<ServerCookieTemplate>;
961
- deleteCookieTemplate(id: string): Promise<void>;
962
- getCookieTemplate(id: string): Promise<ServerCookieTemplate | null>;
963
- getCookieTemplates(tenantId: string): Promise<ServerCookieTemplate[]>;
964
- copyCookieTemplate(id: string, newName: string): Promise<ServerCookieTemplate>;
1511
+ private mapConsentTemplate;
1512
+ createConsentTemplate(data: CreateConsentTemplateInput): Promise<ServerConsentTemplate>;
1513
+ updateConsentTemplate(id: string, data: UpdateConsentTemplateInput): Promise<ServerConsentTemplate>;
1514
+ deleteConsentTemplate(id: string): Promise<void>;
1515
+ getConsentTemplate(id: string): Promise<ServerConsentTemplate | null>;
1516
+ getConsentTemplates(tenantId: string): Promise<ServerConsentTemplate[]>;
1517
+ copyConsentTemplate(id: string, newName: string): Promise<ServerConsentTemplate>;
965
1518
  private mapUITemplate;
966
1519
  createUITemplate(data: CreateUITemplateInput): Promise<ServerUITemplate>;
967
1520
  updateUITemplate(id: string, data: UpdateUITemplateInput): Promise<ServerUITemplate>;
@@ -972,7 +1525,7 @@ declare class BetterSqlite3Adapter implements StorageAdapter {
972
1525
  private mapProfileSummary;
973
1526
  private readonly PROFILE_SUMMARY_SQL;
974
1527
  listProfilesSummary(tenantId: string): Promise<ProfileSummary[]>;
975
- findProfilesUsingCookieTemplate(templateId: string): Promise<ProfileSummary[]>;
1528
+ findProfilesUsingConsentTemplate(templateId: string): Promise<ProfileSummary[]>;
976
1529
  findProfilesUsingUITemplate(templateId: string): Promise<ProfileSummary[]>;
977
1530
  getOptInStats(tenantId: string, filters: OptInFilters): Promise<OptInStats>;
978
1531
  }
@@ -998,16 +1551,18 @@ declare class MongoDBAdapter implements StorageAdapter {
998
1551
  updateConsent(visitorId: string, data: UpdateConsentInput): Promise<ConsentDbRecord>;
999
1552
  deleteConsent(visitorId: string): Promise<void>;
1000
1553
  getConsent(visitorId: string): Promise<ConsentDbRecord | null>;
1001
- getConsents(filters: ConsentFilters): Promise<ConsentDbRecord[]>;
1554
+ getConsents(filters: ConsentFilters): Promise<PagedResult<ConsentSummary>>;
1002
1555
  streamConsents(filters: ConsentFilters): AsyncIterable<ConsentDbRecord>;
1003
1556
  private buildConsentFilter;
1004
1557
  private writeHistory;
1005
1558
  getConsentHistory(visitorId: string): Promise<ConsentHistoryEntry[]>;
1559
+ createNoticeShown(data: CreateNoticeShownInput): Promise<NoticeShownRecord>;
1560
+ getNoticeShownForVisitor(visitorId: string): Promise<NoticeShownRecord[]>;
1006
1561
  createVisitor(data: CreateVisitorInput): Promise<Visitor>;
1007
1562
  updateVisitor(visitorId: string, data: UpdateVisitorInput): Promise<Visitor>;
1008
1563
  deleteVisitor(visitorId: string): Promise<void>;
1009
1564
  getVisitor(visitorId: string): Promise<Visitor | null>;
1010
- getVisitors(filters: VisitorFilters): Promise<Visitor[]>;
1565
+ getVisitors(filters: VisitorFilters): Promise<PagedResult<Visitor>>;
1011
1566
  getOverviewStats(tenantId: string): Promise<OverviewStats>;
1012
1567
  getCategoryStats(tenantId: string): Promise<CategoryStats>;
1013
1568
  getTimeline(tenantId: string, days?: number): Promise<TimelineEntry[]>;
@@ -1030,26 +1585,32 @@ declare class MongoDBAdapter implements StorageAdapter {
1030
1585
  assignPermissionToRole(roleId: string, permissionId: string): Promise<void>;
1031
1586
  revokePermissionFromRole(roleId: string, permissionId: string): Promise<void>;
1032
1587
  createLog(data: CreateAuditLogInput): Promise<void>;
1033
- getLogs(filters: AuditFilters): Promise<AuditLog[]>;
1588
+ getLogs(filters: AuditFilters): Promise<PagedResult<AuditLogSummary>>;
1589
+ getAuditLogById(id: string): Promise<AuditLog | null>;
1034
1590
  streamAuditLogs(filters: AuditFilters): AsyncIterable<AuditLog>;
1035
1591
  private buildAuditFilter;
1036
1592
  getCountries(tenantId: string): Promise<CountryStat[]>;
1037
1593
  getGpcStats(tenantId: string): Promise<GpcStats>;
1038
1594
  getTenants(): Promise<Tenant[]>;
1595
+ private mapApiKey;
1039
1596
  createApiKey(data: CreateApiKeyInput): Promise<ApiKey>;
1040
1597
  getApiKeyByHash(keyHash: string): Promise<ApiKey | null>;
1041
1598
  revokeApiKey(id: string): Promise<void>;
1599
+ reactivateApiKey(id: string): Promise<void>;
1600
+ deleteApiKey(id: string): Promise<void>;
1042
1601
  getApiKeys(tenantId: string): Promise<ApiKey[]>;
1043
1602
  createTenant(data: CreateTenantInput): Promise<Tenant>;
1044
1603
  updateTenant(id: string, data: UpdateTenantInput): Promise<Tenant>;
1045
1604
  deleteTenant(id: string): Promise<void>;
1605
+ getSettings(tenantId: string): Promise<TenantSettings>;
1606
+ updateSettings(tenantId: string, data: Partial<TenantSettings>): Promise<TenantSettings>;
1046
1607
  purgeExpiredConsents(olderThanDays: number): Promise<number>;
1047
- createCookieTemplate(): Promise<never>;
1048
- updateCookieTemplate(): Promise<never>;
1049
- deleteCookieTemplate(): Promise<void>;
1050
- getCookieTemplate(): Promise<never>;
1051
- getCookieTemplates(): Promise<never>;
1052
- copyCookieTemplate(): Promise<never>;
1608
+ createConsentTemplate(): Promise<never>;
1609
+ updateConsentTemplate(): Promise<never>;
1610
+ deleteConsentTemplate(): Promise<void>;
1611
+ getConsentTemplate(): Promise<never>;
1612
+ getConsentTemplates(): Promise<never>;
1613
+ copyConsentTemplate(): Promise<never>;
1053
1614
  createUITemplate(): Promise<never>;
1054
1615
  updateUITemplate(): Promise<never>;
1055
1616
  deleteUITemplate(): Promise<void>;
@@ -1057,7 +1618,7 @@ declare class MongoDBAdapter implements StorageAdapter {
1057
1618
  getUITemplates(): Promise<never>;
1058
1619
  copyUITemplate(): Promise<never>;
1059
1620
  listProfilesSummary(): Promise<never[]>;
1060
- findProfilesUsingCookieTemplate(): Promise<never[]>;
1621
+ findProfilesUsingConsentTemplate(): Promise<never[]>;
1061
1622
  findProfilesUsingUITemplate(): Promise<never[]>;
1062
1623
  getOptInStats(): Promise<never>;
1063
1624
  }
@@ -1082,16 +1643,18 @@ declare class MySQLAdapter implements StorageAdapter {
1082
1643
  updateConsent(visitorId: string, data: UpdateConsentInput): Promise<ConsentDbRecord>;
1083
1644
  deleteConsent(visitorId: string): Promise<void>;
1084
1645
  getConsent(visitorId: string): Promise<ConsentDbRecord | null>;
1085
- getConsents(filters: ConsentFilters): Promise<ConsentDbRecord[]>;
1646
+ getConsents(filters: ConsentFilters): Promise<PagedResult<ConsentSummary>>;
1086
1647
  streamConsents(filters: ConsentFilters): AsyncIterable<ConsentDbRecord>;
1087
- private buildConsentQuery;
1648
+ private buildConsentWhere;
1088
1649
  private writeHistory;
1089
1650
  getConsentHistory(visitorId: string): Promise<ConsentHistoryEntry[]>;
1651
+ createNoticeShown(data: CreateNoticeShownInput): Promise<NoticeShownRecord>;
1652
+ getNoticeShownForVisitor(visitorId: string): Promise<NoticeShownRecord[]>;
1090
1653
  createVisitor(data: CreateVisitorInput): Promise<Visitor>;
1091
1654
  updateVisitor(visitorId: string, data: UpdateVisitorInput): Promise<Visitor>;
1092
1655
  deleteVisitor(visitorId: string): Promise<void>;
1093
1656
  getVisitor(visitorId: string): Promise<Visitor | null>;
1094
- getVisitors(filters: VisitorFilters): Promise<Visitor[]>;
1657
+ getVisitors(filters: VisitorFilters): Promise<PagedResult<Visitor>>;
1095
1658
  getOverviewStats(tenantId: string): Promise<OverviewStats>;
1096
1659
  getCategoryStats(tenantId: string): Promise<CategoryStats>;
1097
1660
  getTimeline(tenantId: string, days?: number): Promise<TimelineEntry[]>;
@@ -1114,26 +1677,32 @@ declare class MySQLAdapter implements StorageAdapter {
1114
1677
  assignPermissionToRole(roleId: string, permissionId: string): Promise<void>;
1115
1678
  revokePermissionFromRole(roleId: string, permissionId: string): Promise<void>;
1116
1679
  createLog(data: CreateAuditLogInput): Promise<void>;
1117
- getLogs(filters: AuditFilters): Promise<AuditLog[]>;
1680
+ getLogs(filters: AuditFilters): Promise<PagedResult<AuditLogSummary>>;
1681
+ getAuditLogById(id: string): Promise<AuditLog | null>;
1118
1682
  streamAuditLogs(filters: AuditFilters): AsyncIterable<AuditLog>;
1119
- private buildAuditQuery;
1683
+ private buildAuditWhere;
1120
1684
  getCountries(tenantId: string): Promise<CountryStat[]>;
1121
1685
  getGpcStats(tenantId: string): Promise<GpcStats>;
1122
1686
  getTenants(): Promise<Tenant[]>;
1687
+ private mapApiKey;
1123
1688
  createApiKey(data: CreateApiKeyInput): Promise<ApiKey>;
1124
1689
  getApiKeyByHash(keyHash: string): Promise<ApiKey | null>;
1125
1690
  revokeApiKey(id: string): Promise<void>;
1691
+ reactivateApiKey(id: string): Promise<void>;
1692
+ deleteApiKey(id: string): Promise<void>;
1126
1693
  getApiKeys(tenantId: string): Promise<ApiKey[]>;
1127
1694
  createTenant(data: CreateTenantInput): Promise<Tenant>;
1128
1695
  updateTenant(id: string, data: UpdateTenantInput): Promise<Tenant>;
1696
+ getSettings(tenantId: string): Promise<TenantSettings>;
1697
+ updateSettings(tenantId: string, data: Partial<TenantSettings>): Promise<TenantSettings>;
1129
1698
  deleteTenant(id: string): Promise<void>;
1130
1699
  purgeExpiredConsents(olderThanDays: number): Promise<number>;
1131
- createCookieTemplate(): Promise<never>;
1132
- updateCookieTemplate(): Promise<never>;
1133
- deleteCookieTemplate(): Promise<void>;
1134
- getCookieTemplate(): Promise<never>;
1135
- getCookieTemplates(): Promise<never>;
1136
- copyCookieTemplate(): Promise<never>;
1700
+ createConsentTemplate(): Promise<never>;
1701
+ updateConsentTemplate(): Promise<never>;
1702
+ deleteConsentTemplate(): Promise<void>;
1703
+ getConsentTemplate(): Promise<never>;
1704
+ getConsentTemplates(): Promise<never>;
1705
+ copyConsentTemplate(): Promise<never>;
1137
1706
  createUITemplate(): Promise<never>;
1138
1707
  updateUITemplate(): Promise<never>;
1139
1708
  deleteUITemplate(): Promise<void>;
@@ -1141,7 +1710,7 @@ declare class MySQLAdapter implements StorageAdapter {
1141
1710
  getUITemplates(): Promise<never>;
1142
1711
  copyUITemplate(): Promise<never>;
1143
1712
  listProfilesSummary(): Promise<never[]>;
1144
- findProfilesUsingCookieTemplate(): Promise<never[]>;
1713
+ findProfilesUsingConsentTemplate(): Promise<never[]>;
1145
1714
  findProfilesUsingUITemplate(): Promise<never[]>;
1146
1715
  getOptInStats(): Promise<never>;
1147
1716
  }
@@ -1165,16 +1734,18 @@ declare class PostgreSQLAdapter implements StorageAdapter {
1165
1734
  updateConsent(visitorId: string, data: UpdateConsentInput): Promise<ConsentDbRecord>;
1166
1735
  deleteConsent(visitorId: string): Promise<void>;
1167
1736
  getConsent(visitorId: string): Promise<ConsentDbRecord | null>;
1168
- getConsents(filters: ConsentFilters): Promise<ConsentDbRecord[]>;
1737
+ getConsents(filters: ConsentFilters): Promise<PagedResult<ConsentSummary>>;
1169
1738
  streamConsents(filters: ConsentFilters): AsyncIterable<ConsentDbRecord>;
1170
- private buildConsentQuery;
1739
+ private buildConsentWhere;
1171
1740
  private writeHistory;
1172
1741
  getConsentHistory(visitorId: string): Promise<ConsentHistoryEntry[]>;
1742
+ createNoticeShown(data: CreateNoticeShownInput): Promise<NoticeShownRecord>;
1743
+ getNoticeShownForVisitor(visitorId: string): Promise<NoticeShownRecord[]>;
1173
1744
  createVisitor(data: CreateVisitorInput): Promise<Visitor>;
1174
1745
  updateVisitor(visitorId: string, data: UpdateVisitorInput): Promise<Visitor>;
1175
1746
  deleteVisitor(visitorId: string): Promise<void>;
1176
1747
  getVisitor(visitorId: string): Promise<Visitor | null>;
1177
- getVisitors(filters: VisitorFilters): Promise<Visitor[]>;
1748
+ getVisitors(filters: VisitorFilters): Promise<PagedResult<Visitor>>;
1178
1749
  getOverviewStats(tenantId: string): Promise<OverviewStats>;
1179
1750
  getCategoryStats(tenantId: string): Promise<CategoryStats>;
1180
1751
  getTimeline(tenantId: string, days?: number): Promise<TimelineEntry[]>;
@@ -1197,26 +1768,32 @@ declare class PostgreSQLAdapter implements StorageAdapter {
1197
1768
  assignPermissionToRole(roleId: string, permissionId: string): Promise<void>;
1198
1769
  revokePermissionFromRole(roleId: string, permissionId: string): Promise<void>;
1199
1770
  createLog(data: CreateAuditLogInput): Promise<void>;
1200
- getLogs(filters: AuditFilters): Promise<AuditLog[]>;
1771
+ getLogs(filters: AuditFilters): Promise<PagedResult<AuditLogSummary>>;
1772
+ getAuditLogById(id: string): Promise<AuditLog | null>;
1201
1773
  streamAuditLogs(filters: AuditFilters): AsyncIterable<AuditLog>;
1202
- private buildAuditQuery;
1774
+ private buildAuditWhere;
1203
1775
  getCountries(tenantId: string): Promise<CountryStat[]>;
1204
1776
  getGpcStats(tenantId: string): Promise<GpcStats>;
1205
1777
  getTenants(): Promise<Tenant[]>;
1778
+ private mapApiKey;
1206
1779
  createApiKey(data: CreateApiKeyInput): Promise<ApiKey>;
1207
1780
  getApiKeyByHash(keyHash: string): Promise<ApiKey | null>;
1208
1781
  revokeApiKey(id: string): Promise<void>;
1782
+ reactivateApiKey(id: string): Promise<void>;
1783
+ deleteApiKey(id: string): Promise<void>;
1209
1784
  getApiKeys(tenantId: string): Promise<ApiKey[]>;
1210
1785
  createTenant(data: CreateTenantInput): Promise<Tenant>;
1211
1786
  updateTenant(id: string, data: UpdateTenantInput): Promise<Tenant>;
1212
1787
  deleteTenant(id: string): Promise<void>;
1788
+ getSettings(tenantId: string): Promise<TenantSettings>;
1789
+ updateSettings(tenantId: string, data: Partial<TenantSettings>): Promise<TenantSettings>;
1213
1790
  purgeExpiredConsents(olderThanDays: number): Promise<number>;
1214
- createCookieTemplate(): Promise<never>;
1215
- updateCookieTemplate(): Promise<never>;
1216
- deleteCookieTemplate(): Promise<void>;
1217
- getCookieTemplate(): Promise<never>;
1218
- getCookieTemplates(): Promise<never>;
1219
- copyCookieTemplate(): Promise<never>;
1791
+ createConsentTemplate(): Promise<never>;
1792
+ updateConsentTemplate(): Promise<never>;
1793
+ deleteConsentTemplate(): Promise<void>;
1794
+ getConsentTemplate(): Promise<never>;
1795
+ getConsentTemplates(): Promise<never>;
1796
+ copyConsentTemplate(): Promise<never>;
1220
1797
  createUITemplate(): Promise<never>;
1221
1798
  updateUITemplate(): Promise<never>;
1222
1799
  deleteUITemplate(): Promise<void>;
@@ -1224,11 +1801,131 @@ declare class PostgreSQLAdapter implements StorageAdapter {
1224
1801
  getUITemplates(): Promise<never>;
1225
1802
  copyUITemplate(): Promise<never>;
1226
1803
  listProfilesSummary(): Promise<never[]>;
1227
- findProfilesUsingCookieTemplate(): Promise<never[]>;
1804
+ findProfilesUsingConsentTemplate(): Promise<never[]>;
1228
1805
  findProfilesUsingUITemplate(): Promise<never[]>;
1229
1806
  getOptInStats(): Promise<never>;
1230
1807
  }
1231
1808
 
1809
+ /**
1810
+ * ConsentAction — server-side counterpart to `@consenti/ui`'s `ConsentAction`.
1811
+ *
1812
+ * Watches a single consent parameter across *every* visitor's submission (not just one
1813
+ * browser's own state) and fires `onGrant`/`onDeny` when that parameter's status actually
1814
+ * changes — via the same `eventBus` `createConsenti()` already emits `consent:created`/
1815
+ * `consent:updated` on. Useful for server-side integrations (CRM sync, suppression lists,
1816
+ * downstream webhooks) that need to react to consent decisions without polling the database.
1817
+ *
1818
+ * There is no server equivalent of the widget's `ConsentScript` — injecting a `<script>` tag
1819
+ * only makes sense in a browser DOM the server doesn't have. `services.consent.create()`/
1820
+ * `.update()` are already the server-side "submit"/"update consent" methods this hook reacts to.
1821
+ *
1822
+ * @example
1823
+ * ```ts
1824
+ * import { createConsenti, ConsentAction } from '@consenti/api'
1825
+ *
1826
+ * const { eventBus } = createConsenti({ ... })
1827
+ *
1828
+ * new ConsentAction({
1829
+ * id: 'analytics_storage',
1830
+ * eventBus,
1831
+ * onGrant: ({ visitorId }) => crm.optIn(visitorId),
1832
+ * onDeny: ({ visitorId }) => crm.optOut(visitorId),
1833
+ * })
1834
+ * ```
1835
+ */
1836
+
1837
+ /** Passed to `onGrant`/`onDeny` — the transitioning parameter's status plus the full consent record. */
1838
+ interface ConsentActionParams {
1839
+ visitorId: string;
1840
+ cookieId: string;
1841
+ status: ConsentStatus;
1842
+ record: ConsentDbRecord;
1843
+ }
1844
+ /** Options for constructing a {@link ConsentAction} instance. */
1845
+ interface ConsentActionOptions {
1846
+ /** The consent parameter ID to watch (e.g. `'analytics_storage'`). */
1847
+ id: string;
1848
+ /** The `eventBus` returned by `createConsenti()`. */
1849
+ eventBus: EventEmitter;
1850
+ /** Called when the parameter's status transitions to `'granted'`, for any visitor. */
1851
+ onGrant?: (params: ConsentActionParams) => void;
1852
+ /** Called when the parameter's status transitions away from `'granted'` (`'denied'` or `'objected'`). */
1853
+ onDeny?: (params: ConsentActionParams) => void;
1854
+ }
1855
+ /**
1856
+ * Watches a single consent parameter across every incoming `consent:created`/`consent:updated`
1857
+ * event and fires `onGrant`/`onDeny` on transitions. Call {@link ConsentAction.destroy} to
1858
+ * remove the event listeners when the hook is no longer needed.
1859
+ */
1860
+ declare class ConsentAction {
1861
+ private readonly options;
1862
+ private readonly onCreated;
1863
+ private readonly onUpdated;
1864
+ constructor(options: ConsentActionOptions);
1865
+ private fire;
1866
+ /** Removes the event listeners. Call when the hook is no longer needed. */
1867
+ destroy(): void;
1868
+ }
1869
+
1870
+ /**
1871
+ * CategoryAction — server-side counterpart to `@consenti/ui`'s `CategoryAction`.
1872
+ *
1873
+ * Same idea as {@link ConsentAction} but watches a whole category's rollup consent state
1874
+ * (granted only when *every* cookie in the category is granted) instead of one parameter —
1875
+ * matching the preference modal's tri-state rollup rule and the widget's `CategoryAction`.
1876
+ *
1877
+ * @example
1878
+ * ```ts
1879
+ * import { createConsenti, CategoryAction } from '@consenti/api'
1880
+ *
1881
+ * const { eventBus, services } = createConsenti({ ... })
1882
+ *
1883
+ * new CategoryAction({
1884
+ * categoryId: 'marketing',
1885
+ * eventBus,
1886
+ * profiles: services.profile,
1887
+ * onGrant: ({ visitorId }) => adsPlatform.optIn(visitorId),
1888
+ * onDeny: ({ visitorId }) => adsPlatform.optOut(visitorId),
1889
+ * })
1890
+ * ```
1891
+ */
1892
+
1893
+ /** Passed to `onGrant`/`onDeny` — the category's rollup status plus the full consent record. */
1894
+ interface CategoryActionParams {
1895
+ visitorId: string;
1896
+ categoryId: string;
1897
+ status: ConsentStatus;
1898
+ record: ConsentDbRecord;
1899
+ }
1900
+ /** Options for constructing a {@link CategoryAction} instance. */
1901
+ interface CategoryActionOptions {
1902
+ /** The category ID to watch (e.g. `'marketing'`). */
1903
+ categoryId: string;
1904
+ /** The `eventBus` returned by `createConsenti()`. */
1905
+ eventBus: EventEmitter;
1906
+ /** The `services.profile` returned by `createConsenti()` — resolves which cookies belong to
1907
+ * this category for the record's profile (categories are defined on the consent template). */
1908
+ profiles: ProfileService;
1909
+ /** Called when every cookie in the category transitions to `'granted'`. */
1910
+ onGrant?: (params: CategoryActionParams) => void;
1911
+ /** Called when the category transitions away from fully granted. */
1912
+ onDeny?: (params: CategoryActionParams) => void;
1913
+ }
1914
+ /**
1915
+ * Watches a category's rollup consent status across every incoming `consent:created`/
1916
+ * `consent:updated` event and fires `onGrant`/`onDeny` on transitions. Call
1917
+ * {@link CategoryAction.destroy} to remove the event listeners when no longer needed.
1918
+ */
1919
+ declare class CategoryAction {
1920
+ private readonly options;
1921
+ private readonly onCreated;
1922
+ private readonly onUpdated;
1923
+ constructor(options: CategoryActionOptions);
1924
+ private fire;
1925
+ /** Removes the event listeners. Call when the hook is no longer needed. */
1926
+ destroy(): void;
1927
+ }
1928
+
1232
1929
  declare const dashboardDir: string;
1233
1930
  declare function createConsenti(userConfig: ConsentiServerConfig): {
1234
1931
  router: (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => void;
@@ -1256,4 +1953,4 @@ declare function createConsenti(userConfig: ConsentiServerConfig): {
1256
1953
  destroy: () => Promise<void>;
1257
1954
  };
1258
1955
 
1259
- export { type AdminUser, type AuditLog, type BrandingConfig, type ConsentDbRecord, type ConsentVerifyResult, type ConsentiServerConfig, MongoDBAdapter, MySQLAdapter, type Permission, PostgreSQLAdapter, type Profile, type Role, BetterSqlite3Adapter as SQLiteAdapter, type SafeAdminUser, type StorageAdapter, type Visitor, createConsenti, dashboardDir };
1956
+ export { type AdminUser, type AuditLog, type BrandingConfig, CategoryAction, type CategoryActionOptions, type CategoryActionParams, ConsentAction, type ConsentActionOptions, type ConsentActionParams, type ConsentDbRecord, type ConsentVerifyResult, type ConsentiServerConfig, MongoDBAdapter, MySQLAdapter, type Permission, PostgreSQLAdapter, type Profile, type Role, BetterSqlite3Adapter as SQLiteAdapter, type SafeAdminUser, type StorageAdapter, type Visitor, createConsenti, dashboardDir };