@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.
package/dist/index.d.ts CHANGED
@@ -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$1 = '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,11 +203,24 @@ 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 LocaleTranslations {
175
219
  mainBanner: DeepPartial<MainBanner>;
176
220
  gpcBanner?: DeepPartial<GpcBanner>;
177
221
  preferenceModal: DeepPartial<PreferenceModal>;
222
+ /** Only meaningful when the profile's `ageGate.enabled` is true. */
223
+ ageGateModal?: DeepPartial<AgeGateModalContent>;
178
224
  }
179
225
  interface ProfileTranslations {
180
226
  [locale: string]: LocaleTranslations;
@@ -184,6 +230,29 @@ interface DpdpaConfig {
184
230
  grievanceEmail: string;
185
231
  purposeDescription?: string;
186
232
  }
233
+ /**
234
+ * Per-profile age gate — GDPR Art. 8's consent age varies 13–16 by EU member state and DPDPA has
235
+ * its own child-data age rules, so one global age (the old `ConsentiServerConfig.ageGate`) was
236
+ * wrong for a multi-region deployment. Mirrors `dpdpa` exactly: profile-scoped, not global.
237
+ */
238
+ interface AgeGateConfig {
239
+ enabled: boolean;
240
+ minimumAge: number;
241
+ requireParentalConsent?: boolean;
242
+ }
243
+ /** Per-locale age-gate modal text — translated the same way `mainBanner`/`preferenceModal`
244
+ * content is, unlike the widget's own hardcoded UI chrome (close button, locale switcher). */
245
+ interface AgeGateModalContent {
246
+ heading?: string;
247
+ htmlText: string;
248
+ confirmButtonLabel: string;
249
+ denyButtonLabel: string;
250
+ parentalConsent: {
251
+ heading?: string;
252
+ htmlText: string;
253
+ confirmButtonLabel: string;
254
+ };
255
+ }
187
256
  interface LocaleTextContent {
188
257
  mainBanner: {
189
258
  heading?: string;
@@ -210,7 +279,11 @@ interface LocaleTextContent {
210
279
  }>;
211
280
  receiptLabel?: string;
212
281
  receiptDescription?: string;
282
+ /** Per-locale override of `PreferenceModal.forgetMeLabel`; only meaningful when `showForgetMe` is true. */
283
+ forgetMeLabel?: string;
213
284
  };
285
+ /** Only meaningful when the profile's `ageGate.enabled` is true. */
286
+ ageGateModal?: AgeGateModalContent;
214
287
  }
215
288
  interface ProfileConfig {
216
289
  id: string;
@@ -225,6 +298,12 @@ interface ProfileConfig {
225
298
  darkMode?: boolean;
226
299
  allowedOrigins?: string[];
227
300
  dpdpa?: DpdpaConfig;
301
+ ageGate?: AgeGateConfig;
302
+ /** Default locale's age-gate modal text — mirrors `mainBanner`/`preferenceModal` (inline,
303
+ * required-shape-when-present), not `dpdpa` (which has no locale text at all). Every other
304
+ * locale's `ageGateModal` lives in `localeContents`/`LocaleContentInput` like the rest of
305
+ * that locale's content. */
306
+ ageGateModal?: AgeGateModalContent;
228
307
  regulation?: 'gdpr' | 'ccpa' | 'cpra' | 'dpdpa' | 'uk-gdpr' | 'lgpd' | 'pipeda' | 'popia' | 'pdpa-th' | 'appi' | 'kvkk';
229
308
  regulations?: string[];
230
309
  consentTemplateId?: string;
@@ -299,9 +378,21 @@ interface ResolvedProfile {
299
378
  preferenceModal: PreferenceModal;
300
379
  cookieSigningKey?: string;
301
380
  dpdpa?: DpdpaConfig;
381
+ ageGate?: AgeGateConfig;
382
+ ageGateModal?: AgeGateModalContent;
302
383
  darkMode?: boolean;
303
384
  gpcMode?: GpcMode;
304
- complianceGroup?: ComplianceGroupId;
385
+ /** One of the 8 built-in groups, or a custom group id (via `customComplianceGroup` /
386
+ * `ComplianceType`'s string passthrough) — this is the join key `complianceGroupsOverride`
387
+ * looks up by, so it must reflect whichever group this profile actually resolved against,
388
+ * built-in or custom. */
389
+ complianceGroup?: ComplianceGroupId | (string & {});
390
+ /** Set when the geo-resolved region carries a `requiresSensitiveOptIn` carve-out (e.g.
391
+ * Colorado under `opt-out`) — cookies tagged `cpraCategory: 'sensitive'` default to denied
392
+ * even though the rest of `complianceGroup`'s cookies default to granted. Only ever set by
393
+ * server-side geo resolution (`api.enabled: true` + a geoip/maxmind provider); the
394
+ * client-only timezone heuristic never resolves a region, so this is always unset there. */
395
+ requiresSensitiveOptIn?: boolean;
305
396
  complianceConfig?: Record<string, string>;
306
397
  /** Falls back to `true` (hidden) if neither the widget config nor the profile set it. */
307
398
  hidePoweredBy?: boolean;
@@ -314,26 +405,66 @@ type WidgetCountryResolverFn = () => Promise<{
314
405
  country: string | null;
315
406
  region: string | null;
316
407
  confidence: number;
408
+ /** Optional escape hatch: when set, profile resolution uses this compliance group directly
409
+ * instead of computing one from `country`/`region` — mirrors the server-side `GeoResult`'s
410
+ * `complianceGroup` field (`@consenti/types`'s `api.ts`). `country`/`region` are still used for
411
+ * anything that reads them independently of group selection. Optional — omitting it preserves
412
+ * the existing timezone/language-based resolution exactly as before. */
413
+ complianceGroup?: string;
317
414
  }>;
318
- interface AgeGateWidgetConfig {
319
- enabled: boolean;
320
- minimumAge: number;
321
- requireParentalConsent?: boolean;
322
- }
323
415
  /** Mirrors the server-side `TcfConfig` shape (`@consenti/api`) — `cmpId`/`cmpVersion` must match
324
- * what the backend uses to encode `tcfString`, since both feed the same simplified TC string. */
416
+ * what the backend uses to encode `tcfString`, since both feed the same simplified TC string.
417
+ *
418
+ * The client-side `__tcfapi` stub never fetches the Global Vendor List itself (multi-megabyte,
419
+ * and IAB policy requires CMPs to cache their own copy rather than have every visitor's browser
420
+ * fetch it) — `publisherCC`/`vendorListVersion`/`gvlVersion` are therefore operator-supplied
421
+ * config here, not auto-derived. Spec-correct binary TC-string encoding happens server-side in
422
+ * `@consenti/api` (see `compliance.tcf.publisherCC` there); this stub's `tcString` stays the
423
+ * simplified format documented in `@consenti/utils`'s `encodeTcString`. */
325
424
  interface TcfWidgetConfig {
326
425
  enabled: boolean;
327
426
  cmpId: number;
328
427
  cmpVersion: number;
428
+ /** ISO 3166-1 alpha-2 publisher country code. No honest default exists — leave unset only if
429
+ * you don't rely on this field downstream; ad-tech vendors that read it will see 'AA'
430
+ * (an explicit "not configured" placeholder, not a real country). */
431
+ publisherCC?: string;
432
+ /** Known Global Vendor List version, if you track it. Defaults to 0 ("unknown"). */
433
+ vendorListVersion?: number;
434
+ /** Known GVL specification version, if you track it. Defaults to 0 ("unknown"). */
435
+ gvlVersion?: number;
436
+ }
437
+ /** Mirrors the server-side `GppConfig` shape (`@consenti/api`) — `cmpId`/`cmpVersion` must match
438
+ * what the backend uses to encode `gppString`. Unlike TCF, the GPP US National section needs no
439
+ * Global Vendor List, so both the widget's `window.__gpp` and the server's `gppString` use the
440
+ * same real, spec-correct encoder (the optional `@iabgpp/cmpapi` peer dependency) — there's no
441
+ * "simplified fallback" format the way TCF has, since a non-spec GPP string has no consumer that
442
+ * would accept it. When `@iabgpp/cmpapi` isn't installed, `window.__gpp` is simply not exposed. */
443
+ interface GppWidgetConfig {
444
+ enabled: boolean;
445
+ cmpId: number;
446
+ cmpVersion: number;
447
+ /** MSPA "covered transaction" — whether this deployment's data transactions fall under MSPA
448
+ * signatory obligations. No honest default, so it's required rather than optional. */
449
+ mspaCoveredTransaction: boolean;
450
+ /** IAB's tri-state encoding for both MSPA fields: 0 = not applicable, 1 = yes, 2 = no. */
451
+ mspaOptOutOptionMode: 0 | 1 | 2;
452
+ mspaServiceProviderMode: 0 | 1 | 2;
329
453
  }
330
454
  interface ComplianceWidgetConfig {
331
455
  type?: ComplianceType;
332
456
  geoDataProvider?: 'default' | WidgetCountryResolverFn;
333
- autoComplianceMap?: 'default' | ComplianceMapData | 'auto';
334
- complianceMapUrl?: string;
335
- ageGate?: AgeGateWidgetConfig;
457
+ /** Only meaningful in standalone mode (no `api.enabled`) — ignored (with a warning) when
458
+ * `api.enabled: true`, since the server resolves the compliance group in that mode.
459
+ * `'default'` (embedded map, the default), a URL to fetch a JSON `ComplianceMapData`
460
+ * document from (the browser's own HTTP cache honors whatever `Cache-Control`/`ETag` the
461
+ * response sends — no custom caching here), or an inline operator-supplied `ComplianceMapData`
462
+ * object. Invalid data from either a URL or an object logs a warning and falls back to
463
+ * `'default'`. Only overrides the country→group mapping — country/region detection itself
464
+ * always uses the embedded geo data. */
465
+ complianceMap?: 'default' | string | ComplianceMapData;
336
466
  tcf?: TcfWidgetConfig;
467
+ gpp?: GppWidgetConfig;
337
468
  }
338
469
  interface GtmConfig {
339
470
  containerId?: string;
@@ -354,37 +485,67 @@ interface ApiConfig {
354
485
  complianceGroup?: ComplianceGroupId;
355
486
  trustDomain?: boolean;
356
487
  }
488
+ /** Field names mirror their `--consenti-*` CSS variable literally (`--consenti-color-primary`
489
+ * → `colorPrimary`) — see `applyTheme()` in `consenti-setup.ts` for the full mapping. */
357
490
  interface ThemeConfig {
358
- bgColor?: string;
359
- textColor?: string;
360
- primaryColor?: string;
361
- primaryTextColor?: string;
362
- secondaryColor?: string;
363
- secondaryTextColor?: string;
364
- borderColor?: string;
491
+ colorBg?: string;
492
+ colorText?: string;
493
+ colorTextMuted?: string;
494
+ colorPrimary?: string;
495
+ colorPrimaryText?: string;
496
+ colorSecondary?: string;
497
+ colorSecondaryText?: string;
498
+ colorBorder?: string;
499
+ colorSecondaryBorder?: string;
500
+ colorOverlay?: string;
501
+ colorAccent?: string;
502
+ colorAccentText?: string;
365
503
  fontFamily?: string;
504
+ fontFamilyMono?: string;
366
505
  fontSizeBase?: string;
367
506
  fontSizeHeading?: string;
368
507
  fontSizeMultiplier?: string;
508
+ fontWeightHeading?: string;
509
+ lineHeight?: string;
510
+ spacingXs?: string;
511
+ spacingSm?: string;
512
+ spacingMd?: string;
513
+ spacingLg?: string;
369
514
  borderRadius?: string;
370
- buttonBorderRadius?: string;
515
+ borderRadiusBtn?: string;
516
+ shadow?: string;
371
517
  toggleBgOn?: string;
518
+ toggleBgPartial?: string;
372
519
  toggleBgOff?: string;
373
- accentColor?: string;
374
- accentTextColor?: string;
520
+ toggleKnob?: string;
521
+ toggleWidth?: string;
522
+ toggleHeight?: string;
523
+ zBanner?: string;
524
+ zOverlay?: string;
525
+ zModal?: string;
375
526
  }
376
527
  interface CoreConfig {
377
528
  tenantId?: string;
378
529
  locale?: string;
379
- /** Text direction for the banner/modal root. `'auto'` (default) derives it from `locale`
380
- * (Arabic, Hebrew, Persian, Urdu, ... → `rtl`); set explicitly to override. */
530
+ /** Text direction for the banner/modal root. `'auto'` (default) derives it from `locale` via
531
+ * `Intl.Locale(...).getTextInfo().direction` (falling back to a hand-maintained RTL-language
532
+ * list on engines without `getTextInfo()` support); set explicitly to override. */
381
533
  dir?: 'ltr' | 'rtl' | 'auto';
382
534
  disableCssTemplate?: boolean;
383
535
  cookieSigningKey?: string;
384
536
  allowReceipt?: boolean;
385
537
  cookieDomains?: string;
538
+ /** Name of the consent cookie/localStorage key. Default: `'consenti_data'`. Not switched
539
+ * automatically by detected region — set explicitly if you want a different name (e.g.
540
+ * `'euconsent-v2'`, the IAB TCF convention, but only meaningful for operators using the
541
+ * spec-correct binary encoder — `@consenti/api` + the optional `@iabtechlabtcf/core` peer
542
+ * dependency — since the IAB name implies IAB's binary format, not Consenti's own encoding). */
543
+ cookieName?: string;
386
544
  storage?: 'cookie' | 'localStorage';
387
545
  theme?: ThemeConfig;
546
+ /** Initial logged-in application user ID; empty/anonymous when unset. Prefer `getUserId()`/
547
+ * `setUserId()` (or the `consenti:listener:identify` event) to change it after init — those
548
+ * also handle reconsent when the identity changes on a shared device. */
388
549
  userId?: string;
389
550
  usePrebuiltProfiles?: 'all' | NonEmptyArray<ComplianceGroupId>;
390
551
  cacheResolvedProfiles?: boolean;
@@ -404,10 +565,25 @@ interface ConsentiConfig {
404
565
  utils?: UtilsConfig;
405
566
  plugins?: ConsentiPlugin$1[];
406
567
  profileOverride?: DeepPartial<ResolvedProfile>;
568
+ /** Per-compliance-group text/button/cookie overrides — applied on top of whichever profile the
569
+ * visitor's resolved group loads, keyed by that group's id (one of the 8 built-in
570
+ * `ComplianceGroupId`s, or any custom group id). Lets you author different copy per country
571
+ * without creating a separate profile (and separate `compliance.type` wiring) for each one —
572
+ * e.g. `{ 'opt-in': { mainBanner: { htmlText: '...' } }, 'my-custom-group': { ... } }`.
573
+ * Applied before `profileOverride`, which still always applies last as the universal catch-all.
574
+ * Works identically in standalone and server (`api.enabled`) mode. */
575
+ complianceGroupsOverride?: Partial<Record<ComplianceGroupId | (string & {}), DeepPartial<ResolvedProfile>>>;
407
576
  /** When true, suppresses the "Powered by Consenti" footer link in the banner and modal. */
408
577
  hidePoweredBy?: boolean;
409
578
  }
410
- type ConsentiEventName = 'bannerInitialized' | 'consenti:bannerInitialized' | 'bannerVisibility' | 'consenti:bannerVisibility' | 'modalVisibility' | 'consenti:modalVisibility' | 'consentBeingSubmitted' | 'consenti:consentBeingSubmitted' | 'consentSubmitted' | 'consenti:consentSubmitted' | 'parentalConsentRequired' | 'consenti:parentalConsentRequired';
579
+ 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';
580
+ /** Snapshot of the current visitor's identity. `visitorId` is `null` until a consent decision
581
+ * has actually happened — it's never minted just to answer this call, see `getVisitor()`. */
582
+ interface VisitorIdentity {
583
+ visitorId: string | null;
584
+ type: 'authenticated' | 'anonymous';
585
+ userId: string | null;
586
+ }
411
587
  interface ConsentiWidgetAPI {
412
588
  hasConsent(): boolean;
413
589
  getConsent(): ConsentValue | null;
@@ -438,10 +614,25 @@ interface ConsentiWidgetAPI {
438
614
  submitConsent(consent: Partial<ConsentValue>): Promise<ConsentDbRecord | void>;
439
615
  deleteConsent(): Promise<void>;
440
616
  reConsent(): Promise<void>;
617
+ /** Erases the stored consent record (same underlying call as `deleteConsent()`) and
618
+ * re-prompts, dispatching `consenti:forgetMeRequested` / `consenti:forgotten` around it so a
619
+ * host app can hook its own identity-verified erasure workflow. Powers the preference modal's
620
+ * "Forget me" button; also callable directly for a custom placement of the same action. */
621
+ forgetMe(resetAgeGate?: boolean): Promise<void>;
441
622
  getRootElement(): HTMLElement | null;
442
623
  getBannerElement(): HTMLElement | null;
443
624
  getModalElement(): HTMLElement | null;
444
625
  switchLocale(locale: string): void;
626
+ getUserId(): string | null;
627
+ /** Sets the logged-in application user ID (`null` for anonymous/logout). When the stored
628
+ * consent record's user differs from the new value, reconsents by default — pass
629
+ * `reConsent: false` to just update the identity without prompting again. No-ops (no
630
+ * reconsent, no warning) when there's no prior consent record to compare against. */
631
+ setUserId(userId: string | null, reConsent?: boolean): Promise<void>;
632
+ /** Snapshot of the current visitor's identity: the stable per-browser `visitorId` (`null` if
633
+ * no consent decision has happened yet — this never creates one), whether they're
634
+ * `authenticated` or `anonymous`, and the app `userId` (same as `getUserId()`). */
635
+ getVisitor(): VisitorIdentity;
445
636
  setDarkMode(enable?: boolean): void;
446
637
  setTheme(theme: Partial<ThemeConfig>): void;
447
638
  setConfig(config: DeepPartial<ConsentiConfig>): void;
@@ -520,17 +711,52 @@ interface ConsentBeingSubmitted {
520
711
  interface ConsentSubmittedDetail extends ConsentBeingSubmitted {
521
712
  apiResponse: ConsentDbRecord;
522
713
  }
523
- /** Dispatched when the age gate is declined and `requireParentalConsent` is set — see
524
- * `AgeGateWidgetConfig`. A deny-all consent (mandatory cookies only) has already been
714
+ /** Dispatched when the age gate is declined and `requireParentalConsent` is set — see the
715
+ * profile's `AgeGateConfig`. A deny-all consent (mandatory cookies only) has already been
525
716
  * submitted with `parentalConsentToken` attached; this event is the hook for the site
526
- * owner to run their own out-of-band parental-consent verification. */
717
+ * owner to run their own out-of-band parental-consent verification. `parentalConsentToken` is
718
+ * server-issued (and signed, when `compliance.dataSigningHash` is configured) when `api.enabled`,
719
+ * otherwise a client-generated, unsigned fallback — see `POST /consent/:visitorId/parental-consent-request`. */
527
720
  interface ParentalConsentRequiredDetail {
528
721
  parentalConsentToken: string;
529
722
  profileId: string;
530
723
  visitorId: string;
531
724
  timestamp: number;
532
725
  }
533
- type ConsentEvent = BannerInitializedDetail | BannerVisibilityDetail | ModalVisibilityDetail | ConsentBeingSubmitted | ConsentSubmittedDetail | ParentalConsentRequiredDetail;
726
+ /** Dispatched by the standalone `resolveParentalConsent()` util (`apps/ui/src/utils/parental-consent.ts`)
727
+ * after `POST /consent/parental-consent-resolve` succeeds — typically from a fresh page/session
728
+ * with no live `ConsentiSetup` instance (the parent's own device, not the child's). A page that
729
+ * does have a live widget instance can listen for this to react (e.g. re-show the banner/modal for
730
+ * the real consent choice); what "resolved" should actually do to consent state is host-defined. */
731
+ interface ParentalConsentGrantedDetail {
732
+ visitorId: string;
733
+ profileId: string;
734
+ }
735
+ /** Detail payload for the inbound `consenti:listener:identify` event — dispatched by the *host*
736
+ * page (not the widget) to tell the widget which application user is logged in, equivalent to
737
+ * calling `setUserId(userId, reConsent)`. `reConsent` defaults to `true`. */
738
+ interface IdentifyEventDetail {
739
+ userId: string | null;
740
+ reConsent?: boolean;
741
+ }
742
+ /** Dispatched by `forgetMe()` right before the erasure call goes out — the hook for a host app
743
+ * to kick off its own identity-verified erasure workflow across other systems (CRM, DMP,
744
+ * analytics). The CMP itself only ever erases its own consent record; see the server-side
745
+ * `consent.erased` eventBus event (fired by `DELETE /consent/:visitorId`) for the same hook on
746
+ * the backend. See the "Right to erasure" guide for the full picture. */
747
+ interface ForgetMeRequestedDetail {
748
+ visitorId: string;
749
+ profileId: string;
750
+ timestamp: number;
751
+ }
752
+ /** Dispatched by `forgetMe()` after the consent record has been erased and the banner/age-gate
753
+ * has been re-prompted (see `reConsent()`, which `forgetMe()` wraps). */
754
+ interface ForgottenDetail {
755
+ visitorId: string;
756
+ profileId: string;
757
+ timestamp: number;
758
+ }
759
+ type ConsentEvent = BannerInitializedDetail | BannerVisibilityDetail | ModalVisibilityDetail | ConsentBeingSubmitted | ConsentSubmittedDetail | ParentalConsentRequiredDetail | ParentalConsentGrantedDetail | ForgetMeRequestedDetail | ForgottenDetail;
534
760
  type ConsentiMessage = {
535
761
  type: 'consentUpdated';
536
762
  consent: ConsentValue;
@@ -563,6 +789,7 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
563
789
  private initializing;
564
790
  private profile;
565
791
  private resolvedBase;
792
+ private profilePatches;
566
793
  /** cookieId → Category lookup, rebuilt whenever `profile` changes. Avoids rescanning categories on every consent action. */
567
794
  private categoryIndex;
568
795
  /** Watchers created by the declarative `data-consenti-*` DOM scan — destroyed alongside the widget since they have no other owner. */
@@ -595,6 +822,8 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
595
822
  private _bannerMounted;
596
823
  /** Active focus trap keydown listener — removed when modal closes. */
597
824
  private _focusTrapListener;
825
+ /** Inbound `consenti:listener:identify` window listener — installed once per init(), removed on destroy(). */
826
+ private _identifyListener;
598
827
  /** Maps original handler functions to their DOM-wrapped listeners for on/off bookkeeping. */
599
828
  private _eventHandlers;
600
829
  /** `core` defaulted to `{}` in the constructor below — every one of its own fields is
@@ -611,6 +840,29 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
611
840
  */
612
841
  constructor(config: ConsentiConfig);
613
842
  private profileId;
843
+ /** Patches `base` with `complianceGroupsOverride[base.complianceGroup]`, if both exist — the
844
+ * per-group counterpart to `profileOverride`. Lets an author supply different text/buttons/
845
+ * cookies per compliance group (built-in or custom) without authoring a separate profile for
846
+ * each one. Applied before `profileOverride`, which still always wins as the final catch-all. */
847
+ private applyComplianceGroupOverride;
848
+ /**
849
+ * Applied in order: compliance-group override → `config.profileOverride` → each `setProfile()`
850
+ * patch. The `setProfile()` patches are kept as a list and replayed, not folded into one
851
+ * object, because folding runs `deepMerge` patch-onto-patch, which consumes `null`/`'*': null`
852
+ * delete markers before they ever reach the profile they were meant to delete from.
853
+ */
854
+ private applyProfileOverrides;
855
+ private warnIfAcceptOnly;
856
+ /**
857
+ * Whether `window.__tcfapi`/`window.__gpp` should stay uninstalled because the corresponding
858
+ * server-side registration (`compliance.tcf`/`compliance.gpp`) is unconfirmed (cmpId/cmpVersion
859
+ * changed since the dashboard's confirmation checkbox was last checked, or never confirmed).
860
+ * Standalone (no-backend) deployments have no governance layer to check against, so this always
861
+ * resolves `false` there — the client-side config is used as configured, same as before this
862
+ * feature existed. A network failure fails closed (`true`) rather than assuming the
863
+ * unverifiable state is fine.
864
+ */
865
+ private isRegistrationBlocked;
614
866
  /**
615
867
  * Starts widget initialisation. Called automatically unless `autoInit: false` is set.
616
868
  * Can be called explicitly after construction when `autoInit: false`, or after `destroy()`
@@ -624,7 +876,7 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
624
876
  * `resolveAgeGate()`, then resume here with `hasExisting` fixed at `false`.
625
877
  */
626
878
  private continueInit;
627
- /** Mounts the age-gate prompt (`compliance.ageGate`) in place of the normal banner flow. */
879
+ /** Mounts the age-gate prompt (`profile.ageGate`) in place of the normal banner flow. */
628
880
  private showAgeGate;
629
881
  /**
630
882
  * Handles the visitor's age-gate answer.
@@ -647,13 +899,35 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
647
899
  * a resolved profile to read consent from.
648
900
  */
649
901
  private finishInit;
902
+ /** Installs the inbound `consenti:listener:identify` window listener — the event-driven
903
+ * counterpart to `setUserId()`. Removes any previously-installed listener first so repeated
904
+ * `init()` calls (e.g. from `switchLocale()`) don't stack up duplicate listeners. */
905
+ private _installIdentifyListener;
906
+ /** Returns the current logged-in application user ID, or `null` for an anonymous visitor. */
907
+ getUserId(): string | null;
908
+ /**
909
+ * Sets the logged-in application user ID (`null` for anonymous/logout). If a stored consent
910
+ * record exists and its user differs from `userId`, this is treated as a different data
911
+ * subject on a shared device: by default it reconsents (deletes the existing record and
912
+ * re-shows the banner) and logs a warning. Pass `reConsent: false` to just update the identity
913
+ * without prompting again (still logs the warning). No-ops entirely — no warning, no
914
+ * reconsent — when there is no prior consent record to compare against (true first-time visitor).
915
+ */
916
+ setUserId(userId: string | null, reConsent?: boolean): Promise<void>;
917
+ /**
918
+ * Snapshot of the current visitor's identity. `visitorId` is the stable per-browser ID —
919
+ * `null` until a consent decision has actually happened, since this never mints one just to
920
+ * answer the call (see `getOrCreateVisitorId()`). `type` is `'authenticated'` whenever a
921
+ * `userId` is set (via `setUserId()` or `core.userId`), `'anonymous'` otherwise.
922
+ */
923
+ getVisitor(): VisitorIdentity;
650
924
  /** Sets the `dir` attribute on the root element from `core.dir` (default `'auto'`, derived from `core.locale`). */
651
925
  private applyDirection;
652
926
  /** Applies the `consenti-root--dark` class based on config or profile flag. */
653
927
  private applyDarkMode;
654
928
  /** Applies `consenti--enhanced-a11y` class when `profile.enhanceAccessibility` is set. */
655
929
  private applyAccessibility;
656
- /** Builds and appends the metadata footer strip to a banner or modal container. */
930
+ /** Builds and appends the metadata footer strip to modal container. */
657
931
  private _appendMetaFooter;
658
932
  /**
659
933
  * Injects a `<style>` element into the root for `stackButtonsOnBreakpoint`.
@@ -697,8 +971,16 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
697
971
  * Pass `type` to get a vendor-ready object instead of the raw Consenti format.
698
972
  * Always returns `null` during SSR or when no consent has been saved.
699
973
  *
700
- * @param type - Vendor format: 'google-gtm', 'category', 'adobe', 'meta',
701
- * 'microsoft-clarity', 'twilio-segment', or omit for raw format.
974
+ * @param type - Output format:
975
+ * - omit / `'default'` → raw ConsentValue, keyed by every cookie parameter ID in the profile
976
+ * - `'purpose'` → consent per the fixed CookiePurpose taxonomy (necessary/functional/preferences/analytics/marketing)
977
+ * - `'category'` → consent per the tenant's own authored preference-modal category, keyed by category ID
978
+ * ('granted' only when every member parameter is granted; 'denied'/'objected' otherwise)
979
+ * - `'google-gtm'` → Google Consent Mode v2 keys + ads_data_redaction, url_passthrough
980
+ * - `'adobe'` → { analytics, target, manager, optimizer }
981
+ * - `'meta'` → { pixel, api, plugins, facebookLogin }
982
+ * - `'microsoft-clarity'` → { session, heatmaps, performance }
983
+ * - `'twilio-segment'` → { identify, page, track, group, alias }
702
984
  */
703
985
  getConsent(): ConsentValue | null;
704
986
  getConsent(type: ConsentType): Record<string, string> | null;
@@ -790,8 +1072,30 @@ declare class ConsentiSetup implements ConsentiWidgetAPI {
790
1072
  * method so callers do not need to coordinate the async deletion with the banner display.
791
1073
  *
792
1074
  * Use this to power "Review my consent" links in footers or privacy dashboards.
1075
+ *
1076
+ * @param resetAgeGate - When `true` (default) and the profile has `ageGate.enabled`, the
1077
+ * age-gate prompt is shown again instead of jumping straight to the
1078
+ * banner — mirroring the original first-visit flow. Pass `false` to
1079
+ * skip it and go straight to the banner, keeping the visitor's prior
1080
+ * age-gate answer.
1081
+ */
1082
+ reConsent(resetAgeGate?: boolean): Promise<void>;
1083
+ /**
1084
+ * Self-service erasure entry point — GDPR Art. 17 / CCPA-CPRA / LGPD Art. 18 and equivalent
1085
+ * "right to be forgotten" rights, exercised from the widget itself. Wraps `reConsent()` (same
1086
+ * erase-then-reprompt behavior) with `consenti:forgetMeRequested` / `consenti:forgotten`
1087
+ * dispatched around it, so a host app can hook its own identity-verified erasure workflow
1088
+ * across other systems (CRM, DMP, analytics) — the CMP itself only ever erases its own consent
1089
+ * record. See the "Right to erasure" guide for the full picture, including the server-side
1090
+ * `consent:erased` eventBus event fired by `DELETE /consent/:visitorId`.
1091
+ *
1092
+ * Powers the preference modal's "Forget me" button (`preferenceModal.showForgetMe`, only
1093
+ * rendered when the visitor already has a stored consent decision). Also callable directly —
1094
+ * e.g. `widget.forgetMe()` from a custom footer link or privacy-settings page.
1095
+ *
1096
+ * @param resetAgeGate - Forwarded to `reConsent()` — see its docs.
793
1097
  */
794
- reConsent(): Promise<void>;
1098
+ forgetMe(resetAgeGate?: boolean): Promise<void>;
795
1099
  /**
796
1100
  * Returns the fully resolved profile for the current session, or `null` during the
797
1101
  * async init phase (before the profile has been fetched/resolved).
@@ -1470,4 +1774,44 @@ declare class BannerTrigger {
1470
1774
  */
1471
1775
  declare function scanConsentScripts(widget: ConsentiSetup): Array<ConsentScript | CategoryScript>;
1472
1776
 
1473
- export { type AgeGateWidgetConfig, type ApiConfig, BannerTrigger, type BannerTriggerOptions, type Button, type ButtonAction, type ButtonStyle, type Category, CategoryAction, type CategoryActionOptions, type CategoryActionParams, type CategoryMap, CategoryScript, type CategoryScriptOptions, type ComplianceWidgetConfig, ConsentAction, type ConsentActionOptions, type ConsentActionParams, type ConsentAction$1 as ConsentActionType, type ConsentEvent, type ConsentReceipt, type ConsentRecord, ConsentScript, type ConsentScriptOptions, type ConsentStatus, type ConsentValue, type ConsentiConfig, type ConsentiEventName, type ConsentiMessage, ConsentiPlugin, ConsentiProfile, ConsentiSetup, type ConsentiWidgetAPI, type Cookie, type CookieMap, type CookieOptions, type CoreConfig, type GpcBanner, type GtmConfig, type LocaleTranslations, type MainBanner, type ParentalConsentRequiredDetail, type ParsedConsent, type PreferenceModal, type ProfileConfig, type ProfileTranslations, type RegisterableProfileConfig, type ResolvedProfile, type TcfWidgetConfig, type ThemeConfig, type UtilsConfig, scanConsentScripts };
1777
+ /**
1778
+ * Standalone resolver for the parental-consent hook (see `AgeGate`/`ConsentiSetup.resolveAgeGate`).
1779
+ * Not a `ConsentiSetup` instance method — the parent typically opens the token link (emailed by
1780
+ * the host's own eventBus listener, see `consenti:parentalConsentRequired`) on a fresh page or
1781
+ * even a different device, with no live widget instance around at all.
1782
+ */
1783
+
1784
+ interface ParentalConsentApiConfig {
1785
+ baseUrl?: string;
1786
+ authToken?: string;
1787
+ }
1788
+ /**
1789
+ * POSTs a parental-consent token to `POST /consent/parental-consent-resolve`. On success,
1790
+ * dispatches `consenti:parentalConsentResolved` on `window` (same mechanism `EventBus.dispatch`
1791
+ * uses) so any page with a live widget instance can react — what "resolved" should actually do to
1792
+ * consent state is left to that listener, matching the "we provide the hook, not the behavior"
1793
+ * scope of this whole feature. Throws on an invalid/expired token or network failure.
1794
+ */
1795
+ declare function resolveParentalConsent(token: string, apiConfig?: ParentalConsentApiConfig): Promise<ParentalConsentGrantedDetail>;
1796
+
1797
+ /**
1798
+ * Builds a synchronous, dependency-free `<head>` snippet that freezes Google Consent Mode
1799
+ * to denied-by-default the instant a visitor's browser sends the Global Privacy Control (GPC)
1800
+ * signal — before any tag-loading script runs, including GTM/gtag.js itself and Consenti's own
1801
+ * bundle (which only applies GPC once it has loaded, parsed, and initialized asynchronously).
1802
+ *
1803
+ * This is a documentation helper, not something the widget loads itself: paste the returned
1804
+ * string as the very first `<script>` in `<head>`, ahead of GTM/gtag.js and the Consenti bundle.
1805
+ * It only touches `window[dataLayerName]`/`window.gtag` (the same stub-queue pattern
1806
+ * `EventBus.initConsentMode()` uses), so it's safe to run before those exist and harmless to
1807
+ * run again after they do — Consenti's own `gtag('consent', 'default', …)` call on init just
1808
+ * pushes another entry onto the same queue.
1809
+ *
1810
+ * The default values pushed here mirror `getGoogleGTMConsent({})` in `consent-mapper.ts` (safe
1811
+ * defaults for an empty consent map) — keep the two in sync if either changes.
1812
+ */
1813
+ declare function buildSyncGpcSnippet(options?: {
1814
+ dataLayerName?: string;
1815
+ }): string;
1816
+
1817
+ export { type ApiConfig, BannerTrigger, type BannerTriggerOptions, type Button, type ButtonAction, type ButtonStyle, type Category, CategoryAction, type CategoryActionOptions, type CategoryActionParams, type CategoryMap, CategoryScript, type CategoryScriptOptions, type ComplianceWidgetConfig, ConsentAction, type ConsentActionOptions, type ConsentActionParams, type ConsentAction$1 as ConsentActionType, type ConsentEvent, type ConsentReceipt, type ConsentRecord, ConsentScript, type ConsentScriptOptions, type ConsentStatus, type ConsentValue, type ConsentiConfig, type ConsentiEventName, type ConsentiMessage, ConsentiPlugin, ConsentiProfile, ConsentiSetup, type ConsentiWidgetAPI, type Cookie, type CookieMap, type CookieOptions, type CoreConfig, type GpcBanner, type GtmConfig, type IdentifyEventDetail, type LocaleTranslations, type MainBanner, type ParentalConsentApiConfig, type ParentalConsentGrantedDetail, type ParentalConsentRequiredDetail, type ParsedConsent, type PreferenceModal, type ProfileConfig, type ProfileTranslations, type RegisterableProfileConfig, type ResolvedProfile, type TcfWidgetConfig, type ThemeConfig, type UtilsConfig, type VisitorIdentity, buildSyncGpcSnippet, resolveParentalConsent, scanConsentScripts };