@consenti/api 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +917 -207
  2. package/dist/chunk-IY4L6MNB.js +2 -0
  3. package/dist/chunk-IY4L6MNB.js.map +1 -0
  4. package/dist/chunk-O5UG4OYI.js +2 -0
  5. package/dist/chunk-O5UG4OYI.js.map +1 -0
  6. package/dist/dashboard/assets/ApiConfig-CE-Noelz.js +1 -0
  7. package/dist/dashboard/assets/ArchivedProfileList-Bw-fx-BD.js +1 -0
  8. package/dist/dashboard/assets/AuditLog-D6HiTQ1o.js +1 -0
  9. package/dist/dashboard/assets/ChangePassword-D2rPkzD1.js +1 -0
  10. package/dist/dashboard/assets/ConfirmDialog-DTtw7VJ-.js +1 -0
  11. package/dist/dashboard/assets/ConsentList-BWOvPOMG.js +1 -0
  12. package/dist/dashboard/assets/ConsentTemplateEditor-BKIEnLmA.js +1 -0
  13. package/dist/dashboard/assets/ConsentTemplateList-DCys2nM3.js +1 -0
  14. package/dist/dashboard/assets/HeadingTagInput-BTjrdzlS.js +1 -0
  15. package/dist/dashboard/assets/HowItWorks-AeyjfQGO.js +1 -0
  16. package/dist/dashboard/assets/PermissionGate-Bhhf7SMh.js +1 -0
  17. package/dist/dashboard/assets/PreviewPane-Dq5K1h2L.js +35 -0
  18. package/dist/dashboard/assets/ProfileEditor-BXtaE0tL.js +2 -0
  19. package/dist/dashboard/assets/ProfileList-CWKSAgu0.js +1 -0
  20. package/dist/dashboard/assets/ProfileVersionHistory-1h8sUgog.js +3 -0
  21. package/dist/dashboard/assets/RecordDetailModal-FaFhzQXi.js +1 -0
  22. package/dist/dashboard/assets/Reports-DWEjQOKh.js +1 -0
  23. package/dist/dashboard/assets/RoleList-BSGoqlzp.js +1 -0
  24. package/dist/dashboard/assets/Select-DjJNwlbY.js +1 -0
  25. package/dist/dashboard/assets/Settings-CVYrlyLm.js +1 -0
  26. package/dist/dashboard/assets/SetupWizard-4NTb5bZj.js +1 -0
  27. package/dist/dashboard/assets/Table-DH_jh8_N.js +1 -0
  28. package/dist/dashboard/assets/TenantList-5AwFtoHf.js +1 -0
  29. package/dist/dashboard/assets/UITemplateEditor-9A55scBe.js +1 -0
  30. package/dist/dashboard/assets/UITemplateList-BS7X4u4F.js +1 -0
  31. package/dist/dashboard/assets/UserList-SPkMyFRy.js +1 -0
  32. package/dist/dashboard/assets/VendorList-C5fPhr6U.js +1 -0
  33. package/dist/dashboard/assets/VisitorList-COP1SyNd.js +1 -0
  34. package/dist/dashboard/assets/check-o5cp1s2J.js +1 -0
  35. package/dist/dashboard/assets/client-ymc9nwWr.js +1 -0
  36. package/dist/dashboard/assets/copy-C01KoUUH.js +1 -0
  37. package/dist/dashboard/assets/createLucideIcon-MdjI3m3u.js +1 -0
  38. package/dist/dashboard/assets/de-DET92Jb5.js +5 -0
  39. package/dist/dashboard/assets/debounce-DlvAAT2V.js +1 -0
  40. package/dist/dashboard/assets/dist-DlhLroYC.js +1 -0
  41. package/dist/dashboard/assets/es-CJNunrnU.js +5 -0
  42. package/dist/dashboard/assets/external-link-DVe00ZBK.js +1 -0
  43. package/dist/dashboard/assets/fr-akfTuf5x.js +5 -0
  44. package/dist/dashboard/assets/history-CHTts_m2.js +1 -0
  45. package/dist/dashboard/assets/index-DW3qOrry.css +2 -0
  46. package/dist/dashboard/assets/index-H_Zm0X-u.js +6 -0
  47. package/dist/dashboard/assets/ja-B0nLWkOd.js +5 -0
  48. package/dist/dashboard/assets/profiles-DmrdGv7t.js +1 -0
  49. package/dist/dashboard/assets/roles-C0dJPFl4.js +1 -0
  50. package/dist/dashboard/assets/search-Bd2RdDRh.js +1 -0
  51. package/dist/dashboard/assets/templates-B2xiivh4.js +1 -0
  52. package/dist/dashboard/assets/templates-CnMoLavN.js +1 -0
  53. package/dist/dashboard/assets/trash-2-BBnkdgm_.js +1 -0
  54. package/dist/dashboard/assets/trash-Dj1jhzHJ.js +1 -0
  55. package/dist/dashboard/assets/useHashQuery-B5ytMIMp.js +1 -0
  56. package/dist/dashboard/assets/users-czK8LRVh.js +1 -0
  57. package/dist/dashboard/assets/x-BRuyBEQP.js +1 -0
  58. package/dist/dashboard/assets/zh-CN-Dxg3G-BL.js +5 -0
  59. package/dist/dashboard/index.html +4 -2
  60. package/dist/dist-Z5RVTVBT.js +2 -0
  61. package/dist/dist-Z5RVTVBT.js.map +1 -0
  62. package/dist/index.cjs +129 -9089
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +963 -254
  65. package/dist/index.d.ts +963 -254
  66. package/dist/index.js +129 -9043
  67. package/dist/index.js.map +1 -1
  68. package/dist/profiles-XTYPWLVC.js +2 -0
  69. package/dist/profiles-XTYPWLVC.js.map +1 -0
  70. package/dist/widget/widget.css +1 -0
  71. package/dist/widget/widget.js +1 -2304
  72. package/package.json +19 -12
  73. package/dist/dashboard/assets/index-BBbhHyc_.js +0 -2637
  74. package/dist/dashboard/assets/index-DYK73WL5.css +0 -1
package/dist/index.d.ts CHANGED
@@ -1,205 +1,71 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { IncomingMessage, ServerResponse } from 'node:http';
3
3
 
4
- type ConsentStatus = 'granted' | 'denied' | 'objected';
5
- type ConsentValue = Record<string, ConsentStatus>;
6
- interface Cookie {
7
- id: string;
8
- type?: 'consent' | 'legitimate_interest';
9
- mandatory?: boolean;
10
- listenGpc?: boolean;
11
- expiry?: number;
12
- tcfVendorId?: number;
13
- tcfPurposes?: number[];
14
- cpraCategory?: 'sale' | 'sharing' | 'sensitive';
15
- }
16
- type ButtonStyle = 'primary' | 'secondary' | 'text' | 'accent';
17
- type ButtonAction = 'submit' | 'manage' | 'close' | 'custom' | 'link';
18
- interface Button {
19
- text: string;
20
- style: ButtonStyle;
21
- action: ButtonAction;
22
- cookies?: string[] | '*' | '!';
23
- url?: string;
24
- }
25
- interface MainBanner {
26
- position: 'top' | 'bottom' | 'middle' | 'left-bottom' | 'right-bottom';
27
- overlayOpacity?: number;
28
- showClose?: boolean;
29
- showLocaleSwitcher?: boolean;
30
- heading?: string;
31
- headingTag?: string;
32
- htmlText: string;
33
- buttons: Button[];
34
- }
35
- type GpcBanner = MainBanner;
36
- interface Category {
37
- id: string;
38
- heading: string;
39
- headingTag?: string;
40
- htmlText: string;
41
- mandatory?: boolean;
42
- type?: 'consent' | 'legitimate_interest';
43
- legitimateInterest?: {
44
- enabled?: boolean;
45
- description?: string;
46
- };
47
- cookies: string[];
48
- }
49
- interface PreferenceModal {
50
- position?: 'left' | 'right' | 'center';
51
- overlayOpacity?: number;
52
- showClose?: boolean;
53
- showLocaleSwitcher?: boolean;
54
- heading?: string;
55
- headingTag?: string;
56
- subheading?: string;
57
- htmlText?: string;
58
- buttons: Button[];
59
- categories: Category[];
60
- persistent?: boolean;
61
- }
62
- interface LocaleTranslations {
63
- mainBanner: MainBanner;
64
- gpcBanner?: GpcBanner;
65
- preferenceModal: PreferenceModal;
66
- }
67
- interface ProfileTranslations {
68
- [locale: string]: LocaleTranslations;
69
- }
70
- interface DpdpaConfig {
71
- dataFiduciary: string;
72
- grievanceEmail: string;
73
- purposeDescription?: string;
74
- }
75
- interface LocaleTextContent {
76
- mainBanner: {
77
- heading: string;
78
- htmlText: string;
79
- buttonLabels: string[];
80
- };
81
- gpcBanner: {
82
- heading: string;
83
- htmlText: string;
84
- buttonLabels: string[];
85
- };
86
- preferenceModal: {
87
- heading: string;
88
- subheading?: string;
89
- htmlText?: string;
90
- buttonLabels: string[];
91
- categories: Array<{
92
- id: string;
93
- heading: string;
94
- htmlText: string;
95
- }>;
96
- };
97
- }
98
- interface ProfileConfig {
99
- cookies?: Cookie[];
100
- defaultLocale: string;
101
- translations?: ProfileTranslations;
102
- gpcBanner?: boolean;
103
- darkMode?: boolean;
104
- allowedOrigins?: string[];
105
- dpdpa?: DpdpaConfig;
106
- regulation?: 'gdpr' | 'ccpa' | 'cpra' | 'dpdpa' | 'uk-gdpr' | 'lgpd' | 'pipeda' | 'popia' | 'pdpa-th' | 'appi' | 'kvkk';
107
- regulations?: string[];
108
- cookieTemplateId?: string;
109
- uiTemplateId?: string;
110
- localeContents?: Record<string, LocaleTextContent>;
111
- }
112
- interface UITemplateButton {
113
- text: string;
114
- type: string;
115
- action: 'submit' | 'custom' | 'manage' | 'close' | 'link';
116
- cookies?: string | string[];
117
- url?: string;
118
- }
119
- interface UITemplateBanner {
120
- position: string;
121
- overlayOpacity: number;
122
- showClose: boolean;
123
- showLocaleSwitcher?: boolean;
124
- headingTag: string;
125
- buttons: UITemplateButton[];
126
- }
127
- interface UITemplateCategory {
128
- id: string;
129
- headingTag: string;
130
- mandatory: boolean;
131
- type: 'consent' | 'legitimate_interest';
132
- liEnabled: boolean;
133
- cookies: string[];
134
- }
135
- interface UITemplateModal extends UITemplateBanner {
136
- hasSubheading: boolean;
137
- persistent: boolean;
138
- showLocaleSwitcher?: boolean;
139
- categories: UITemplateCategory[];
140
- }
141
- interface ServerUITemplate {
142
- id: string;
143
- tenantId: string;
144
- name: string;
145
- mainBanner: UITemplateBanner;
146
- gpcBanner: UITemplateBanner;
147
- preferenceModal: UITemplateModal;
148
- createdAt: string;
149
- updatedAt: string;
150
- }
151
- interface ServerCookieTemplate {
152
- id: string;
153
- tenantId: string;
154
- name: string;
155
- cookies: Cookie[];
156
- createdAt: string;
157
- updatedAt: string;
158
- }
159
- interface CreateCookieTemplateInput {
160
- tenantId: string;
161
- name: string;
162
- cookies: Cookie[];
163
- }
164
- interface UpdateCookieTemplateInput {
165
- name?: string;
166
- cookies?: Cookie[];
167
- }
168
- interface CreateUITemplateInput {
169
- tenantId: string;
170
- name: string;
171
- mainBanner: UITemplateBanner;
172
- gpcBanner: UITemplateBanner;
173
- preferenceModal: UITemplateModal;
174
- }
175
- interface UpdateUITemplateInput {
176
- name?: string;
177
- mainBanner?: UITemplateBanner;
178
- gpcBanner?: UITemplateBanner;
179
- preferenceModal?: UITemplateModal;
180
- }
181
- interface PublicProfileResponse {
182
- id: string;
183
- version: number;
184
- defaultLocale: string;
185
- currentLocale: string;
186
- locales: string[];
187
- cookies: Cookie[];
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
+ 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
+ declare const COOKIE_PURPOSE_IDS: readonly ["necessary", "functional", "preferences", "analytics", "marketing"];
18
+
19
+ /**
20
+ * Compliance-related literal types, derived from the regulation/group ID data
21
+ * that lives in @consenti/utils (packages/utils/src/compliance.ts).
22
+ *
23
+ * This import is type-only and fully erased at compile time — @consenti/types
24
+ * has no runtime dependency on @consenti/utils. The actual data (COMPLIANCE_GROUPS,
25
+ * EMBEDDED_COMPLIANCE_MAP, COMPLIANCE_VALIDATION_RULES, GPC_OPTIONS, etc.) lives in
26
+ * @consenti/utils; import it from there.
27
+ *
28
+ * HOW TO ADD A NEW REGULATION OR COMPLIANCE GROUP: see the header comment in
29
+ * packages/utils/src/compliance.ts — these types auto-update from that data.
30
+ */
31
+
32
+ type ComplianceGroupId = typeof COMPLIANCE_GROUP_IDS[number];
33
+
34
+ /** Resolved content for one non-default locale — template shape merged with that locale's
35
+ * author text, computed client-side (dashboard) the same way the default locale's content on
36
+ * `StoredProfileJson` is. Never persisted in the DB row; `ProfileService` writes it straight to
37
+ * that locale's on-disk version file. Keyed by locale code on `CreateProfileInput`/
38
+ * `UpdateProfileInput.localeContent` — only locales actually touched this save need an entry;
39
+ * any other locale already in `profileJson.locales` is carried forward unchanged from the
40
+ * previous version. */
41
+ interface LocaleContentInput {
188
42
  mainBanner: MainBanner;
189
43
  gpcBanner?: GpcBanner;
190
44
  preferenceModal: PreferenceModal;
191
45
  }
192
-
193
46
  interface StorageConfig {
194
47
  driver: 'sqlite' | 'better-sqlite3' | 'node-sqlite3-wasm' | 'node:sqlite' | 'mongodb' | 'mysql' | 'postgresql' | 'json';
195
48
  path?: string;
196
49
  uri?: string;
197
- dbName?: string;
198
50
  database?: string;
199
51
  host?: string;
200
52
  port?: number;
201
53
  user?: string;
202
54
  password?: string;
55
+ /** Postgres/MySQL only. Max pool connections. Default: 10 (both drivers' own default — set
56
+ * explicitly here so it's a documented, tunable value rather than an implicit library default). */
57
+ poolMax?: number;
58
+ /**
59
+ * Postgres/MySQL only. Milliseconds before an in-flight query is aborted, so one slow/runaway
60
+ * query can't hold a pool connection indefinitely. Unset by default: a forced default could
61
+ * silently break legitimate long-running operations (e.g. exporting millions of records) that
62
+ * this project has no way to load-test — opt in once you've measured your own worst-case query.
63
+ */
64
+ statementTimeoutMs?: number;
65
+ /** Postgres only. Milliseconds a client may sit idle inside an open transaction before being
66
+ * terminated — protects against a `BEGIN` with no matching `COMMIT`/`ROLLBACK` holding a
67
+ * connection forever. Unset by default, same reasoning as `statementTimeoutMs`. */
68
+ idleInTransactionTimeoutMs?: number;
203
69
  }
204
70
  interface OidcConfig {
205
71
  issuer: string;
@@ -231,10 +97,112 @@ interface RateLimitConfig {
231
97
  windowMs?: number;
232
98
  maxRequests?: number;
233
99
  }
100
+ type GeoResult = {
101
+ country: string | null;
102
+ region: string | null;
103
+ locale: string | null;
104
+ };
105
+ type CountryResolverFn = (ctx: {
106
+ ip: string;
107
+ language: string;
108
+ timezone: string;
109
+ }) => Promise<GeoResult>;
110
+ interface S3ApiConfig {
111
+ enabled: boolean;
112
+ region: string;
113
+ bucketName: string;
114
+ accessKeyId: string;
115
+ secretAccessKey: string;
116
+ sessionToken?: string;
117
+ }
118
+ interface ProfileSummary {
119
+ id: string;
120
+ name: string;
121
+ defaultLocale: string;
122
+ complianceGroup: ComplianceGroupId | null;
123
+ customComplianceGroup: string | null;
124
+ isActive: boolean;
125
+ consentTemplateName: string | null;
126
+ uiTemplateName: string | null;
127
+ createdAt: string;
128
+ updatedAt: string;
129
+ }
130
+ /**
131
+ * One snapshot in a profile's edit history. `Profile.id` is stable across edits; each save
132
+ * increments `Profile.version` in place and writes a new snapshot under that version number.
133
+ */
134
+ interface ProfileVersionEntry {
135
+ version: number;
136
+ createdAt: string;
137
+ locales: string[];
138
+ }
139
+ /**
140
+ * A profile-id directory found on disk with no matching DB row — the profile was deleted
141
+ * (or predates the current versioning model) but its version-snapshot history wasn't. Built
142
+ * from a directory listing only (id, version count, last-modified) — no file content is read
143
+ * until a specific version is opened via the normal version-history endpoints.
144
+ */
145
+ interface ArchivedProfileSummary {
146
+ id: string;
147
+ versionCount: number;
148
+ lastModified: string;
149
+ }
150
+ interface OptInFilters {
151
+ profileId?: string;
152
+ complianceGroup?: string;
153
+ from?: string;
154
+ to?: string;
155
+ locale?: string;
156
+ }
157
+ interface OptInByLocale {
158
+ total: number;
159
+ granted: number;
160
+ denied: number;
161
+ managed: number;
162
+ }
163
+ interface OptInByDate {
164
+ date: string;
165
+ total: number;
166
+ granted: number;
167
+ denied: number;
168
+ managed: number;
169
+ }
170
+ interface OptInStats {
171
+ total: number;
172
+ granted: number;
173
+ denied: number;
174
+ managed: number;
175
+ grantedPct: number;
176
+ deniedPct: number;
177
+ managedPct: number;
178
+ byLocale: Record<string, OptInByLocale>;
179
+ byDate: OptInByDate[];
180
+ }
181
+ type ComplianceMapOverride = 'default' | ComplianceMapData | 'auto';
182
+ interface ComplianceMapData {
183
+ version: string;
184
+ countries: Record<string, CountryComplianceEntry>;
185
+ }
186
+ interface CountryComplianceEntry {
187
+ complianceGroup: ComplianceGroupId;
188
+ default: ComplianceGroupId;
189
+ description: string;
190
+ overriddenRegions?: Record<string, {
191
+ complianceGroup: ComplianceGroupId;
192
+ description: string;
193
+ }>;
194
+ }
234
195
  interface ComplianceConfig {
196
+ /** @deprecated use type */
235
197
  gdpr?: boolean;
198
+ /** @deprecated use type */
236
199
  ccpa?: boolean;
237
- gpc?: boolean;
200
+ gpc?: boolean | 'strict';
201
+ 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;
238
206
  }
239
207
  interface DashboardConfig {
240
208
  enabled?: boolean;
@@ -255,6 +223,23 @@ interface TcfConfig {
255
223
  }
256
224
  interface DataRetentionConfig {
257
225
  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
+ }
232
+ interface BrandingConfig {
233
+ /** Display name shown in the dashboard (login page, header, browser tab). Default: 'Consenti'. */
234
+ appName?: string;
235
+ /**
236
+ * Logo to display in the dashboard. Accepts a public URL (https://…) or a local
237
+ * filesystem path — when a path is given, the file is copied into the dashboard
238
+ * dist and served as a static asset automatically.
239
+ */
240
+ appLogoPath?: string;
241
+ /** When true, hides the "Powered by Consenti" badge in the dashboard. Default: false. */
242
+ hidePoweredBy?: boolean;
258
243
  }
259
244
  interface ConsentiServerConfig {
260
245
  basePath?: string;
@@ -270,6 +255,15 @@ interface ConsentiServerConfig {
270
255
  dataRetention?: DataRetentionConfig;
271
256
  maxBodySize?: number;
272
257
  trustedProxies?: string[];
258
+ branding?: BrandingConfig;
259
+ 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
+ /** Called after every profile activate/deactivate/delete. isPurge=true means invalidate; false means warm. */
266
+ handleCache?: (paths: string[], profileId: string, isPurge: boolean) => void;
273
267
  }
274
268
  interface Tenant {
275
269
  id: string;
@@ -278,21 +272,42 @@ interface Tenant {
278
272
  createdAt: string;
279
273
  updatedAt: string;
280
274
  }
275
+ /** Tenant-wide dashboard-managed defaults, shown on the API Config page. */
276
+ interface TenantSettings {
277
+ /** Public API origin allowlist. Distinct from `ProfileConfig.allowedOrigins`, which the
278
+ * origin check on `POST /consent` prefers when a profile sets its own list; this is the
279
+ * fallback. The public API has no auth token, so this is its only access gate. */
280
+ allowedOrigins?: string[];
281
+ /** 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
283
+ * (Bearer token auth alone still applies); only enforced when the request carries an
284
+ * `Origin` header — server-to-server callers are unaffected. */
285
+ adminAllowedOrigins?: string[];
286
+ /** Whether this tenant has completed (or skipped) the first-run setup wizard. Gates the
287
+ * one-time `#/setup` redirect in the dashboard — never reset once true. Default: false. */
288
+ setupCompleted?: boolean;
289
+ }
281
290
  interface ApiKey {
282
291
  id: string;
283
292
  tenantId: string;
284
293
  keyHash: string;
285
294
  name: string;
286
295
  isActive: boolean;
296
+ /** User id of the admin who created this key. Undefined for keys created before this field existed. */
297
+ createdBy?: string;
298
+ /** Optional expiry set at creation. Checked lazily at auth time and on list — no background sweep. */
299
+ expireBy?: string;
287
300
  createdAt: string;
301
+ updatedAt?: string;
288
302
  }
289
303
  interface Profile {
290
304
  id: string;
291
305
  tenantId: string;
292
306
  name: string;
293
307
  defaultLocale: string;
308
+ /** 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. */
294
309
  version: number;
295
- profileJson: ProfileConfig;
310
+ profileJson: StoredProfileJson;
296
311
  createdAt: string;
297
312
  updatedAt: string;
298
313
  }
@@ -301,7 +316,6 @@ interface ConsentDbRecord {
301
316
  tenantId: string;
302
317
  visitorId: string;
303
318
  profileId: string;
304
- profileVersion: number;
305
319
  locale: string;
306
320
  consentJson: ConsentValue;
307
321
  gpcDetected: boolean;
@@ -309,6 +323,10 @@ interface ConsentDbRecord {
309
323
  ageVerified?: boolean;
310
324
  parentalConsentToken?: string;
311
325
  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`). */
329
+ signature?: string;
312
330
  createdAt: string;
313
331
  updatedAt: string;
314
332
  }
@@ -343,6 +361,8 @@ interface AdminUser {
343
361
  isActive: boolean;
344
362
  totpEnabled?: boolean;
345
363
  totpSecret?: string;
364
+ /** Empty array = access to all tenants. Non-empty = restricted to listed tenant IDs. */
365
+ allowedTenants?: string[];
346
366
  createdAt: string;
347
367
  updatedAt: string;
348
368
  }
@@ -357,6 +377,22 @@ interface Permission {
357
377
  name: string;
358
378
  description?: string;
359
379
  }
380
+ /** Proof that the consent banner was rendered to a visitor — recorded even if they never
381
+ * interact with it, distinct from (and never merged into) an actual consent decision. */
382
+ interface NoticeShownRecord {
383
+ id: string;
384
+ tenantId: string;
385
+ visitorId: string;
386
+ profileId: string;
387
+ locale: string;
388
+ createdAt: string;
389
+ }
390
+ interface CreateNoticeShownInput {
391
+ tenantId: string;
392
+ visitorId: string;
393
+ profileId: string;
394
+ locale: string;
395
+ }
360
396
  interface AuditLog {
361
397
  id: string;
362
398
  tenantId: string;
@@ -368,22 +404,38 @@ interface AuditLog {
368
404
  newData?: unknown;
369
405
  createdAt: string;
370
406
  }
407
+ /** 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'>;
412
+ /** List-view shape for audit logs — omits `oldData`/`newData` (full before/after snapshots) for
413
+ * the same reason as `ConsentSummary`. Fetch the full `AuditLog` via `getAuditLogById(id)` when a
414
+ * single entry's full detail is needed. */
415
+ type AuditLogSummary = Omit<AuditLog, 'oldData' | 'newData'>;
371
416
  interface CreateProfileInput {
372
417
  tenantId: string;
373
418
  name: string;
374
419
  defaultLocale: string;
375
- profileJson: ProfileConfig;
420
+ profileJson: StoredProfileJson;
421
+ /** Non-default locales' resolved content for this save — see {@link LocaleContentInput}. */
422
+ localeContent?: Record<string, LocaleContentInput>;
376
423
  }
377
424
  interface UpdateProfileInput {
378
425
  name?: string;
379
426
  defaultLocale?: string;
380
- profileJson?: ProfileConfig;
427
+ profileJson?: StoredProfileJson;
428
+ /** 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. */
429
+ version?: number;
430
+ /** Non-default locales' resolved content for this save — see {@link LocaleContentInput}. Any
431
+ * locale in `profileJson.locales` not present here is carried forward unchanged from the
432
+ * previous version's on-disk file. */
433
+ localeContent?: Record<string, LocaleContentInput>;
381
434
  }
382
435
  interface CreateConsentInput {
383
436
  tenantId: string;
384
437
  visitorId: string;
385
438
  profileId: string;
386
- profileVersion: number;
387
439
  locale: string;
388
440
  consentJson: ConsentValue;
389
441
  gpcDetected: boolean;
@@ -391,11 +443,13 @@ interface CreateConsentInput {
391
443
  ageVerified?: boolean;
392
444
  parentalConsentToken?: string;
393
445
  tcfString?: string;
446
+ signature?: string;
394
447
  }
395
448
  interface UpdateConsentInput {
396
449
  consentJson: ConsentValue;
397
450
  locale?: string;
398
451
  gpcDetected?: boolean;
452
+ signature?: string;
399
453
  }
400
454
  interface CreateVisitorInput {
401
455
  tenantId: string;
@@ -417,6 +471,7 @@ interface CreateUserInput {
417
471
  name: string;
418
472
  email: string;
419
473
  passwordHash: string;
474
+ allowedTenants?: string[];
420
475
  }
421
476
  interface UpdateUserInput {
422
477
  name?: string;
@@ -425,6 +480,7 @@ interface UpdateUserInput {
425
480
  isActive?: boolean;
426
481
  totpSecret?: string | null;
427
482
  totpEnabled?: boolean;
483
+ allowedTenants?: string[];
428
484
  }
429
485
  interface CreateAuditLogInput {
430
486
  tenantId: string;
@@ -442,6 +498,8 @@ interface ConsentFilters {
442
498
  to?: string;
443
499
  page?: number;
444
500
  limit?: number;
501
+ /** Free-text search across visitorId, profileId, locale, source. */
502
+ q?: string;
445
503
  }
446
504
  interface VisitorFilters {
447
505
  tenantId: string;
@@ -449,6 +507,8 @@ interface VisitorFilters {
449
507
  to?: string;
450
508
  page?: number;
451
509
  limit?: number;
510
+ /** Free-text search across visitorId, country. */
511
+ q?: string;
452
512
  }
453
513
  interface AuditFilters {
454
514
  tenantId: string;
@@ -458,12 +518,24 @@ interface AuditFilters {
458
518
  to?: string;
459
519
  page?: number;
460
520
  limit?: number;
521
+ /** Free-text search across action, resourceType, resourceId, userId. */
522
+ q?: string;
523
+ }
524
+ /** Page of results plus the total row count matching the same filters (ignoring page/limit) —
525
+ * lets callers compute total page count for numbered pagination without a second round trip. */
526
+ interface PagedResult<T> {
527
+ items: T[];
528
+ total: number;
529
+ page: number;
530
+ limit: number;
461
531
  }
462
532
  interface ConsentVerifyResult {
463
533
  valid: boolean;
464
- reasons: Array<'profile_version_mismatch' | 'consent_expired' | 'hmac_invalid'>;
465
- currentProfileVersion?: number;
466
- consentProfileVersion?: number;
534
+ reasons: Array<'profile_changed' | 'consent_expired' | 'hmac_invalid'>;
535
+ /** The id of the profile currently active for this consent's compliance group, if any. */
536
+ currentProfileId?: string;
537
+ /** The id of the profile this consent was originally collected against. */
538
+ consentProfileId?: string;
467
539
  }
468
540
  type SafeAdminUser = Omit<AdminUser, 'passwordHash'>;
469
541
  interface CreateRoleInput {
@@ -504,6 +576,8 @@ interface CreateApiKeyInput {
504
576
  tenantId: string;
505
577
  name: string;
506
578
  keyHash: string;
579
+ createdBy?: string;
580
+ expireBy?: string;
507
581
  }
508
582
  interface CreateTenantInput {
509
583
  name: string;
@@ -522,19 +596,27 @@ interface StorageAdapter {
522
596
  deleteProfile(id: string): Promise<void>;
523
597
  getProfile(id: string): Promise<Profile | null>;
524
598
  getProfiles(tenantId: string): Promise<Profile[]>;
599
+ findActiveProfileByComplianceGroup(tenantId: string, complianceGroup: string): Promise<Profile | null>;
600
+ /** Lightweight list — no profile_json blob; single JOIN for template names. */
601
+ listProfilesSummary(tenantId: string): Promise<ProfileSummary[]>;
602
+ findProfilesUsingConsentTemplate(templateId: string): Promise<ProfileSummary[]>;
603
+ findProfilesUsingUITemplate(templateId: string): Promise<ProfileSummary[]>;
604
+ getOptInStats(tenantId: string, filters: OptInFilters): Promise<OptInStats>;
525
605
  createConsent(data: CreateConsentInput): Promise<ConsentDbRecord>;
526
606
  updateConsent(visitorId: string, data: UpdateConsentInput): Promise<ConsentDbRecord>;
527
607
  deleteConsent(visitorId: string): Promise<void>;
528
608
  getConsent(visitorId: string): Promise<ConsentDbRecord | null>;
529
- getConsents(filters: ConsentFilters): Promise<ConsentDbRecord[]>;
609
+ getConsents(filters: ConsentFilters): Promise<PagedResult<ConsentSummary>>;
530
610
  streamConsents(filters: ConsentFilters): AsyncIterable<ConsentDbRecord>;
531
611
  createVisitor(data: CreateVisitorInput): Promise<Visitor>;
532
612
  updateVisitor(visitorId: string, data: UpdateVisitorInput): Promise<Visitor>;
533
613
  deleteVisitor(visitorId: string): Promise<void>;
534
614
  getVisitor(visitorId: string): Promise<Visitor | null>;
535
- getVisitors(filters: VisitorFilters): Promise<Visitor[]>;
615
+ getVisitors(filters: VisitorFilters): Promise<PagedResult<Visitor>>;
536
616
  getConsentHistory(visitorId: string): Promise<ConsentHistoryEntry[]>;
537
617
  streamAuditLogs(filters: AuditFilters): AsyncIterable<AuditLog>;
618
+ createNoticeShown(data: CreateNoticeShownInput): Promise<NoticeShownRecord>;
619
+ getNoticeShownForVisitor(visitorId: string): Promise<NoticeShownRecord[]>;
538
620
  getOverviewStats(tenantId: string): Promise<OverviewStats>;
539
621
  getCategoryStats(tenantId: string): Promise<CategoryStats>;
540
622
  getTimeline(tenantId: string, days?: number): Promise<TimelineEntry[]>;
@@ -544,17 +626,26 @@ interface StorageAdapter {
544
626
  createTenant(data: CreateTenantInput): Promise<Tenant>;
545
627
  updateTenant(id: string, data: UpdateTenantInput): Promise<Tenant>;
546
628
  deleteTenant(id: string): Promise<void>;
629
+ /** Returns `{}` (all fields unset) if no settings row exists yet for this tenant. */
630
+ getSettings(tenantId: string): Promise<TenantSettings>;
631
+ updateSettings(tenantId: string, data: Partial<TenantSettings>): Promise<TenantSettings>;
547
632
  createApiKey(data: CreateApiKeyInput): Promise<ApiKey>;
548
633
  getApiKeyByHash(keyHash: string): Promise<ApiKey | null>;
549
634
  revokeApiKey(id: string): Promise<void>;
635
+ /** Re-enables a previously revoked key (same hash, no new secret). Clears `expireBy` if it's
636
+ * in the past, so a lazily-expired key doesn't immediately re-expire on the next check. */
637
+ reactivateApiKey(id: string): Promise<void>;
638
+ /** Permanently removes the key row — unlike `revokeApiKey`, this cannot be undone. */
639
+ deleteApiKey(id: string): Promise<void>;
550
640
  getApiKeys(tenantId: string): Promise<ApiKey[]>;
551
641
  purgeExpiredConsents(olderThanDays: number): Promise<number>;
552
- createCookieTemplate(data: CreateCookieTemplateInput): Promise<ServerCookieTemplate>;
553
- updateCookieTemplate(id: string, data: UpdateCookieTemplateInput): Promise<ServerCookieTemplate>;
554
- deleteCookieTemplate(id: string): Promise<void>;
555
- getCookieTemplate(id: string): Promise<ServerCookieTemplate | null>;
556
- getCookieTemplates(tenantId: string): Promise<ServerCookieTemplate[]>;
557
- copyCookieTemplate(id: string, newName: string): Promise<ServerCookieTemplate>;
642
+ purgeExpiredAuditLogs(olderThanDays: number): Promise<number>;
643
+ createConsentTemplate(data: CreateConsentTemplateInput): Promise<ServerConsentTemplate>;
644
+ updateConsentTemplate(id: string, data: UpdateConsentTemplateInput): Promise<ServerConsentTemplate>;
645
+ deleteConsentTemplate(id: string): Promise<void>;
646
+ getConsentTemplate(id: string): Promise<ServerConsentTemplate | null>;
647
+ getConsentTemplates(tenantId: string): Promise<ServerConsentTemplate[]>;
648
+ copyConsentTemplate(id: string, newName: string): Promise<ServerConsentTemplate>;
558
649
  createUITemplate(data: CreateUITemplateInput): Promise<ServerUITemplate>;
559
650
  updateUITemplate(id: string, data: UpdateUITemplateInput): Promise<ServerUITemplate>;
560
651
  deleteUITemplate(id: string): Promise<void>;
@@ -580,7 +671,8 @@ interface StorageAdapter {
580
671
  assignPermissionToRole(roleId: string, permissionId: string): Promise<void>;
581
672
  revokePermissionFromRole(roleId: string, permissionId: string): Promise<void>;
582
673
  createLog(data: CreateAuditLogInput): Promise<void>;
583
- getLogs(filters: AuditFilters): Promise<AuditLog[]>;
674
+ getLogs(filters: AuditFilters): Promise<PagedResult<AuditLogSummary>>;
675
+ getAuditLogById(id: string): Promise<AuditLog | null>;
584
676
  }
585
677
  interface PluginContext {
586
678
  storage: StorageAdapter;
@@ -599,6 +691,325 @@ declare abstract class ConsentiServerPlugin {
599
691
  beforeUserCreate?(data: CreateUserInput): Promise<CreateUserInput>;
600
692
  afterUserCreate?(user: AdminUser): Promise<void>;
601
693
  }
694
+ /**
695
+ * The server's stored shape of `Profile.profileJson` — extends the shared `ProfileConfig`
696
+ * (`./ui`, also used by `apps/ui`'s local `ConsentiProfile`/`registerProfile()` authoring, which
697
+ * genuinely needs the full multi-locale `translations`/`localeContents` blob for its own
698
+ * no-backend use case and must stay untouched).
699
+ *
700
+ * On the server, only `defaultLocale`'s own resolved content is ever stored here —
701
+ * `mainBanner`/`gpcBanner`/`preferenceModal` (inherited from `ProfileConfig` as-is: template shape
702
+ * merged with the author's text, optional fields simply omitted rather than blank — never a
703
+ * multi-locale map). Every other locale's content lives only in its own per-version JSON file on
704
+ * disk (`profiles/{tenant}/{id}/{version}/{locale}.json`, see `ProfileService`), never in the DB
705
+ * row — `translations`/`localeContents` (both full multi-locale blobs) are dropped entirely.
706
+ */
707
+ interface StoredProfileJson extends Omit<ProfileConfig, 'translations' | 'localeContents'> {
708
+ /** Every locale this profile has content for — drives the dashboard's tab list. Content for any
709
+ * locale other than `defaultLocale` lives only in that locale's on-disk version file, not here. */
710
+ locales: string[];
711
+ }
712
+
713
+ type ConsentStatus = 'granted' | 'denied' | 'objected';
714
+ type ConsentValue = Record<string, ConsentStatus>;
715
+ type GpcMode = 'ignore' | 'honor' | 'strict';
716
+ type LegalBasis = 'mandatory' | 'consent' | 'legitimate_interest';
717
+ type CookiePurpose = typeof COOKIE_PURPOSE_IDS[number];
718
+ /**
719
+ * A single consent parameter (tracker/cookie definition).
720
+ * Keyed by its own id inside `Record<string, Cookie>` — the map key IS the id,
721
+ * there is no `id` field on the value itself.
722
+ */
723
+ interface Cookie {
724
+ purpose?: CookiePurpose;
725
+ listenGpc?: boolean;
726
+ /**
727
+ * Pre-grant this parameter's default consent to 'granted' (instead of the
728
+ * compliance-group-driven default) when no stored decision exists yet.
729
+ * Only meaningful when the owning category's `legalBasis === 'consent'` —
730
+ * `mandatory`/`legitimate_interest` categories are already effectively
731
+ * pre-granted regardless of this flag. Never overrides an active GPC signal.
732
+ * Default: false.
733
+ */
734
+ preGrant?: boolean;
735
+ tcfVendorId?: number;
736
+ tcfPurposes?: number[];
737
+ tcfSpecialFeatures?: number[];
738
+ cpraCategory?: 'sale' | 'sharing' | 'sensitive';
739
+ }
740
+ /** `Cookie` keyed by its own id — the standard shape used everywhere a parameter list is passed around. */
741
+ type CookieMap = Record<string, Cookie>;
742
+ type ButtonStyle = 'primary' | 'secondary' | 'text' | 'accent';
743
+ type ButtonAction = 'submit' | 'manage' | 'close' | 'custom' | 'link';
744
+ interface Button {
745
+ text: string;
746
+ style: ButtonStyle;
747
+ action: ButtonAction;
748
+ cookies?: string[] | '*' | '!';
749
+ url?: string;
750
+ }
751
+ /** `Button` keyed by its own id — e.g. `'accept-all'`. The map key IS the id, rendered as the DOM
752
+ * `id` (`consenti-btn-{id}`) so integrators can target specific buttons, and is what
753
+ * `profileOverride`'s `deepMerge` deletes by (`{ 'reject-optional': null }` removes one button). */
754
+ type ButtonMap = Record<string, Button>;
755
+ interface MainBanner {
756
+ position: 'top' | 'bottom' | 'middle' | 'left-bottom' | 'right-bottom';
757
+ overlayOpacity?: number;
758
+ showClose?: boolean;
759
+ showLocaleSwitcher?: boolean;
760
+ heading?: string;
761
+ headingTag?: string;
762
+ htmlText: string;
763
+ buttons: ButtonMap;
764
+ /** Stack buttons vertically below this viewport width in px. 0 or undefined = disabled. Default: 576. */
765
+ stackButtonsOnBreakpoint?: number;
766
+ /** Trap keyboard Tab focus within the consent root while this banner is visible. */
767
+ trapFocus?: boolean;
768
+ }
769
+ type GpcBanner = MainBanner;
770
+ /**
771
+ * A consent category — the single source of legal basis for every parameter
772
+ * it lists in `cookies`. Keyed by its own id inside `Record<string, Category>` —
773
+ * the map key IS the id, there is no `id` field on the value itself.
774
+ *
775
+ * A parameter must belong to exactly one category (enforced at authoring time,
776
+ * not by this type) — that relationship is what `getCookieLegalBasis`/
777
+ * `isMandatoryCookie` (`@consenti/utils`) resolve at runtime.
778
+ */
779
+ interface Category {
780
+ heading: string;
781
+ headingTag?: string;
782
+ htmlText: string;
783
+ legalBasis: LegalBasis;
784
+ /** Only meaningful when legalBasis === 'legitimate_interest' — the GDPR balancing-test justification shown to visitors. */
785
+ legitimateInterestDescription?: string;
786
+ /** IDs of the parameters (from the profile's `cookies` map) that belong to this category. */
787
+ cookies: string[];
788
+ }
789
+ /** `Category` keyed by its own id — the standard shape used everywhere a category list is passed around. */
790
+ type CategoryMap = Record<string, Category>;
791
+ interface PreferenceModal extends Omit<MainBanner, "position"> {
792
+ position?: 'left' | 'right' | 'center';
793
+ subheading?: string;
794
+ categories: CategoryMap;
795
+ persistent?: boolean;
796
+ /** Screen width in px below which the modal expands to full screen. Default: 576. Set to 0 to disable. */
797
+ mobileFullScreenBreakpoint?: number;
798
+ /** Label for the optional consent-receipt download checkbox (shown when `allowReceipt` is true). Falls back to a default when omitted. */
799
+ receiptLabel?: string;
800
+ /** Description shown beneath the consent-receipt checkbox. Falls back to a default when omitted. */
801
+ receiptDescription?: string;
802
+ }
803
+ /**
804
+ * A UI-template button definition. Keyed by its own machine id (e.g. `'accept-all'`) inside
805
+ * `TemplateButtonMap` — the map key IS the id, not display text. UI templates own layout/behavior
806
+ * only, never text. The visitor-facing label is authored per-locale on the profile instead
807
+ * (`LocaleTextContent.*.buttonLabels`, keyed by the same button id) and composed into a resolved
808
+ * `Button.text` at profile-build time. Distinct from `Button` (the resolved-profile shape), which
809
+ * always carries real `text`.
810
+ */
811
+ interface TemplateButtonDef {
812
+ type?: ButtonStyle;
813
+ action: ButtonAction;
814
+ cookies?: string[] | '*' | '!';
815
+ url?: string;
816
+ }
817
+ /** `TemplateButtonDef` keyed by its own id — the standard shape everywhere a UI template's
818
+ * button set is passed around. */
819
+ type TemplateButtonMap = Record<string, TemplateButtonDef>;
820
+ interface TemplateBannerDef {
821
+ position: 'top' | 'bottom' | 'middle' | 'left-bottom' | 'right-bottom';
822
+ overlayOpacity?: number;
823
+ showClose?: boolean;
824
+ showLocaleSwitcher?: boolean;
825
+ headingTag?: string;
826
+ buttons: TemplateButtonMap;
827
+ stackButtonsOnBreakpoint?: number;
828
+ trapFocus?: boolean;
829
+ }
830
+ type TemplateGpcBannerDef = TemplateBannerDef;
831
+ interface TemplateModalDef extends Omit<TemplateBannerDef, 'position'> {
832
+ position?: 'left' | 'right' | 'center';
833
+ persistent?: boolean;
834
+ hasSubheading?: boolean;
835
+ mobileFullScreenBreakpoint?: number;
836
+ }
837
+ interface LocaleTranslations {
838
+ mainBanner: DeepPartial<MainBanner>;
839
+ gpcBanner?: DeepPartial<GpcBanner>;
840
+ preferenceModal: DeepPartial<PreferenceModal>;
841
+ }
842
+ interface ProfileTranslations {
843
+ [locale: string]: LocaleTranslations;
844
+ }
845
+ interface DpdpaConfig {
846
+ dataFiduciary: string;
847
+ grievanceEmail: string;
848
+ purposeDescription?: string;
849
+ }
850
+ interface LocaleTextContent {
851
+ mainBanner: {
852
+ heading?: string;
853
+ htmlText: string;
854
+ /** Keyed by the linked UI template's button id — was a positional array matched to
855
+ * `buttons[i]` by index; now matched by key, robust against button reordering. */
856
+ buttonLabels?: Record<string, string>;
857
+ };
858
+ gpcBanner: {
859
+ heading?: string;
860
+ htmlText: string;
861
+ buttonLabels?: Record<string, string>;
862
+ };
863
+ preferenceModal: {
864
+ heading: string;
865
+ subheading?: string;
866
+ htmlText?: string;
867
+ buttonLabels?: Record<string, string>;
868
+ /** `legitimateInterestDescription` is only meaningful for categories whose legalBasis is 'legitimate_interest'; optional GDPR balancing-test text, translated per locale like heading/htmlText. */
869
+ categories: Record<string, {
870
+ heading: string;
871
+ htmlText: string;
872
+ legitimateInterestDescription?: string;
873
+ }>;
874
+ receiptLabel?: string;
875
+ receiptDescription?: string;
876
+ };
877
+ }
878
+ interface ProfileConfig {
879
+ id: string;
880
+ cookies?: CookieMap;
881
+ defaultLocale: string;
882
+ /** Days until consent expires and the visitor is asked again (profile-wide). Default: 365. */
883
+ expiryDays?: number;
884
+ translations?: ProfileTranslations;
885
+ mainBanner: MainBanner;
886
+ gpcBanner?: GpcBanner;
887
+ preferenceModal: PreferenceModal;
888
+ darkMode?: boolean;
889
+ allowedOrigins?: string[];
890
+ dpdpa?: DpdpaConfig;
891
+ regulation?: 'gdpr' | 'ccpa' | 'cpra' | 'dpdpa' | 'uk-gdpr' | 'lgpd' | 'pipeda' | 'popia' | 'pdpa-th' | 'appi' | 'kvkk';
892
+ regulations?: string[];
893
+ consentTemplateId?: string;
894
+ uiTemplateId?: string;
895
+ localeContents?: Record<string, LocaleTextContent>;
896
+ complianceGroup?: ComplianceGroupId;
897
+ /**
898
+ * Free-form identifier (lower-kebab-case) for profiles that don't map to one of the
899
+ * built-in `complianceGroup` values — the identifier the widget's `compliance.type`
900
+ * config targets to fetch this profile. Participates in activation, deactivation, and
901
+ * "one active profile per group" conflict detection the same way `complianceGroup` does;
902
+ * no `COMPLIANCE_GROUPS` validation rules or GPC defaults apply to it, since none exist
903
+ * for a free-form name.
904
+ */
905
+ customComplianceGroup?: string;
906
+ /**
907
+ * Per-profile deltas applied on top of the consent template's authored `Cookie` values
908
+ * (e.g. an overridden `preGrant`) when resolving this profile — see the dashboard's Step 2
909
+ * pre-grant override UX. Keyed by cookie id; only the overridden fields need to be present.
910
+ */
911
+ cookiesOverride?: Record<string, Partial<Cookie>>;
912
+ /**
913
+ * Reserved for a future phase — stored but not yet applied when resolving a profile.
914
+ * Intended to mirror `cookiesOverride` for per-category deltas.
915
+ */
916
+ categoriesOverride?: Record<string, Partial<Category>>;
917
+ /**
918
+ * Reserved for a future phase — stored but not yet applied when resolving a profile.
919
+ * Intended to hold per-profile UI-template deltas.
920
+ */
921
+ uiOverride?: Record<string, unknown>;
922
+ /**
923
+ * When registered via `registerProfile()` and `complianceGroup` matches one of
924
+ * Consenti's built-in groups: `false` (default) fully replaces the built-in
925
+ * embedded profile for that group; `true` deep-merges this config onto the
926
+ * built-in profile instead (only meaningful together with `complianceGroup`).
927
+ */
928
+ deepMerge?: boolean;
929
+ gpcMode?: GpcMode;
930
+ isActive?: boolean;
931
+ hidePoweredBy?: boolean;
932
+ allowReceipt?: boolean;
933
+ /** Per-compliance extra config (e.g. DPDPA data fiduciary name). */
934
+ complianceConfig?: Record<string, string>;
935
+ /** Show a metadata footer strip in the banner/modal with Consent ID, Date, Version, Privacy Settings link. */
936
+ showFooterMetadata?: boolean;
937
+ /** Apply WCAG 2.1 AA button sizing (44px min-height), visible focus rings, and screen-reader labels. */
938
+ enhanceAccessibility?: boolean;
939
+ }
940
+ interface ServerUITemplate {
941
+ id: string;
942
+ tenantId: string;
943
+ name: string;
944
+ mainBanner: TemplateBannerDef;
945
+ gpcBanner: TemplateGpcBannerDef;
946
+ preferenceModal: TemplateModalDef;
947
+ createdAt: string;
948
+ updatedAt: string;
949
+ }
950
+ interface ServerConsentTemplate {
951
+ id: string;
952
+ tenantId: string;
953
+ name: string;
954
+ cookies: CookieMap;
955
+ categories: CategoryMap;
956
+ createdAt: string;
957
+ updatedAt: string;
958
+ }
959
+ interface CreateConsentTemplateInput {
960
+ tenantId: string;
961
+ name: string;
962
+ cookies: CookieMap;
963
+ categories: CategoryMap;
964
+ }
965
+ interface UpdateConsentTemplateInput {
966
+ name?: string;
967
+ cookies?: CookieMap;
968
+ categories?: CategoryMap;
969
+ }
970
+ interface CreateUITemplateInput {
971
+ tenantId: string;
972
+ name: string;
973
+ mainBanner: TemplateBannerDef;
974
+ gpcBanner: TemplateGpcBannerDef;
975
+ preferenceModal: TemplateModalDef;
976
+ }
977
+ interface UpdateUITemplateInput {
978
+ name?: string;
979
+ mainBanner?: TemplateBannerDef;
980
+ gpcBanner?: TemplateGpcBannerDef;
981
+ preferenceModal?: TemplateModalDef;
982
+ }
983
+ interface PublicProfileResponse {
984
+ id: string;
985
+ /** Incremented in place on every save of this profile. */
986
+ version: number;
987
+ name?: string;
988
+ tenantId?: string;
989
+ defaultLocale: string;
990
+ currentLocale: string;
991
+ locales: string[];
992
+ cookies: CookieMap;
993
+ /** Days until consent expires and the visitor is asked again (profile-wide). */
994
+ expiryDays?: number;
995
+ mainBanner: MainBanner;
996
+ gpcBanner?: GpcBanner;
997
+ preferenceModal: PreferenceModal;
998
+ resolvedComplianceGroup?: string;
999
+ complianceGroup?: ComplianceGroupId;
1000
+ compliances?: string[];
1001
+ complianceConfig?: Record<string, string>;
1002
+ allowedOrigins?: string[];
1003
+ gpcMode?: GpcMode;
1004
+ hidePoweredBy?: boolean;
1005
+ allowReceipt?: boolean;
1006
+ darkMode?: boolean;
1007
+ dpdpa?: DpdpaConfig;
1008
+ showFooterMetadata?: boolean;
1009
+ enhanceAccessibility?: boolean;
1010
+ createdAt?: string;
1011
+ updatedAt?: string;
1012
+ }
602
1013
 
603
1014
  declare class ProfileRepo {
604
1015
  private adapter;
@@ -608,13 +1019,25 @@ declare class ProfileRepo {
608
1019
  delete(id: string): Promise<void>;
609
1020
  get(id: string): Promise<Profile | null>;
610
1021
  list(tenantId: string): Promise<Profile[]>;
1022
+ findActiveByComplianceGroup(tenantId: string, complianceGroup: string): Promise<Profile | null>;
611
1023
  }
612
1024
 
613
1025
  declare class AuditRepo {
614
1026
  private adapter;
615
1027
  constructor(adapter: StorageAdapter);
616
1028
  log(data: CreateAuditLogInput): Promise<void>;
617
- list(filters: AuditFilters): Promise<AuditLog[]>;
1029
+ list(filters: AuditFilters): Promise<PagedResult<AuditLogSummary>>;
1030
+ get(id: string): Promise<AuditLog | null>;
1031
+ }
1032
+
1033
+ declare class LocaleJsonCacheService {
1034
+ private readonly dir;
1035
+ constructor(dir: string);
1036
+ read(profileId: string, locale: string): PublicProfileResponse | null;
1037
+ write(profileId: string, locale: string, response: PublicProfileResponse): void;
1038
+ rebuild(profileId: string, responses: Map<string, PublicProfileResponse>): void;
1039
+ invalidate(profileId: string): void;
1040
+ private path;
618
1041
  }
619
1042
 
620
1043
  declare class ProfileService {
@@ -622,13 +1045,101 @@ declare class ProfileService {
622
1045
  private audit;
623
1046
  private tenantId;
624
1047
  private storage?;
625
- constructor(profiles: ProfileRepo, audit: AuditRepo, tenantId?: string, storage?: StorageAdapter | undefined);
1048
+ private eventBus?;
1049
+ private localeCache?;
1050
+ private profilesDir?;
1051
+ private handleCache?;
1052
+ private s3Config?;
1053
+ 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);
1054
+ /** Root directory holding every version snapshot of one logical profile, keyed by its stable id. */
1055
+ private versionsRootDir;
1056
+ private complianceGroupDir;
1057
+ /**
1058
+ * The directory key used for activation/hot-serve writes and the "one active profile
1059
+ * per group" conflict check — the real `complianceGroup` when set, otherwise the
1060
+ * free-form `customComplianceGroup` (which the widget's `compliance.type` targets the
1061
+ * same way). Compliance *validation* still only ever runs against a real `complianceGroup`.
1062
+ */
1063
+ private groupKey;
1064
+ private profileFilePaths;
1065
+ /** Resolves this profile's cookie map (consent-template lookup + `cookiesOverride` applied) —
1066
+ * the one piece of content still resolved live rather than baked at save time, since cookies
1067
+ * are small and never the source of the row-size problem `mainBanner`/`preferenceModal` were. */
1068
+ private resolveCookies;
1069
+ /**
1070
+ * Writes every locale's on-disk version file for one save (create or an edit) in a single
1071
+ * atomic worker call — default locale content comes from `profile.profileJson` itself, every
1072
+ * other touched locale from `localeContent`. A locale already in `profileJson.locales` but not
1073
+ * present in `localeContent` (a tab the user never opened this session) is carried forward
1074
+ * unchanged from `previousVersion` by the worker — never silently dropped.
1075
+ */
1076
+ private writeLocaleFiles;
626
1077
  create(input: Omit<CreateProfileInput, 'tenantId'>): Promise<Profile>;
1078
+ /**
1079
+ * Duplicates a profile as a brand new, always-inactive profile (own `id`, `version` starts at
1080
+ * 1 again). Delegates to {@link create} rather than copying on-disk files directly — the
1081
+ * resolved-JSON snapshot embeds its own `id`/`version`, so it has to be regenerated for the
1082
+ * new profile rather than byte-copied from the source's. Non-default locale content is read
1083
+ * back off the source's current version files so the copy carries every locale forward too.
1084
+ */
1085
+ copy(id: string, name?: string): Promise<Profile>;
1086
+ /**
1087
+ * Mutates the existing row in place — `id` is stable across every edit, `version` is
1088
+ * incremented. The prior state isn't kept as a separate DB row; its resolved-JSON snapshot
1089
+ * on disk (see {@link listVersions}) is the audit trail for that edit.
1090
+ */
627
1091
  update(id: string, input: UpdateProfileInput): Promise<Profile>;
628
1092
  delete(id: string): Promise<void>;
629
- get(id: string, locale?: string): Promise<Profile | null>;
1093
+ activate(id: string): Promise<Profile>;
1094
+ deactivate(id: string): Promise<Profile>;
1095
+ private runDeactivate;
1096
+ get(id: string): Promise<Profile | null>;
1097
+ /**
1098
+ * Resolves one locale's full public response. The default locale's content lives directly on
1099
+ * the DB row (`profileJson.mainBanner`/`gpcBanner`/`preferenceModal`) — no template lookups, no
1100
+ * live resolution. Any other locale's content lives only in that locale's on-disk version file;
1101
+ * this reads it directly, falling back to the default locale's content if the file is missing
1102
+ * (e.g. a locale listed but never actually authored yet). This method is not a hot path — the
1103
+ * public `/profiles/:tenant/:group/:locale` route serves the static file directly and only
1104
+ * falls back to this for previewing an inactive/versioned profile.
1105
+ */
630
1106
  getResolved(id: string, locale?: string): Promise<PublicProfileResponse | null>;
1107
+ findActiveByComplianceGroup(complianceGroup: string): Promise<Profile | null>;
631
1108
  list(): Promise<Profile[]>;
1109
+ listSummary(): Promise<ProfileSummary[]>;
1110
+ /**
1111
+ * Lists every version snapshot of `profileId` on disk (newest first), read straight from
1112
+ * the version-directory tree — no DB query, `Profile.version` is just the current pointer.
1113
+ * Works for archived profiles too (DB row deleted, files still on disk) — the directory
1114
+ * tree is the only thing this reads.
1115
+ */
1116
+ listVersions(profileId: string): Promise<ProfileVersionEntry[]>;
1117
+ /** Works for archived profiles too — see `listVersions()`. */
1118
+ getVersionFile(profileId: string, version: string, locale: string): Promise<string | null>;
1119
+ /**
1120
+ * Profile-id directories on disk with no matching DB row — deleted profiles whose version
1121
+ * snapshots `delete()` never removes. Directory-only read (id, version count, last-modified
1122
+ * mtime) — no file content is opened here; that only happens once a specific version is
1123
+ * requested via `getVersionFile()`, which works for these ids too.
1124
+ */
1125
+ listArchivedProfiles(): Promise<ArchivedProfileSummary[]>;
1126
+ /**
1127
+ * Seeds a default profile for a compliance group from the embedded English profile in
1128
+ * `@consenti/utils`, merged with locale text overlays for de/es/fr/ja. Every locale — including
1129
+ * non-English ones — is passed through `create()`'s normal `localeContent` path, the same as an
1130
+ * author-submitted profile; there is no separate "seeded profile" code path for locale content
1131
+ * anymore, which is what previously let a locale silently never make it into the served file.
1132
+ * Only creates the profile if no active profile already exists for the compliance group.
1133
+ * Safe to call repeatedly (idempotent).
1134
+ *
1135
+ * @param complianceGroup - One of the 8 compliance group IDs.
1136
+ */
1137
+ seedDefaultProfile(complianceGroup: string): Promise<void>;
1138
+ /**
1139
+ * Seeds default profiles for all 8 compliance groups.
1140
+ * Skips any group that already has an active profile (idempotent).
1141
+ */
1142
+ seedAllDefaults(): Promise<void>;
632
1143
  }
633
1144
 
634
1145
  declare class ConsentRepo {
@@ -638,7 +1149,7 @@ declare class ConsentRepo {
638
1149
  update(visitorId: string, data: UpdateConsentInput): Promise<ConsentDbRecord>;
639
1150
  delete(visitorId: string): Promise<void>;
640
1151
  get(visitorId: string): Promise<ConsentDbRecord | null>;
641
- list(filters: ConsentFilters): Promise<ConsentDbRecord[]>;
1152
+ list(filters: ConsentFilters): Promise<PagedResult<ConsentSummary>>;
642
1153
  }
643
1154
 
644
1155
  declare class VisitorRepo {
@@ -674,7 +1185,20 @@ declare class ConsentService {
674
1185
  private pluginEngine?;
675
1186
  private eventBus?;
676
1187
  private tcfConfig?;
677
- constructor(consents: ConsentRepo, visitors: VisitorRepo, profiles: ProfileRepo, audit: AuditRepo, tenantId?: string, compliance?: ComplianceConfig | undefined, pluginEngine?: PluginEngine | undefined, eventBus?: EventEmitter | undefined, tcfConfig?: TcfConfig | undefined);
1188
+ 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);
1191
+ private sign;
1192
+ /**
1193
+ * Profiles created via the dashboard wizard reference a consent template (`consentTemplateId`)
1194
+ * rather than embedding `cookies`/`preferenceModal.categories` directly on `profileJson` — those
1195
+ * fields are only populated for the embedded/seeded default profiles (see
1196
+ * `ProfileService.seedDefaultProfile`). Cookie/category membership and `legalBasis` are
1197
+ * locale-independent, so the raw consent template's data is sufficient here — no need to
1198
+ * resolve per-locale text via `ProfileService.getResolved()`. Mirrors the same fallback
1199
+ * `ProfileService.getResolved()` and the admin route validators already apply.
1200
+ */
1201
+ private resolveTemplateData;
678
1202
  create(input: Omit<CreateConsentInput, 'tenantId'>): Promise<ConsentDbRecord>;
679
1203
  update(visitorId: string, input: UpdateConsentInput): Promise<ConsentDbRecord>;
680
1204
  get(visitorId: string): Promise<ConsentDbRecord | null>;
@@ -685,7 +1209,8 @@ declare class ConsentService {
685
1209
  declare class VisitorService {
686
1210
  private visitors;
687
1211
  private tenantId;
688
- constructor(visitors: VisitorRepo, tenantId?: string);
1212
+ private eventBus?;
1213
+ constructor(visitors: VisitorRepo, tenantId?: string, eventBus?: EventEmitter | undefined);
689
1214
  upsert(data: {
690
1215
  visitorId: string;
691
1216
  ip?: string;
@@ -721,12 +1246,14 @@ declare class UserService {
721
1246
  email: string;
722
1247
  password: string;
723
1248
  roleId?: string;
1249
+ allowedTenants?: string[];
724
1250
  }, actorId?: string): Promise<SafeAdminUser>;
725
1251
  update(id: string, input: {
726
1252
  name?: string;
727
1253
  email?: string;
728
1254
  isActive?: boolean;
729
1255
  password?: string;
1256
+ allowedTenants?: string[];
730
1257
  }, actorId?: string): Promise<SafeAdminUser>;
731
1258
  delete(id: string, actorId?: string): Promise<void>;
732
1259
  get(id: string): Promise<SafeAdminUser | null>;
@@ -751,20 +1278,25 @@ declare class BetterSqlite3Adapter implements StorageAdapter {
751
1278
  deleteProfile(id: string): Promise<void>;
752
1279
  getProfile(id: string): Promise<Profile | null>;
753
1280
  getProfiles(tenantId: string): Promise<Profile[]>;
1281
+ findActiveProfileByComplianceGroup(tenantId: string, complianceGroup: string): Promise<Profile | null>;
754
1282
  private mapConsent;
1283
+ private static readonly CONSENT_SUMMARY_COLS;
1284
+ private mapConsentSummary;
755
1285
  createConsent(data: CreateConsentInput): Promise<ConsentDbRecord>;
756
1286
  updateConsent(visitorId: string, data: UpdateConsentInput): Promise<ConsentDbRecord>;
757
1287
  deleteConsent(visitorId: string): Promise<void>;
758
1288
  getConsent(visitorId: string): Promise<ConsentDbRecord | null>;
759
- getConsents(filters: ConsentFilters): Promise<ConsentDbRecord[]>;
1289
+ getConsents(filters: ConsentFilters): Promise<PagedResult<ConsentSummary>>;
760
1290
  streamConsents(filters: ConsentFilters): AsyncIterable<ConsentDbRecord>;
761
1291
  getConsentHistory(visitorId: string): Promise<ConsentHistoryEntry[]>;
1292
+ createNoticeShown(data: CreateNoticeShownInput): Promise<NoticeShownRecord>;
1293
+ getNoticeShownForVisitor(visitorId: string): Promise<NoticeShownRecord[]>;
762
1294
  private mapVisitor;
763
1295
  createVisitor(data: CreateVisitorInput): Promise<Visitor>;
764
1296
  updateVisitor(visitorId: string, data: UpdateVisitorInput): Promise<Visitor>;
765
1297
  deleteVisitor(visitorId: string): Promise<void>;
766
1298
  getVisitor(visitorId: string): Promise<Visitor | null>;
767
- getVisitors(filters: VisitorFilters): Promise<Visitor[]>;
1299
+ getVisitors(filters: VisitorFilters): Promise<PagedResult<Visitor>>;
768
1300
  private mapUser;
769
1301
  createUser(data: CreateUserInput): Promise<AdminUser>;
770
1302
  updateUser(id: string, data: UpdateUserInput): Promise<AdminUser>;
@@ -785,7 +1317,10 @@ declare class BetterSqlite3Adapter implements StorageAdapter {
785
1317
  assignRole(userId: string, roleId: string, tenantId?: string): Promise<void>;
786
1318
  revokeRole(userId: string, roleId: string, tenantId?: string): Promise<void>;
787
1319
  createLog(data: CreateAuditLogInput): Promise<void>;
788
- getLogs(filters: AuditFilters): Promise<AuditLog[]>;
1320
+ getLogs(filters: AuditFilters): Promise<PagedResult<AuditLogSummary>>;
1321
+ getAuditLogById(id: string): Promise<AuditLog | null>;
1322
+ private static readonly AUDIT_SUMMARY_COLS;
1323
+ private mapAuditSummary;
789
1324
  private mapAuditLog;
790
1325
  streamAuditLogs(filters: AuditFilters): AsyncIterable<AuditLog>;
791
1326
  getOverviewStats(tenantId: string): Promise<OverviewStats>;
@@ -797,19 +1332,24 @@ declare class BetterSqlite3Adapter implements StorageAdapter {
797
1332
  createTenant(data: CreateTenantInput): Promise<Tenant>;
798
1333
  updateTenant(id: string, data: UpdateTenantInput): Promise<Tenant>;
799
1334
  deleteTenant(id: string): Promise<void>;
1335
+ getSettings(tenantId: string): Promise<TenantSettings>;
1336
+ updateSettings(tenantId: string, data: Partial<TenantSettings>): Promise<TenantSettings>;
800
1337
  purgeExpiredConsents(olderThanDays: number): Promise<number>;
1338
+ purgeExpiredAuditLogs(olderThanDays: number): Promise<number>;
801
1339
  private mapApiKey;
802
1340
  createApiKey(data: CreateApiKeyInput): Promise<ApiKey>;
803
1341
  getApiKeyByHash(keyHash: string): Promise<ApiKey | null>;
804
1342
  revokeApiKey(id: string): Promise<void>;
1343
+ reactivateApiKey(id: string): Promise<void>;
1344
+ deleteApiKey(id: string): Promise<void>;
805
1345
  getApiKeys(tenantId: string): Promise<ApiKey[]>;
806
- private mapCookieTemplate;
807
- createCookieTemplate(data: CreateCookieTemplateInput): Promise<ServerCookieTemplate>;
808
- updateCookieTemplate(id: string, data: UpdateCookieTemplateInput): Promise<ServerCookieTemplate>;
809
- deleteCookieTemplate(id: string): Promise<void>;
810
- getCookieTemplate(id: string): Promise<ServerCookieTemplate | null>;
811
- getCookieTemplates(tenantId: string): Promise<ServerCookieTemplate[]>;
812
- copyCookieTemplate(id: string, newName: string): Promise<ServerCookieTemplate>;
1346
+ private mapConsentTemplate;
1347
+ createConsentTemplate(data: CreateConsentTemplateInput): Promise<ServerConsentTemplate>;
1348
+ updateConsentTemplate(id: string, data: UpdateConsentTemplateInput): Promise<ServerConsentTemplate>;
1349
+ deleteConsentTemplate(id: string): Promise<void>;
1350
+ getConsentTemplate(id: string): Promise<ServerConsentTemplate | null>;
1351
+ getConsentTemplates(tenantId: string): Promise<ServerConsentTemplate[]>;
1352
+ copyConsentTemplate(id: string, newName: string): Promise<ServerConsentTemplate>;
813
1353
  private mapUITemplate;
814
1354
  createUITemplate(data: CreateUITemplateInput): Promise<ServerUITemplate>;
815
1355
  updateUITemplate(id: string, data: UpdateUITemplateInput): Promise<ServerUITemplate>;
@@ -817,6 +1357,12 @@ declare class BetterSqlite3Adapter implements StorageAdapter {
817
1357
  getUITemplate(id: string): Promise<ServerUITemplate | null>;
818
1358
  getUITemplates(tenantId: string): Promise<ServerUITemplate[]>;
819
1359
  copyUITemplate(id: string, newName: string): Promise<ServerUITemplate>;
1360
+ private mapProfileSummary;
1361
+ private readonly PROFILE_SUMMARY_SQL;
1362
+ listProfilesSummary(tenantId: string): Promise<ProfileSummary[]>;
1363
+ findProfilesUsingConsentTemplate(templateId: string): Promise<ProfileSummary[]>;
1364
+ findProfilesUsingUITemplate(templateId: string): Promise<ProfileSummary[]>;
1365
+ getOptInStats(tenantId: string, filters: OptInFilters): Promise<OptInStats>;
820
1366
  }
821
1367
 
822
1368
  declare class MongoDBAdapter implements StorageAdapter {
@@ -835,20 +1381,23 @@ declare class MongoDBAdapter implements StorageAdapter {
835
1381
  deleteProfile(id: string): Promise<void>;
836
1382
  getProfile(id: string): Promise<Profile | null>;
837
1383
  getProfiles(tenantId: string): Promise<Profile[]>;
1384
+ findActiveProfileByComplianceGroup(tenantId: string, complianceGroup: string): Promise<Profile | null>;
838
1385
  createConsent(data: CreateConsentInput): Promise<ConsentDbRecord>;
839
1386
  updateConsent(visitorId: string, data: UpdateConsentInput): Promise<ConsentDbRecord>;
840
1387
  deleteConsent(visitorId: string): Promise<void>;
841
1388
  getConsent(visitorId: string): Promise<ConsentDbRecord | null>;
842
- getConsents(filters: ConsentFilters): Promise<ConsentDbRecord[]>;
1389
+ getConsents(filters: ConsentFilters): Promise<PagedResult<ConsentSummary>>;
843
1390
  streamConsents(filters: ConsentFilters): AsyncIterable<ConsentDbRecord>;
844
1391
  private buildConsentFilter;
845
1392
  private writeHistory;
846
1393
  getConsentHistory(visitorId: string): Promise<ConsentHistoryEntry[]>;
1394
+ createNoticeShown(data: CreateNoticeShownInput): Promise<NoticeShownRecord>;
1395
+ getNoticeShownForVisitor(visitorId: string): Promise<NoticeShownRecord[]>;
847
1396
  createVisitor(data: CreateVisitorInput): Promise<Visitor>;
848
1397
  updateVisitor(visitorId: string, data: UpdateVisitorInput): Promise<Visitor>;
849
1398
  deleteVisitor(visitorId: string): Promise<void>;
850
1399
  getVisitor(visitorId: string): Promise<Visitor | null>;
851
- getVisitors(filters: VisitorFilters): Promise<Visitor[]>;
1400
+ getVisitors(filters: VisitorFilters): Promise<PagedResult<Visitor>>;
852
1401
  getOverviewStats(tenantId: string): Promise<OverviewStats>;
853
1402
  getCategoryStats(tenantId: string): Promise<CategoryStats>;
854
1403
  getTimeline(tenantId: string, days?: number): Promise<TimelineEntry[]>;
@@ -871,32 +1420,43 @@ declare class MongoDBAdapter implements StorageAdapter {
871
1420
  assignPermissionToRole(roleId: string, permissionId: string): Promise<void>;
872
1421
  revokePermissionFromRole(roleId: string, permissionId: string): Promise<void>;
873
1422
  createLog(data: CreateAuditLogInput): Promise<void>;
874
- getLogs(filters: AuditFilters): Promise<AuditLog[]>;
1423
+ getLogs(filters: AuditFilters): Promise<PagedResult<AuditLogSummary>>;
1424
+ getAuditLogById(id: string): Promise<AuditLog | null>;
875
1425
  streamAuditLogs(filters: AuditFilters): AsyncIterable<AuditLog>;
876
1426
  private buildAuditFilter;
877
1427
  getCountries(tenantId: string): Promise<CountryStat[]>;
878
1428
  getGpcStats(tenantId: string): Promise<GpcStats>;
879
1429
  getTenants(): Promise<Tenant[]>;
1430
+ private mapApiKey;
880
1431
  createApiKey(data: CreateApiKeyInput): Promise<ApiKey>;
881
1432
  getApiKeyByHash(keyHash: string): Promise<ApiKey | null>;
882
1433
  revokeApiKey(id: string): Promise<void>;
1434
+ reactivateApiKey(id: string): Promise<void>;
1435
+ deleteApiKey(id: string): Promise<void>;
883
1436
  getApiKeys(tenantId: string): Promise<ApiKey[]>;
884
1437
  createTenant(data: CreateTenantInput): Promise<Tenant>;
885
1438
  updateTenant(id: string, data: UpdateTenantInput): Promise<Tenant>;
886
1439
  deleteTenant(id: string): Promise<void>;
1440
+ getSettings(tenantId: string): Promise<TenantSettings>;
1441
+ updateSettings(tenantId: string, data: Partial<TenantSettings>): Promise<TenantSettings>;
887
1442
  purgeExpiredConsents(olderThanDays: number): Promise<number>;
888
- createCookieTemplate(): Promise<never>;
889
- updateCookieTemplate(): Promise<never>;
890
- deleteCookieTemplate(): Promise<void>;
891
- getCookieTemplate(): Promise<never>;
892
- getCookieTemplates(): Promise<never>;
893
- copyCookieTemplate(): Promise<never>;
1443
+ purgeExpiredAuditLogs(olderThanDays: number): Promise<number>;
1444
+ createConsentTemplate(): Promise<never>;
1445
+ updateConsentTemplate(): Promise<never>;
1446
+ deleteConsentTemplate(): Promise<void>;
1447
+ getConsentTemplate(): Promise<never>;
1448
+ getConsentTemplates(): Promise<never>;
1449
+ copyConsentTemplate(): Promise<never>;
894
1450
  createUITemplate(): Promise<never>;
895
1451
  updateUITemplate(): Promise<never>;
896
1452
  deleteUITemplate(): Promise<void>;
897
1453
  getUITemplate(): Promise<never>;
898
1454
  getUITemplates(): Promise<never>;
899
1455
  copyUITemplate(): Promise<never>;
1456
+ listProfilesSummary(): Promise<never[]>;
1457
+ findProfilesUsingConsentTemplate(): Promise<never[]>;
1458
+ findProfilesUsingUITemplate(): Promise<never[]>;
1459
+ getOptInStats(): Promise<never>;
900
1460
  }
901
1461
 
902
1462
  declare class MySQLAdapter implements StorageAdapter {
@@ -914,20 +1474,23 @@ declare class MySQLAdapter implements StorageAdapter {
914
1474
  deleteProfile(id: string): Promise<void>;
915
1475
  getProfile(id: string): Promise<Profile | null>;
916
1476
  getProfiles(tenantId: string): Promise<Profile[]>;
1477
+ findActiveProfileByComplianceGroup(tenantId: string, complianceGroup: string): Promise<Profile | null>;
917
1478
  createConsent(data: CreateConsentInput): Promise<ConsentDbRecord>;
918
1479
  updateConsent(visitorId: string, data: UpdateConsentInput): Promise<ConsentDbRecord>;
919
1480
  deleteConsent(visitorId: string): Promise<void>;
920
1481
  getConsent(visitorId: string): Promise<ConsentDbRecord | null>;
921
- getConsents(filters: ConsentFilters): Promise<ConsentDbRecord[]>;
1482
+ getConsents(filters: ConsentFilters): Promise<PagedResult<ConsentSummary>>;
922
1483
  streamConsents(filters: ConsentFilters): AsyncIterable<ConsentDbRecord>;
923
- private buildConsentQuery;
1484
+ private buildConsentWhere;
924
1485
  private writeHistory;
925
1486
  getConsentHistory(visitorId: string): Promise<ConsentHistoryEntry[]>;
1487
+ createNoticeShown(data: CreateNoticeShownInput): Promise<NoticeShownRecord>;
1488
+ getNoticeShownForVisitor(visitorId: string): Promise<NoticeShownRecord[]>;
926
1489
  createVisitor(data: CreateVisitorInput): Promise<Visitor>;
927
1490
  updateVisitor(visitorId: string, data: UpdateVisitorInput): Promise<Visitor>;
928
1491
  deleteVisitor(visitorId: string): Promise<void>;
929
1492
  getVisitor(visitorId: string): Promise<Visitor | null>;
930
- getVisitors(filters: VisitorFilters): Promise<Visitor[]>;
1493
+ getVisitors(filters: VisitorFilters): Promise<PagedResult<Visitor>>;
931
1494
  getOverviewStats(tenantId: string): Promise<OverviewStats>;
932
1495
  getCategoryStats(tenantId: string): Promise<CategoryStats>;
933
1496
  getTimeline(tenantId: string, days?: number): Promise<TimelineEntry[]>;
@@ -950,32 +1513,43 @@ declare class MySQLAdapter implements StorageAdapter {
950
1513
  assignPermissionToRole(roleId: string, permissionId: string): Promise<void>;
951
1514
  revokePermissionFromRole(roleId: string, permissionId: string): Promise<void>;
952
1515
  createLog(data: CreateAuditLogInput): Promise<void>;
953
- getLogs(filters: AuditFilters): Promise<AuditLog[]>;
1516
+ getLogs(filters: AuditFilters): Promise<PagedResult<AuditLogSummary>>;
1517
+ getAuditLogById(id: string): Promise<AuditLog | null>;
954
1518
  streamAuditLogs(filters: AuditFilters): AsyncIterable<AuditLog>;
955
- private buildAuditQuery;
1519
+ private buildAuditWhere;
956
1520
  getCountries(tenantId: string): Promise<CountryStat[]>;
957
1521
  getGpcStats(tenantId: string): Promise<GpcStats>;
958
1522
  getTenants(): Promise<Tenant[]>;
1523
+ private mapApiKey;
959
1524
  createApiKey(data: CreateApiKeyInput): Promise<ApiKey>;
960
1525
  getApiKeyByHash(keyHash: string): Promise<ApiKey | null>;
961
1526
  revokeApiKey(id: string): Promise<void>;
1527
+ reactivateApiKey(id: string): Promise<void>;
1528
+ deleteApiKey(id: string): Promise<void>;
962
1529
  getApiKeys(tenantId: string): Promise<ApiKey[]>;
963
1530
  createTenant(data: CreateTenantInput): Promise<Tenant>;
964
1531
  updateTenant(id: string, data: UpdateTenantInput): Promise<Tenant>;
1532
+ getSettings(tenantId: string): Promise<TenantSettings>;
1533
+ updateSettings(tenantId: string, data: Partial<TenantSettings>): Promise<TenantSettings>;
965
1534
  deleteTenant(id: string): Promise<void>;
966
1535
  purgeExpiredConsents(olderThanDays: number): Promise<number>;
967
- createCookieTemplate(): Promise<never>;
968
- updateCookieTemplate(): Promise<never>;
969
- deleteCookieTemplate(): Promise<void>;
970
- getCookieTemplate(): Promise<never>;
971
- getCookieTemplates(): Promise<never>;
972
- copyCookieTemplate(): Promise<never>;
1536
+ purgeExpiredAuditLogs(olderThanDays: number): Promise<number>;
1537
+ createConsentTemplate(): Promise<never>;
1538
+ updateConsentTemplate(): Promise<never>;
1539
+ deleteConsentTemplate(): Promise<void>;
1540
+ getConsentTemplate(): Promise<never>;
1541
+ getConsentTemplates(): Promise<never>;
1542
+ copyConsentTemplate(): Promise<never>;
973
1543
  createUITemplate(): Promise<never>;
974
1544
  updateUITemplate(): Promise<never>;
975
1545
  deleteUITemplate(): Promise<void>;
976
1546
  getUITemplate(): Promise<never>;
977
1547
  getUITemplates(): Promise<never>;
978
1548
  copyUITemplate(): Promise<never>;
1549
+ listProfilesSummary(): Promise<never[]>;
1550
+ findProfilesUsingConsentTemplate(): Promise<never[]>;
1551
+ findProfilesUsingUITemplate(): Promise<never[]>;
1552
+ getOptInStats(): Promise<never>;
979
1553
  }
980
1554
 
981
1555
  declare class PostgreSQLAdapter implements StorageAdapter {
@@ -992,20 +1566,23 @@ declare class PostgreSQLAdapter implements StorageAdapter {
992
1566
  deleteProfile(id: string): Promise<void>;
993
1567
  getProfile(id: string): Promise<Profile | null>;
994
1568
  getProfiles(tenantId: string): Promise<Profile[]>;
1569
+ findActiveProfileByComplianceGroup(tenantId: string, complianceGroup: string): Promise<Profile | null>;
995
1570
  createConsent(data: CreateConsentInput): Promise<ConsentDbRecord>;
996
1571
  updateConsent(visitorId: string, data: UpdateConsentInput): Promise<ConsentDbRecord>;
997
1572
  deleteConsent(visitorId: string): Promise<void>;
998
1573
  getConsent(visitorId: string): Promise<ConsentDbRecord | null>;
999
- getConsents(filters: ConsentFilters): Promise<ConsentDbRecord[]>;
1574
+ getConsents(filters: ConsentFilters): Promise<PagedResult<ConsentSummary>>;
1000
1575
  streamConsents(filters: ConsentFilters): AsyncIterable<ConsentDbRecord>;
1001
- private buildConsentQuery;
1576
+ private buildConsentWhere;
1002
1577
  private writeHistory;
1003
1578
  getConsentHistory(visitorId: string): Promise<ConsentHistoryEntry[]>;
1579
+ createNoticeShown(data: CreateNoticeShownInput): Promise<NoticeShownRecord>;
1580
+ getNoticeShownForVisitor(visitorId: string): Promise<NoticeShownRecord[]>;
1004
1581
  createVisitor(data: CreateVisitorInput): Promise<Visitor>;
1005
1582
  updateVisitor(visitorId: string, data: UpdateVisitorInput): Promise<Visitor>;
1006
1583
  deleteVisitor(visitorId: string): Promise<void>;
1007
1584
  getVisitor(visitorId: string): Promise<Visitor | null>;
1008
- getVisitors(filters: VisitorFilters): Promise<Visitor[]>;
1585
+ getVisitors(filters: VisitorFilters): Promise<PagedResult<Visitor>>;
1009
1586
  getOverviewStats(tenantId: string): Promise<OverviewStats>;
1010
1587
  getCategoryStats(tenantId: string): Promise<CategoryStats>;
1011
1588
  getTimeline(tenantId: string, days?: number): Promise<TimelineEntry[]>;
@@ -1028,39 +1605,170 @@ declare class PostgreSQLAdapter implements StorageAdapter {
1028
1605
  assignPermissionToRole(roleId: string, permissionId: string): Promise<void>;
1029
1606
  revokePermissionFromRole(roleId: string, permissionId: string): Promise<void>;
1030
1607
  createLog(data: CreateAuditLogInput): Promise<void>;
1031
- getLogs(filters: AuditFilters): Promise<AuditLog[]>;
1608
+ getLogs(filters: AuditFilters): Promise<PagedResult<AuditLogSummary>>;
1609
+ getAuditLogById(id: string): Promise<AuditLog | null>;
1032
1610
  streamAuditLogs(filters: AuditFilters): AsyncIterable<AuditLog>;
1033
- private buildAuditQuery;
1611
+ private buildAuditWhere;
1034
1612
  getCountries(tenantId: string): Promise<CountryStat[]>;
1035
1613
  getGpcStats(tenantId: string): Promise<GpcStats>;
1036
1614
  getTenants(): Promise<Tenant[]>;
1615
+ private mapApiKey;
1037
1616
  createApiKey(data: CreateApiKeyInput): Promise<ApiKey>;
1038
1617
  getApiKeyByHash(keyHash: string): Promise<ApiKey | null>;
1039
1618
  revokeApiKey(id: string): Promise<void>;
1619
+ reactivateApiKey(id: string): Promise<void>;
1620
+ deleteApiKey(id: string): Promise<void>;
1040
1621
  getApiKeys(tenantId: string): Promise<ApiKey[]>;
1041
1622
  createTenant(data: CreateTenantInput): Promise<Tenant>;
1042
1623
  updateTenant(id: string, data: UpdateTenantInput): Promise<Tenant>;
1043
1624
  deleteTenant(id: string): Promise<void>;
1625
+ getSettings(tenantId: string): Promise<TenantSettings>;
1626
+ updateSettings(tenantId: string, data: Partial<TenantSettings>): Promise<TenantSettings>;
1044
1627
  purgeExpiredConsents(olderThanDays: number): Promise<number>;
1045
- createCookieTemplate(): Promise<never>;
1046
- updateCookieTemplate(): Promise<never>;
1047
- deleteCookieTemplate(): Promise<void>;
1048
- getCookieTemplate(): Promise<never>;
1049
- getCookieTemplates(): Promise<never>;
1050
- copyCookieTemplate(): Promise<never>;
1628
+ purgeExpiredAuditLogs(olderThanDays: number): Promise<number>;
1629
+ createConsentTemplate(): Promise<never>;
1630
+ updateConsentTemplate(): Promise<never>;
1631
+ deleteConsentTemplate(): Promise<void>;
1632
+ getConsentTemplate(): Promise<never>;
1633
+ getConsentTemplates(): Promise<never>;
1634
+ copyConsentTemplate(): Promise<never>;
1051
1635
  createUITemplate(): Promise<never>;
1052
1636
  updateUITemplate(): Promise<never>;
1053
1637
  deleteUITemplate(): Promise<void>;
1054
1638
  getUITemplate(): Promise<never>;
1055
1639
  getUITemplates(): Promise<never>;
1056
1640
  copyUITemplate(): Promise<never>;
1641
+ listProfilesSummary(): Promise<never[]>;
1642
+ findProfilesUsingConsentTemplate(): Promise<never[]>;
1643
+ findProfilesUsingUITemplate(): Promise<never[]>;
1644
+ getOptInStats(): Promise<never>;
1645
+ }
1646
+
1647
+ /**
1648
+ * ConsentAction — server-side counterpart to `@consenti/ui`'s `ConsentAction`.
1649
+ *
1650
+ * Watches a single consent parameter across *every* visitor's submission (not just one
1651
+ * 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,
1654
+ * downstream webhooks) that need to react to consent decisions without polling the database.
1655
+ *
1656
+ * There is no server equivalent of the widget's `ConsentScript` — injecting a `<script>` tag
1657
+ * only makes sense in a browser DOM the server doesn't have. `services.consent.create()`/
1658
+ * `.update()` are already the server-side "submit"/"update consent" methods this hook reacts to.
1659
+ *
1660
+ * @example
1661
+ * ```ts
1662
+ * import { createConsenti, ConsentAction } from '@consenti/api'
1663
+ *
1664
+ * const { eventBus } = createConsenti({ ... })
1665
+ *
1666
+ * new ConsentAction({
1667
+ * id: 'analytics_storage',
1668
+ * eventBus,
1669
+ * onGrant: ({ visitorId }) => crm.optIn(visitorId),
1670
+ * onDeny: ({ visitorId }) => crm.optOut(visitorId),
1671
+ * })
1672
+ * ```
1673
+ */
1674
+
1675
+ /** Passed to `onGrant`/`onDeny` — the transitioning parameter's status plus the full consent record. */
1676
+ interface ConsentActionParams {
1677
+ visitorId: string;
1678
+ cookieId: string;
1679
+ status: ConsentStatus;
1680
+ record: ConsentDbRecord;
1681
+ }
1682
+ /** Options for constructing a {@link ConsentAction} instance. */
1683
+ interface ConsentActionOptions {
1684
+ /** The consent parameter ID to watch (e.g. `'analytics_storage'`). */
1685
+ id: string;
1686
+ /** The `eventBus` returned by `createConsenti()`. */
1687
+ eventBus: EventEmitter;
1688
+ /** Called when the parameter's status transitions to `'granted'`, for any visitor. */
1689
+ onGrant?: (params: ConsentActionParams) => void;
1690
+ /** Called when the parameter's status transitions away from `'granted'` (`'denied'` or `'objected'`). */
1691
+ onDeny?: (params: ConsentActionParams) => void;
1692
+ }
1693
+ /**
1694
+ * Watches a single consent parameter across every incoming `consent.created`/`consent.updated`
1695
+ * event and fires `onGrant`/`onDeny` on transitions. Call {@link ConsentAction.destroy} to
1696
+ * remove the event listeners when the hook is no longer needed.
1697
+ */
1698
+ declare class ConsentAction {
1699
+ private readonly options;
1700
+ private readonly onCreated;
1701
+ private readonly onUpdated;
1702
+ constructor(options: ConsentActionOptions);
1703
+ private fire;
1704
+ /** Removes the event listeners. Call when the hook is no longer needed. */
1705
+ destroy(): void;
1706
+ }
1707
+
1708
+ /**
1709
+ * CategoryAction — server-side counterpart to `@consenti/ui`'s `CategoryAction`.
1710
+ *
1711
+ * Same idea as {@link ConsentAction} but watches a whole category's rollup consent state
1712
+ * (granted only when *every* cookie in the category is granted) instead of one parameter —
1713
+ * matching the preference modal's tri-state rollup rule and the widget's `CategoryAction`.
1714
+ *
1715
+ * @example
1716
+ * ```ts
1717
+ * import { createConsenti, CategoryAction } from '@consenti/api'
1718
+ *
1719
+ * const { eventBus, services } = createConsenti({ ... })
1720
+ *
1721
+ * new CategoryAction({
1722
+ * categoryId: 'marketing',
1723
+ * eventBus,
1724
+ * profiles: services.profile,
1725
+ * onGrant: ({ visitorId }) => adsPlatform.optIn(visitorId),
1726
+ * onDeny: ({ visitorId }) => adsPlatform.optOut(visitorId),
1727
+ * })
1728
+ * ```
1729
+ */
1730
+
1731
+ /** Passed to `onGrant`/`onDeny` — the category's rollup status plus the full consent record. */
1732
+ interface CategoryActionParams {
1733
+ visitorId: string;
1734
+ categoryId: string;
1735
+ status: ConsentStatus;
1736
+ record: ConsentDbRecord;
1737
+ }
1738
+ /** Options for constructing a {@link CategoryAction} instance. */
1739
+ interface CategoryActionOptions {
1740
+ /** The category ID to watch (e.g. `'marketing'`). */
1741
+ categoryId: string;
1742
+ /** The `eventBus` returned by `createConsenti()`. */
1743
+ eventBus: EventEmitter;
1744
+ /** The `services.profile` returned by `createConsenti()` — resolves which cookies belong to
1745
+ * this category for the record's profile (categories are defined on the consent template). */
1746
+ profiles: ProfileService;
1747
+ /** Called when every cookie in the category transitions to `'granted'`. */
1748
+ onGrant?: (params: CategoryActionParams) => void;
1749
+ /** Called when the category transitions away from fully granted. */
1750
+ onDeny?: (params: CategoryActionParams) => void;
1751
+ }
1752
+ /**
1753
+ * Watches a category's rollup consent status across every incoming `consent.created`/
1754
+ * `consent.updated` event and fires `onGrant`/`onDeny` on transitions. Call
1755
+ * {@link CategoryAction.destroy} to remove the event listeners when no longer needed.
1756
+ */
1757
+ declare class CategoryAction {
1758
+ private readonly options;
1759
+ private readonly onCreated;
1760
+ private readonly onUpdated;
1761
+ constructor(options: CategoryActionOptions);
1762
+ private fire;
1763
+ /** Removes the event listeners. Call when the hook is no longer needed. */
1764
+ destroy(): void;
1057
1765
  }
1058
1766
 
1059
1767
  declare const dashboardDir: string;
1060
1768
  declare function createConsenti(userConfig: ConsentiServerConfig): {
1061
1769
  router: (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => void;
1062
1770
  handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
1063
- fastifyPlugin: (fastify: {
1771
+ fastifyHandler: (fastify: {
1064
1772
  all: (path: string, handler: (req: {
1065
1773
  raw: IncomingMessage;
1066
1774
  }, reply: {
@@ -1078,8 +1786,9 @@ declare function createConsenti(userConfig: ConsentiServerConfig): {
1078
1786
  user: UserService;
1079
1787
  };
1080
1788
  storage: StorageAdapter;
1081
- eventBus: EventEmitter<[never]>;
1789
+ eventBus: EventEmitter<any>;
1790
+ ready: Promise<void>;
1082
1791
  destroy: () => Promise<void>;
1083
1792
  };
1084
1793
 
1085
- export { type AdminUser, type AuditLog, 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 };
1794
+ 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 };