@consenti/ui 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,32 @@ type DeepPartial<T> = T extends Function ? T : T extends Array<infer InferredArr
11
11
  [Key in keyof T]?: DeepPartial<T[Key]> | null;
12
12
  } : T;
13
13
 
14
+ /**
15
+ * Single source of truth for all compliance groups, regulations, and country mappings.
16
+ *
17
+ * This is DATA, not types. The `Compliance` and `ComplianceGroupId` literal types
18
+ * derived from the ID arrays below live in @consenti/types (packages/types/src/compliance.ts),
19
+ * via a type-only import — that keeps @consenti/types free of runtime code.
20
+ *
21
+ * ─── HOW TO UPDATE ────────────────────────────────────────────────────────────
22
+ *
23
+ * ADD A NEW REGULATION:
24
+ * 1. Add regulation ID string to `COMPLIANCE_REGULATION_IDS` below
25
+ * (the `Compliance` type in @consenti/types auto-updates from it).
26
+ * 2. Add it to the appropriate group's `compliances` array below.
27
+ * 3. Add / update the country entry in `EMBEDDED_COMPLIANCE_MAP` below.
28
+ * That is all — no other files need touching.
29
+ *
30
+ * ADD A NEW COMPLIANCE GROUP:
31
+ * 1. Add the kebab-case ID to `COMPLIANCE_GROUP_IDS` below.
32
+ * 2. Add a full group entry to `COMPLIANCE_GROUPS` below (label, compliances, flags).
33
+ * 3. Add a validation rule set to `COMPLIANCE_VALIDATION_RULES` below.
34
+ * 4. Map affected countries in `EMBEDDED_COMPLIANCE_MAP` to the new group.
35
+ *
36
+ * ADD OR UPDATE A COUNTRY:
37
+ * Edit the entry in `EMBEDDED_COMPLIANCE_MAP.countries` below. That is the ONLY
38
+ * place that needs changing for country-level jurisdiction updates.
39
+ */
14
40
  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"];
15
41
  declare const COOKIE_PURPOSE_IDS: readonly ["necessary", "functional", "preferences", "analytics", "marketing"];
16
42
 
@@ -42,11 +68,14 @@ interface ComplianceMapData {
42
68
  }
43
69
  interface CountryComplianceEntry {
44
70
  complianceGroup: ComplianceGroupId;
45
- default: ComplianceGroupId;
46
- description: string;
71
+ /** Fallback group used when only the country (not the region) is known. Defaults to
72
+ * `complianceGroup` when omitted — only set this explicitly when a region within this
73
+ * country can be stricter than the country-wide baseline (see `overriddenRegions`). */
74
+ default?: ComplianceGroupId;
75
+ description?: string;
47
76
  overriddenRegions?: Record<string, {
48
77
  complianceGroup: ComplianceGroupId;
49
- description: string;
78
+ description?: string;
50
79
  }>;
51
80
  }
52
81
  interface ConsentDbRecord {
@@ -61,9 +90,10 @@ interface ConsentDbRecord {
61
90
  ageVerified?: boolean;
62
91
  parentalConsentToken?: string;
63
92
  tcfString?: string;
64
- /** HMAC-SHA256 signature over the record's core fields, hex-encoded. Only present when
65
- * `consentSigningKey` is configured — opt-in tamper-evidence for server-stored records,
66
- * independent of the widget's own cookie-signing (`core.cookieSigningKey`). */
93
+ gppString?: string;
94
+ /** HMAC-SHA256 signature over the record's core fields, hex-encoded — tamper-evidence for
95
+ * server-stored records via `dataSigningHash` (auto-generated when unset), independent of the
96
+ * widget's own cookie-signing (`core.cookieSigningKey`). */
67
97
  signature?: string;
68
98
  createdAt: string;
69
99
  updatedAt: string;
@@ -74,15 +104,18 @@ type ConsentAction = 'accept_all' | 'reject_all' | 'custom' | 'update';
74
104
  type ConsentValue = Record<string, ConsentStatus>;
75
105
  /**
76
106
  * Output format for getConsent(type).
77
- * 'default' → raw ConsentValue (existing behaviour)
107
+ * 'default' → raw ConsentValue, keyed by every cookie parameter ID in the profile (existing behaviour)
78
108
  * 'google-gtm' → Google Consent Mode v2 keys + ads_data_redaction, url_passthrough
79
- * 'category' → consent per CookiePurpose category
109
+ * 'purpose' → consent per the fixed CookiePurpose taxonomy (necessary/functional/preferences/analytics/marketing)
110
+ * 'category' → consent per the tenant's own authored preference-modal category, keyed by category ID.
111
+ * 'granted' only when every member parameter is granted; 'denied'/'objected' otherwise
112
+ * (same ConsentStatus values as everything else — see getCategoryConsent for the exact rule)
80
113
  * 'adobe' → { analytics, target, manager, optimizer }
81
114
  * 'meta' → { pixel, api, plugins, facebookLogin }
82
115
  * 'microsoft-clarity' → { session, heatmaps, performance }
83
116
  * 'twilio-segment' → { identify, page, track, group, alias }
84
117
  */
85
- type ConsentType = 'default' | 'google-gtm' | 'category' | 'adobe' | 'meta' | 'microsoft-clarity' | 'twilio-segment';
118
+ type ConsentType = 'default' | 'google-gtm' | 'purpose' | 'category' | 'adobe' | 'meta' | 'microsoft-clarity' | 'twilio-segment';
86
119
  type GpcMode = 'ignore' | 'honor' | 'strict';
87
120
  type LegalBasis = 'mandatory' | 'consent' | 'legitimate_interest';
88
121
  type CookiePurpose = typeof COOKIE_PURPOSE_IDS[number];
@@ -170,12 +203,46 @@ interface PreferenceModal extends Omit<MainBanner, "position"> {
170
203
  receiptLabel?: string;
171
204
  /** Description shown beneath the consent-receipt checkbox. Falls back to a default when omitted. */
172
205
  receiptDescription?: string;
206
+ /**
207
+ * Renders a "Forget me" control in the modal body, below the categories — only shown to
208
+ * visitors who already have a stored consent decision (nothing to erase otherwise). Wired to
209
+ * the same `DELETE /consent/:visitorId` erasure endpoint as `widget.deleteConsent()`, giving
210
+ * visitors self-service access to GDPR Art. 17 / CCPA-CPRA / LGPD Art. 18 / UK GDPR / DPDPA /
211
+ * KVKK / POPIA / PDPA-TH / APPI erasure-equivalent rights without the host wiring a link
212
+ * themselves. See the "Right to erasure" guide for the full compliance mapping.
213
+ */
214
+ showForgetMe?: boolean;
215
+ /** Label for the "Forget me" button (shown when `showForgetMe` is true). Falls back to a default when omitted. */
216
+ forgetMeLabel?: string;
173
217
  }
174
218
  interface DpdpaConfig {
175
219
  dataFiduciary: string;
176
220
  grievanceEmail: string;
177
221
  purposeDescription?: string;
178
222
  }
223
+ /**
224
+ * Per-profile age gate — GDPR Art. 8's consent age varies 13–16 by EU member state and DPDPA has
225
+ * its own child-data age rules, so one global age (the old `ConsentiServerConfig.ageGate`) was
226
+ * wrong for a multi-region deployment. Mirrors `dpdpa` exactly: profile-scoped, not global.
227
+ */
228
+ interface AgeGateConfig {
229
+ enabled: boolean;
230
+ minimumAge: number;
231
+ requireParentalConsent?: boolean;
232
+ }
233
+ /** Per-locale age-gate modal text — translated the same way `mainBanner`/`preferenceModal`
234
+ * content is, unlike the widget's own hardcoded UI chrome (close button, locale switcher). */
235
+ interface AgeGateModalContent {
236
+ heading?: string;
237
+ htmlText: string;
238
+ confirmButtonLabel: string;
239
+ denyButtonLabel: string;
240
+ parentalConsent: {
241
+ heading?: string;
242
+ htmlText: string;
243
+ confirmButtonLabel: string;
244
+ };
245
+ }
179
246
  interface ResolvedProfile {
180
247
  id: string;
181
248
  /** Incremented in place on every save of this profile. Absent for embedded/local profiles that have no server-side version counter. */
@@ -191,9 +258,21 @@ interface ResolvedProfile {
191
258
  preferenceModal: PreferenceModal;
192
259
  cookieSigningKey?: string;
193
260
  dpdpa?: DpdpaConfig;
261
+ ageGate?: AgeGateConfig;
262
+ ageGateModal?: AgeGateModalContent;
194
263
  darkMode?: boolean;
195
264
  gpcMode?: GpcMode;
196
- complianceGroup?: ComplianceGroupId;
265
+ /** One of the 8 built-in groups, or a custom group id (via `customComplianceGroup` /
266
+ * `ComplianceType`'s string passthrough) — this is the join key `complianceGroupsOverride`
267
+ * looks up by, so it must reflect whichever group this profile actually resolved against,
268
+ * built-in or custom. */
269
+ complianceGroup?: ComplianceGroupId | (string & {});
270
+ /** Set when the geo-resolved region carries a `requiresSensitiveOptIn` carve-out (e.g.
271
+ * Colorado under `opt-out`) — cookies tagged `cpraCategory: 'sensitive'` default to denied
272
+ * even though the rest of `complianceGroup`'s cookies default to granted. Only ever set by
273
+ * server-side geo resolution (`api.enabled: true` + a geoip/maxmind provider); the
274
+ * client-only timezone heuristic never resolves a region, so this is always unset there. */
275
+ requiresSensitiveOptIn?: boolean;
197
276
  complianceConfig?: Record<string, string>;
198
277
  /** Falls back to `true` (hidden) if neither the widget config nor the profile set it. */
199
278
  hidePoweredBy?: boolean;
@@ -206,26 +285,66 @@ type WidgetCountryResolverFn = () => Promise<{
206
285
  country: string | null;
207
286
  region: string | null;
208
287
  confidence: number;
288
+ /** Optional escape hatch: when set, profile resolution uses this compliance group directly
289
+ * instead of computing one from `country`/`region` — mirrors the server-side `GeoResult`'s
290
+ * `complianceGroup` field (`@consenti/types`'s `api.ts`). `country`/`region` are still used for
291
+ * anything that reads them independently of group selection. Optional — omitting it preserves
292
+ * the existing timezone/language-based resolution exactly as before. */
293
+ complianceGroup?: string;
209
294
  }>;
210
- interface AgeGateWidgetConfig {
211
- enabled: boolean;
212
- minimumAge: number;
213
- requireParentalConsent?: boolean;
214
- }
215
295
  /** Mirrors the server-side `TcfConfig` shape (`@consenti/api`) — `cmpId`/`cmpVersion` must match
216
- * what the backend uses to encode `tcfString`, since both feed the same simplified TC string. */
296
+ * what the backend uses to encode `tcfString`, since both feed the same simplified TC string.
297
+ *
298
+ * The client-side `__tcfapi` stub never fetches the Global Vendor List itself (multi-megabyte,
299
+ * and IAB policy requires CMPs to cache their own copy rather than have every visitor's browser
300
+ * fetch it) — `publisherCC`/`vendorListVersion`/`gvlVersion` are therefore operator-supplied
301
+ * config here, not auto-derived. Spec-correct binary TC-string encoding happens server-side in
302
+ * `@consenti/api` (see `compliance.tcf.publisherCC` there); this stub's `tcString` stays the
303
+ * simplified format documented in `@consenti/utils`'s `encodeTcString`. */
217
304
  interface TcfWidgetConfig {
218
305
  enabled: boolean;
219
306
  cmpId: number;
220
307
  cmpVersion: number;
308
+ /** ISO 3166-1 alpha-2 publisher country code. No honest default exists — leave unset only if
309
+ * you don't rely on this field downstream; ad-tech vendors that read it will see 'AA'
310
+ * (an explicit "not configured" placeholder, not a real country). */
311
+ publisherCC?: string;
312
+ /** Known Global Vendor List version, if you track it. Defaults to 0 ("unknown"). */
313
+ vendorListVersion?: number;
314
+ /** Known GVL specification version, if you track it. Defaults to 0 ("unknown"). */
315
+ gvlVersion?: number;
316
+ }
317
+ /** Mirrors the server-side `GppConfig` shape (`@consenti/api`) — `cmpId`/`cmpVersion` must match
318
+ * what the backend uses to encode `gppString`. Unlike TCF, the GPP US National section needs no
319
+ * Global Vendor List, so both the widget's `window.__gpp` and the server's `gppString` use the
320
+ * same real, spec-correct encoder (the optional `@iabgpp/cmpapi` peer dependency) — there's no
321
+ * "simplified fallback" format the way TCF has, since a non-spec GPP string has no consumer that
322
+ * would accept it. When `@iabgpp/cmpapi` isn't installed, `window.__gpp` is simply not exposed. */
323
+ interface GppWidgetConfig {
324
+ enabled: boolean;
325
+ cmpId: number;
326
+ cmpVersion: number;
327
+ /** MSPA "covered transaction" — whether this deployment's data transactions fall under MSPA
328
+ * signatory obligations. No honest default, so it's required rather than optional. */
329
+ mspaCoveredTransaction: boolean;
330
+ /** IAB's tri-state encoding for both MSPA fields: 0 = not applicable, 1 = yes, 2 = no. */
331
+ mspaOptOutOptionMode: 0 | 1 | 2;
332
+ mspaServiceProviderMode: 0 | 1 | 2;
221
333
  }
222
334
  interface ComplianceWidgetConfig {
223
335
  type?: ComplianceType;
224
336
  geoDataProvider?: 'default' | WidgetCountryResolverFn;
225
- autoComplianceMap?: 'default' | ComplianceMapData | 'auto';
226
- complianceMapUrl?: string;
227
- ageGate?: AgeGateWidgetConfig;
337
+ /** Only meaningful in standalone mode (no `api.enabled`) — ignored (with a warning) when
338
+ * `api.enabled: true`, since the server resolves the compliance group in that mode.
339
+ * `'default'` (embedded map, the default), a URL to fetch a JSON `ComplianceMapData`
340
+ * document from (the browser's own HTTP cache honors whatever `Cache-Control`/`ETag` the
341
+ * response sends — no custom caching here), or an inline operator-supplied `ComplianceMapData`
342
+ * object. Invalid data from either a URL or an object logs a warning and falls back to
343
+ * `'default'`. Only overrides the country→group mapping — country/region detection itself
344
+ * always uses the embedded geo data. */
345
+ complianceMap?: 'default' | string | ComplianceMapData;
228
346
  tcf?: TcfWidgetConfig;
347
+ gpp?: GppWidgetConfig;
229
348
  }
230
349
  interface GtmConfig {
231
350
  containerId?: string;
@@ -246,37 +365,67 @@ interface ApiConfig {
246
365
  complianceGroup?: ComplianceGroupId;
247
366
  trustDomain?: boolean;
248
367
  }
368
+ /** Field names mirror their `--consenti-*` CSS variable literally (`--consenti-color-primary`
369
+ * → `colorPrimary`) — see `applyTheme()` in `consenti-setup.ts` for the full mapping. */
249
370
  interface ThemeConfig {
250
- bgColor?: string;
251
- textColor?: string;
252
- primaryColor?: string;
253
- primaryTextColor?: string;
254
- secondaryColor?: string;
255
- secondaryTextColor?: string;
256
- borderColor?: string;
371
+ colorBg?: string;
372
+ colorText?: string;
373
+ colorTextMuted?: string;
374
+ colorPrimary?: string;
375
+ colorPrimaryText?: string;
376
+ colorSecondary?: string;
377
+ colorSecondaryText?: string;
378
+ colorBorder?: string;
379
+ colorSecondaryBorder?: string;
380
+ colorOverlay?: string;
381
+ colorAccent?: string;
382
+ colorAccentText?: string;
257
383
  fontFamily?: string;
384
+ fontFamilyMono?: string;
258
385
  fontSizeBase?: string;
259
386
  fontSizeHeading?: string;
260
387
  fontSizeMultiplier?: string;
388
+ fontWeightHeading?: string;
389
+ lineHeight?: string;
390
+ spacingXs?: string;
391
+ spacingSm?: string;
392
+ spacingMd?: string;
393
+ spacingLg?: string;
261
394
  borderRadius?: string;
262
- buttonBorderRadius?: string;
395
+ borderRadiusBtn?: string;
396
+ shadow?: string;
263
397
  toggleBgOn?: string;
398
+ toggleBgPartial?: string;
264
399
  toggleBgOff?: string;
265
- accentColor?: string;
266
- accentTextColor?: string;
400
+ toggleKnob?: string;
401
+ toggleWidth?: string;
402
+ toggleHeight?: string;
403
+ zBanner?: string;
404
+ zOverlay?: string;
405
+ zModal?: string;
267
406
  }
268
407
  interface CoreConfig {
269
408
  tenantId?: string;
270
409
  locale?: string;
271
- /** Text direction for the banner/modal root. `'auto'` (default) derives it from `locale`
272
- * (Arabic, Hebrew, Persian, Urdu, ... → `rtl`); set explicitly to override. */
410
+ /** Text direction for the banner/modal root. `'auto'` (default) derives it from `locale` via
411
+ * `Intl.Locale(...).getTextInfo().direction` (falling back to a hand-maintained RTL-language
412
+ * list on engines without `getTextInfo()` support); set explicitly to override. */
273
413
  dir?: 'ltr' | 'rtl' | 'auto';
274
414
  disableCssTemplate?: boolean;
275
415
  cookieSigningKey?: string;
276
416
  allowReceipt?: boolean;
277
417
  cookieDomains?: string;
418
+ /** Name of the consent cookie/localStorage key. Default: `'consenti_data'`. Not switched
419
+ * automatically by detected region — set explicitly if you want a different name (e.g.
420
+ * `'euconsent-v2'`, the IAB TCF convention, but only meaningful for operators using the
421
+ * spec-correct binary encoder — `@consenti/api` + the optional `@iabtechlabtcf/core` peer
422
+ * dependency — since the IAB name implies IAB's binary format, not Consenti's own encoding). */
423
+ cookieName?: string;
278
424
  storage?: 'cookie' | 'localStorage';
279
425
  theme?: ThemeConfig;
426
+ /** Initial logged-in application user ID; empty/anonymous when unset. Prefer `getUserId()`/
427
+ * `setUserId()` (or the `consenti:listener:identify` event) to change it after init — those
428
+ * also handle reconsent when the identity changes on a shared device. */
280
429
  userId?: string;
281
430
  usePrebuiltProfiles?: 'all' | NonEmptyArray<ComplianceGroupId>;
282
431
  cacheResolvedProfiles?: boolean;
@@ -296,10 +445,25 @@ interface ConsentiConfig {
296
445
  utils?: UtilsConfig;
297
446
  plugins?: ConsentiPlugin[];
298
447
  profileOverride?: DeepPartial<ResolvedProfile>;
448
+ /** Per-compliance-group text/button/cookie overrides — applied on top of whichever profile the
449
+ * visitor's resolved group loads, keyed by that group's id (one of the 8 built-in
450
+ * `ComplianceGroupId`s, or any custom group id). Lets you author different copy per country
451
+ * without creating a separate profile (and separate `compliance.type` wiring) for each one —
452
+ * e.g. `{ 'opt-in': { mainBanner: { htmlText: '...' } }, 'my-custom-group': { ... } }`.
453
+ * Applied before `profileOverride`, which still always applies last as the universal catch-all.
454
+ * Works identically in standalone and server (`api.enabled`) mode. */
455
+ complianceGroupsOverride?: Partial<Record<ComplianceGroupId | (string & {}), DeepPartial<ResolvedProfile>>>;
299
456
  /** When true, suppresses the "Powered by Consenti" footer link in the banner and modal. */
300
457
  hidePoweredBy?: boolean;
301
458
  }
302
- type ConsentiEventName = 'bannerInitialized' | 'consenti:bannerInitialized' | 'bannerVisibility' | 'consenti:bannerVisibility' | 'modalVisibility' | 'consenti:modalVisibility' | 'consentBeingSubmitted' | 'consenti:consentBeingSubmitted' | 'consentSubmitted' | 'consenti:consentSubmitted' | 'parentalConsentRequired' | 'consenti:parentalConsentRequired';
459
+ type ConsentiEventName = 'bannerInitialized' | 'consenti:bannerInitialized' | 'bannerVisibility' | 'consenti:bannerVisibility' | 'modalVisibility' | 'consenti:modalVisibility' | 'consentBeingSubmitted' | 'consenti:consentBeingSubmitted' | 'consentSubmitted' | 'consenti:consentSubmitted' | 'parentalConsentRequired' | 'consenti:parentalConsentRequired' | 'forgetMeRequested' | 'consenti:forgetMeRequested' | 'forgotten' | 'consenti:forgotten';
460
+ /** Snapshot of the current visitor's identity. `visitorId` is `null` until a consent decision
461
+ * has actually happened — it's never minted just to answer this call, see `getVisitor()`. */
462
+ interface VisitorIdentity {
463
+ visitorId: string | null;
464
+ type: 'authenticated' | 'anonymous';
465
+ userId: string | null;
466
+ }
303
467
  interface ConsentiWidgetAPI {
304
468
  hasConsent(): boolean;
305
469
  getConsent(): ConsentValue | null;
@@ -330,10 +494,25 @@ interface ConsentiWidgetAPI {
330
494
  submitConsent(consent: Partial<ConsentValue>): Promise<ConsentDbRecord | void>;
331
495
  deleteConsent(): Promise<void>;
332
496
  reConsent(): Promise<void>;
497
+ /** Erases the stored consent record (same underlying call as `deleteConsent()`) and
498
+ * re-prompts, dispatching `consenti:forgetMeRequested` / `consenti:forgotten` around it so a
499
+ * host app can hook its own identity-verified erasure workflow. Powers the preference modal's
500
+ * "Forget me" button; also callable directly for a custom placement of the same action. */
501
+ forgetMe(resetAgeGate?: boolean): Promise<void>;
333
502
  getRootElement(): HTMLElement | null;
334
503
  getBannerElement(): HTMLElement | null;
335
504
  getModalElement(): HTMLElement | null;
336
505
  switchLocale(locale: string): void;
506
+ getUserId(): string | null;
507
+ /** Sets the logged-in application user ID (`null` for anonymous/logout). When the stored
508
+ * consent record's user differs from the new value, reconsents by default — pass
509
+ * `reConsent: false` to just update the identity without prompting again. No-ops (no
510
+ * reconsent, no warning) when there's no prior consent record to compare against. */
511
+ setUserId(userId: string | null, reConsent?: boolean): Promise<void>;
512
+ /** Snapshot of the current visitor's identity: the stable per-browser `visitorId` (`null` if
513
+ * no consent decision has happened yet — this never creates one), whether they're
514
+ * `authenticated` or `anonymous`, and the app `userId` (same as `getUserId()`). */
515
+ getVisitor(): VisitorIdentity;
337
516
  setDarkMode(enable?: boolean): void;
338
517
  setTheme(theme: Partial<ThemeConfig>): void;
339
518
  setConfig(config: DeepPartial<ConsentiConfig>): void;
@@ -381,17 +560,45 @@ interface ConsentBeingSubmitted {
381
560
  interface ConsentSubmittedDetail extends ConsentBeingSubmitted {
382
561
  apiResponse: ConsentDbRecord;
383
562
  }
384
- /** Dispatched when the age gate is declined and `requireParentalConsent` is set — see
385
- * `AgeGateWidgetConfig`. A deny-all consent (mandatory cookies only) has already been
563
+ /** Dispatched when the age gate is declined and `requireParentalConsent` is set — see the
564
+ * profile's `AgeGateConfig`. A deny-all consent (mandatory cookies only) has already been
386
565
  * submitted with `parentalConsentToken` attached; this event is the hook for the site
387
- * owner to run their own out-of-band parental-consent verification. */
566
+ * owner to run their own out-of-band parental-consent verification. `parentalConsentToken` is
567
+ * server-issued (and signed, when `compliance.dataSigningHash` is configured) when `api.enabled`,
568
+ * otherwise a client-generated, unsigned fallback — see `POST /consent/:visitorId/parental-consent-request`. */
388
569
  interface ParentalConsentRequiredDetail {
389
570
  parentalConsentToken: string;
390
571
  profileId: string;
391
572
  visitorId: string;
392
573
  timestamp: number;
393
574
  }
394
- type ConsentEvent = BannerInitializedDetail | BannerVisibilityDetail | ModalVisibilityDetail | ConsentBeingSubmitted | ConsentSubmittedDetail | ParentalConsentRequiredDetail;
575
+ /** Dispatched by the standalone `resolveParentalConsent()` util (`apps/ui/src/utils/parental-consent.ts`)
576
+ * after `POST /consent/parental-consent-resolve` succeeds — typically from a fresh page/session
577
+ * with no live `ConsentiSetup` instance (the parent's own device, not the child's). A page that
578
+ * does have a live widget instance can listen for this to react (e.g. re-show the banner/modal for
579
+ * the real consent choice); what "resolved" should actually do to consent state is host-defined. */
580
+ interface ParentalConsentGrantedDetail {
581
+ visitorId: string;
582
+ profileId: string;
583
+ }
584
+ /** Dispatched by `forgetMe()` right before the erasure call goes out — the hook for a host app
585
+ * to kick off its own identity-verified erasure workflow across other systems (CRM, DMP,
586
+ * analytics). The CMP itself only ever erases its own consent record; see the server-side
587
+ * `consent.erased` eventBus event (fired by `DELETE /consent/:visitorId`) for the same hook on
588
+ * the backend. See the "Right to erasure" guide for the full picture. */
589
+ interface ForgetMeRequestedDetail {
590
+ visitorId: string;
591
+ profileId: string;
592
+ timestamp: number;
593
+ }
594
+ /** Dispatched by `forgetMe()` after the consent record has been erased and the banner/age-gate
595
+ * has been re-prompted (see `reConsent()`, which `forgetMe()` wraps). */
596
+ interface ForgottenDetail {
597
+ visitorId: string;
598
+ profileId: string;
599
+ timestamp: number;
600
+ }
601
+ type ConsentEvent = BannerInitializedDetail | BannerVisibilityDetail | ModalVisibilityDetail | ConsentBeingSubmitted | ConsentSubmittedDetail | ParentalConsentRequiredDetail | ParentalConsentGrantedDetail | ForgetMeRequestedDetail | ForgottenDetail;
395
602
 
396
603
  /**
397
604
  * Main entry point for the Consenti widget.
@@ -415,6 +622,7 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
415
622
  private initializing;
416
623
  private profile;
417
624
  private resolvedBase;
625
+ private profilePatches;
418
626
  /** cookieId → Category lookup, rebuilt whenever `profile` changes. Avoids rescanning categories on every consent action. */
419
627
  private categoryIndex;
420
628
  /** Watchers created by the declarative `data-consenti-*` DOM scan — destroyed alongside the widget since they have no other owner. */
@@ -447,6 +655,8 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
447
655
  private _bannerMounted;
448
656
  /** Active focus trap keydown listener — removed when modal closes. */
449
657
  private _focusTrapListener;
658
+ /** Inbound `consenti:listener:identify` window listener — installed once per init(), removed on destroy(). */
659
+ private _identifyListener;
450
660
  /** Maps original handler functions to their DOM-wrapped listeners for on/off bookkeeping. */
451
661
  private _eventHandlers;
452
662
  /** `core` defaulted to `{}` in the constructor below — every one of its own fields is
@@ -463,6 +673,29 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
463
673
  */
464
674
  constructor(config: ConsentiConfig);
465
675
  private profileId;
676
+ /** Patches `base` with `complianceGroupsOverride[base.complianceGroup]`, if both exist — the
677
+ * per-group counterpart to `profileOverride`. Lets an author supply different text/buttons/
678
+ * cookies per compliance group (built-in or custom) without authoring a separate profile for
679
+ * each one. Applied before `profileOverride`, which still always wins as the final catch-all. */
680
+ private applyComplianceGroupOverride;
681
+ /**
682
+ * Applied in order: compliance-group override → `config.profileOverride` → each `setProfile()`
683
+ * patch. The `setProfile()` patches are kept as a list and replayed, not folded into one
684
+ * object, because folding runs `deepMerge` patch-onto-patch, which consumes `null`/`'*': null`
685
+ * delete markers before they ever reach the profile they were meant to delete from.
686
+ */
687
+ private applyProfileOverrides;
688
+ private warnIfAcceptOnly;
689
+ /**
690
+ * Whether `window.__tcfapi`/`window.__gpp` should stay uninstalled because the corresponding
691
+ * server-side registration (`compliance.tcf`/`compliance.gpp`) is unconfirmed (cmpId/cmpVersion
692
+ * changed since the dashboard's confirmation checkbox was last checked, or never confirmed).
693
+ * Standalone (no-backend) deployments have no governance layer to check against, so this always
694
+ * resolves `false` there — the client-side config is used as configured, same as before this
695
+ * feature existed. A network failure fails closed (`true`) rather than assuming the
696
+ * unverifiable state is fine.
697
+ */
698
+ private isRegistrationBlocked;
466
699
  /**
467
700
  * Starts widget initialisation. Called automatically unless `autoInit: false` is set.
468
701
  * Can be called explicitly after construction when `autoInit: false`, or after `destroy()`
@@ -476,7 +709,7 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
476
709
  * `resolveAgeGate()`, then resume here with `hasExisting` fixed at `false`.
477
710
  */
478
711
  private continueInit;
479
- /** Mounts the age-gate prompt (`compliance.ageGate`) in place of the normal banner flow. */
712
+ /** Mounts the age-gate prompt (`profile.ageGate`) in place of the normal banner flow. */
480
713
  private showAgeGate;
481
714
  /**
482
715
  * Handles the visitor's age-gate answer.
@@ -499,13 +732,35 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
499
732
  * a resolved profile to read consent from.
500
733
  */
501
734
  private finishInit;
735
+ /** Installs the inbound `consenti:listener:identify` window listener — the event-driven
736
+ * counterpart to `setUserId()`. Removes any previously-installed listener first so repeated
737
+ * `init()` calls (e.g. from `switchLocale()`) don't stack up duplicate listeners. */
738
+ private _installIdentifyListener;
739
+ /** Returns the current logged-in application user ID, or `null` for an anonymous visitor. */
740
+ getUserId(): string | null;
741
+ /**
742
+ * Sets the logged-in application user ID (`null` for anonymous/logout). If a stored consent
743
+ * record exists and its user differs from `userId`, this is treated as a different data
744
+ * subject on a shared device: by default it reconsents (deletes the existing record and
745
+ * re-shows the banner) and logs a warning. Pass `reConsent: false` to just update the identity
746
+ * without prompting again (still logs the warning). No-ops entirely — no warning, no
747
+ * reconsent — when there is no prior consent record to compare against (true first-time visitor).
748
+ */
749
+ setUserId(userId: string | null, reConsent?: boolean): Promise<void>;
750
+ /**
751
+ * Snapshot of the current visitor's identity. `visitorId` is the stable per-browser ID —
752
+ * `null` until a consent decision has actually happened, since this never mints one just to
753
+ * answer the call (see `getOrCreateVisitorId()`). `type` is `'authenticated'` whenever a
754
+ * `userId` is set (via `setUserId()` or `core.userId`), `'anonymous'` otherwise.
755
+ */
756
+ getVisitor(): VisitorIdentity;
502
757
  /** Sets the `dir` attribute on the root element from `core.dir` (default `'auto'`, derived from `core.locale`). */
503
758
  private applyDirection;
504
759
  /** Applies the `consenti-root--dark` class based on config or profile flag. */
505
760
  private applyDarkMode;
506
761
  /** Applies `consenti--enhanced-a11y` class when `profile.enhanceAccessibility` is set. */
507
762
  private applyAccessibility;
508
- /** Builds and appends the metadata footer strip to a banner or modal container. */
763
+ /** Builds and appends the metadata footer strip to modal container. */
509
764
  private _appendMetaFooter;
510
765
  /**
511
766
  * Injects a `<style>` element into the root for `stackButtonsOnBreakpoint`.
@@ -549,8 +804,16 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
549
804
  * Pass `type` to get a vendor-ready object instead of the raw Consenti format.
550
805
  * Always returns `null` during SSR or when no consent has been saved.
551
806
  *
552
- * @param type - Vendor format: 'google-gtm', 'category', 'adobe', 'meta',
553
- * 'microsoft-clarity', 'twilio-segment', or omit for raw format.
807
+ * @param type - Output format:
808
+ * - omit / `'default'` → raw ConsentValue, keyed by every cookie parameter ID in the profile
809
+ * - `'purpose'` → consent per the fixed CookiePurpose taxonomy (necessary/functional/preferences/analytics/marketing)
810
+ * - `'category'` → consent per the tenant's own authored preference-modal category, keyed by category ID
811
+ * ('granted' only when every member parameter is granted; 'denied'/'objected' otherwise)
812
+ * - `'google-gtm'` → Google Consent Mode v2 keys + ads_data_redaction, url_passthrough
813
+ * - `'adobe'` → { analytics, target, manager, optimizer }
814
+ * - `'meta'` → { pixel, api, plugins, facebookLogin }
815
+ * - `'microsoft-clarity'` → { session, heatmaps, performance }
816
+ * - `'twilio-segment'` → { identify, page, track, group, alias }
554
817
  */
555
818
  getConsent(): ConsentValue | null;
556
819
  getConsent(type: ConsentType): Record<string, string> | null;
@@ -642,8 +905,30 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
642
905
  * method so callers do not need to coordinate the async deletion with the banner display.
643
906
  *
644
907
  * Use this to power "Review my consent" links in footers or privacy dashboards.
908
+ *
909
+ * @param resetAgeGate - When `true` (default) and the profile has `ageGate.enabled`, the
910
+ * age-gate prompt is shown again instead of jumping straight to the
911
+ * banner — mirroring the original first-visit flow. Pass `false` to
912
+ * skip it and go straight to the banner, keeping the visitor's prior
913
+ * age-gate answer.
645
914
  */
646
- reConsent(): Promise<void>;
915
+ reConsent(resetAgeGate?: boolean): Promise<void>;
916
+ /**
917
+ * Self-service erasure entry point — GDPR Art. 17 / CCPA-CPRA / LGPD Art. 18 and equivalent
918
+ * "right to be forgotten" rights, exercised from the widget itself. Wraps `reConsent()` (same
919
+ * erase-then-reprompt behavior) with `consenti:forgetMeRequested` / `consenti:forgotten`
920
+ * dispatched around it, so a host app can hook its own identity-verified erasure workflow
921
+ * across other systems (CRM, DMP, analytics) — the CMP itself only ever erases its own consent
922
+ * record. See the "Right to erasure" guide for the full picture, including the server-side
923
+ * `consent:erased` eventBus event fired by `DELETE /consent/:visitorId`.
924
+ *
925
+ * Powers the preference modal's "Forget me" button (`preferenceModal.showForgetMe`, only
926
+ * rendered when the visitor already has a stored consent decision). Also callable directly —
927
+ * e.g. `widget.forgetMe()` from a custom footer link or privacy-settings page.
928
+ *
929
+ * @param resetAgeGate - Forwarded to `reConsent()` — see its docs.
930
+ */
931
+ forgetMe(resetAgeGate?: boolean): Promise<void>;
647
932
  /**
648
933
  * Returns the fully resolved profile for the current session, or `null` during the
649
934
  * async init phase (before the profile has been fetched/resolved).
@@ -822,14 +1107,21 @@ declare function injectConsent(): {
822
1107
  denyAll: (includingMandatory?: boolean) => Promise<void>;
823
1108
  /** Saves the given partial consent map. Missing cookie IDs default to their current stored value. */
824
1109
  submitConsent: (consent: Partial<ConsentValue>) => Promise<void | ConsentDbRecord>;
825
- /** Deletes the current consent record and re-shows the banner. */
826
- reConsent: () => Promise<void>;
1110
+ /** Deletes the current consent record and re-shows the banner. Pass `false` to skip
1111
+ * re-showing the age-gate prompt (if the profile has one) and go straight to the banner. */
1112
+ reConsent: (resetAgeGate?: boolean) => Promise<void>;
827
1113
  /** Returns `true` if the given cookie ID is `'granted'`. */
828
1114
  isCookieGranted: (id: string) => boolean;
829
1115
  /** Returns `true` if every cookie in the given category is `'granted'`. */
830
1116
  isCategoryGranted: (id: string) => boolean;
831
1117
  /** Switches the active locale and re-renders the banner and modal. */
832
1118
  switchLocale: (locale: string) => void | undefined;
1119
+ /** Returns the current logged-in application user ID, or `null` if anonymous. */
1120
+ getUserId: () => string | null;
1121
+ /** Sets the logged-in application user ID; reconsents by default when it changes. */
1122
+ setUserId: (userId: string | null, reConsent?: boolean) => Promise<void>;
1123
+ /** Snapshot of the current visitor's identity (`visitorId`, `type`, `userId`). */
1124
+ getVisitor: () => VisitorIdentity;
833
1125
  /**
834
1126
  * Subscribes to consent change events. Call in `ngOnInit`; return value unsubscribes
835
1127
  * in `ngOnDestroy`.