@cookieyes/core 0.1.1 → 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.
package/dist/index.d.ts CHANGED
@@ -19,8 +19,82 @@ type ConsentChecker = (category: ConsentCategory) => boolean;
19
19
  declare function installNetworkBlocker(config: NetworkBlockerConfig, hasConsent: ConsentChecker): () => void;
20
20
  declare function uninstallNetworkBlocker(): void;
21
21
 
22
- type ConsentCategory = "necessary" | "functional" | "analytics" | "performance" | "advertisement";
22
+ /**
23
+ * A tool that can be stopped (and optionally resumed) at runtime when consent
24
+ * for its category changes — no page reload needed. `stop()` is called when the
25
+ * category is revoked; `resume()` (if provided) when it's re-granted.
26
+ *
27
+ * If `stop()` throws, that tool is treated as "couldn't be stopped cleanly" and
28
+ * falls back to the reload notice for that one tool — it never breaks the page.
29
+ */
30
+ type StopHandler = {
31
+ id: string;
32
+ category: ConsentCategory;
33
+ stop: () => void;
34
+ resume?: (() => void) | undefined;
35
+ };
36
+ /**
37
+ * A tool with no known clean runtime stop — revoking its category can only be
38
+ * fully applied by reloading the page. Registering one means "if this category
39
+ * is revoked, show the visitor the reload notice."
40
+ */
41
+ type ReloadOnlyHandler = {
42
+ id: string;
43
+ category: ConsentCategory;
44
+ needsReload: true;
45
+ };
46
+ type AnyStopHandler = StopHandler | ReloadOnlyHandler;
47
+ /**
48
+ * Built-in, first-party integrations. Each maps to either a clean stop-handler
49
+ * or a reload-only marker (see the audit in the README).
50
+ *
51
+ * Note: Google Analytics 4 and Google Tag Manager are **not** listed here.
52
+ * They're governed by Google Consent Mode v2, which the SDK broadcasts
53
+ * automatically whenever a `dataLayer` is present (see google-consent-mode.ts)
54
+ * — on load and on every consent change, derived from each category's `gcm`
55
+ * mapping. So you don't register them as integrations; just add the standard
56
+ * Consent Mode default snippet and the SDK owns the updates.
57
+ *
58
+ * VERIFIED clean-stop vendors (documented, stable runtime opt-out):
59
+ * - `meta` — `fbq('consent','revoke'|'grant')`, Meta's official consent API.
60
+ *
61
+ * The rest have no confident, documented runtime stop, so they're modelled as
62
+ * reload-only (Story 1's honest answer). Upgrading any of them to a clean-stop
63
+ * later is a one-line change here once a real API is confirmed.
64
+ */
65
+ type BuiltInIntegration = {
66
+ vendor: "meta";
67
+ category?: ConsentCategory | undefined;
68
+ } | {
69
+ vendor: "tiktok";
70
+ category?: ConsentCategory | undefined;
71
+ } | {
72
+ vendor: "linkedin";
73
+ category?: ConsentCategory | undefined;
74
+ } | {
75
+ vendor: "hotjar";
76
+ category?: ConsentCategory | undefined;
77
+ } | {
78
+ vendor: "segment";
79
+ category?: ConsentCategory | undefined;
80
+ };
81
+ declare function resolveBuiltInIntegration(cfg: BuiltInIntegration): AnyStopHandler;
82
+ declare function registerStopHandler(handler: AnyStopHandler): void;
83
+ /** Test-only: reset registry + transition state between cases. */
84
+ declare function _clearStopHandlers(): void;
85
+
86
+ /**
87
+ * A consent category id. The five built-in ids are offered for autocomplete,
88
+ * but any string is valid — customers can define their own taxonomy via
89
+ * `categories` (see {@link CategoryDef}).
90
+ */
91
+ type ConsentCategory = "necessary" | "functional" | "analytics" | "performance" | "advertisement" | (string & {});
23
92
  type Regulation = "GDPR" | "CCPA" | "DEFAULT";
93
+ /** Display text for one consent category. */
94
+ type CategoryText = {
95
+ label: string;
96
+ description: string;
97
+ };
24
98
  type TranslationMap = {
25
99
  bannerTitle: string;
26
100
  bannerDescription: string;
@@ -35,27 +109,12 @@ type TranslationMap = {
35
109
  preferencesTitle: string;
36
110
  preferencesIntro: string;
37
111
  categories: {
38
- necessary: {
39
- label: string;
40
- description: string;
41
- };
42
- functional: {
43
- label: string;
44
- description: string;
45
- };
46
- analytics: {
47
- label: string;
48
- description: string;
49
- };
50
- performance: {
51
- label: string;
52
- description: string;
53
- };
54
- advertisement: {
55
- label: string;
56
- description: string;
57
- };
58
- };
112
+ necessary: CategoryText;
113
+ functional: CategoryText;
114
+ analytics: CategoryText;
115
+ performance: CategoryText;
116
+ advertisement: CategoryText;
117
+ } & Record<string, CategoryText>;
59
118
  optOut: {
60
119
  title: string;
61
120
  description: string;
@@ -63,6 +122,24 @@ type TranslationMap = {
63
122
  successText: string;
64
123
  successCountdown: string;
65
124
  };
125
+ reloadNotice: {
126
+ message: string;
127
+ reloadButton: string;
128
+ dismissButton: string;
129
+ };
130
+ };
131
+ /** A subset of TranslationMap — lets a customer override just a few strings. */
132
+ type DeepPartial<T> = T extends object ? {
133
+ [K in keyof T]?: DeepPartial<T[K]>;
134
+ } : T;
135
+ type PartialTranslations = DeepPartial<TranslationMap>;
136
+ /** Reading direction of a language. */
137
+ type TextDirection = "ltr" | "rtl";
138
+ /** The active language, its reading direction, and the languages currently loaded. */
139
+ type LanguageInfo = {
140
+ language: string;
141
+ direction: TextDirection;
142
+ languages: string[];
66
143
  };
67
144
  type ThemeConfig = {
68
145
  primaryColor?: string | undefined;
@@ -83,29 +160,79 @@ type ScriptEntry = {
83
160
  onLoad?: (() => void) | undefined;
84
161
  };
85
162
  type I18nConfig = {
86
- messages?: Record<string, TranslationMap> | undefined;
163
+ /** Translations per language. Each may be partial — missing text falls back to English. */
164
+ messages?: Record<string, PartialTranslations> | undefined;
87
165
  locale?: string | undefined;
88
166
  detectBrowserLanguage?: boolean | undefined;
167
+ /**
168
+ * Called when a language is switched to that isn't already in `messages` —
169
+ * return its translations (fetch them from your own URL, import them, etc.).
170
+ * Lets you load languages on demand instead of bundling them all upfront.
171
+ */
172
+ loadLanguage?: ((tag: string) => PartialTranslations | Promise<PartialTranslations>) | undefined;
89
173
  };
90
174
  type ConsentConfig = {
91
175
  apiUrl?: string | undefined;
92
176
  apiKey?: string | undefined;
93
177
  backend?: ConsentBackend | undefined;
94
178
  regulation?: Regulation | undefined;
179
+ /**
180
+ * Define your own category taxonomy. Omit to get the built-in five
181
+ * (necessary, functional, analytics, performance, advertisement) unchanged.
182
+ * At least one category must be `{ required: true }`. Invalid configs fall
183
+ * back to the built-in five with a console warning. See {@link CategoryDef}.
184
+ */
185
+ categories?: CategoryDef[] | undefined;
95
186
  theme?: ThemeConfig | undefined;
96
- colorScheme?: "light" | "dark" | "system" | undefined;
187
+ colorScheme?: ColorScheme | undefined;
97
188
  reloadOnRevoke?: boolean | undefined;
189
+ /**
190
+ * Built-in, first-party integrations to stop cleanly (no reload) when their
191
+ * category is revoked — e.g. `{ vendor: "meta" }`. (Google Analytics/Tag
192
+ * Manager are handled automatically via the Consent Mode broadcast — no entry
193
+ * needed.) Integrations with no clean runtime stop fall back to the reload notice.
194
+ */
195
+ integrations?: BuiltInIntegration[] | undefined;
196
+ /**
197
+ * Your own scripts' stop instructions, for anything without a built-in
198
+ * integration. A handler that can stop cleanly provides `stop()`; one that
199
+ * can't should be registered as a reload-only handler instead so revoking it
200
+ * shows the reload notice rather than silently continuing to track.
201
+ */
202
+ customStopHandlers?: StopHandler[] | undefined;
98
203
  onConsentReady?: ((state: ConsentSnapshot) => void) | undefined;
99
204
  onConsentUpdate?: ((state: ConsentSnapshot) => void) | undefined;
100
205
  };
206
+ /**
207
+ * Surfaced when a revoked tool has no clean runtime stop and can only be fully
208
+ * applied by reloading. `required` is false once dismissed; `reasons` lists the
209
+ * handler ids that triggered it (e.g. `["hotjar"]`).
210
+ */
211
+ type ReloadNoticeState = {
212
+ required: boolean;
213
+ reasons: string[];
214
+ };
101
215
  type ConsentSnapshot = {
102
216
  consentId: string;
103
217
  hasActed: boolean;
104
- categories: Record<ConsentCategory, boolean>;
218
+ /** Category id → granted. Keys are the configured taxonomy's ids. */
219
+ categories: Record<string, boolean>;
105
220
  regulation: Regulation;
106
221
  lastRenewed?: number | undefined;
222
+ /**
223
+ * Signature of the category taxonomy in effect when this consent was
224
+ * recorded. Lets us (and the customer) tell what a returning visitor
225
+ * actually agreed to, and drives re-request when the taxonomy changes.
226
+ */
227
+ taxonomyHash?: string | undefined;
107
228
  };
108
229
  type ConsentManager = ConsentSnapshot & {
230
+ /**
231
+ * Consent in effect — changes only on a real decision (accept / reject / save
232
+ * / reset), never on a dialog toggle. Gate scripts/embeds on this. (`categories`
233
+ * is the live value that drives the dialog checkboxes.)
234
+ */
235
+ committedCategories: Record<string, boolean>;
109
236
  acceptAll: () => void;
110
237
  rejectAll: () => void;
111
238
  acceptSelected: (categories: ConsentCategory[]) => void;
@@ -117,9 +244,13 @@ type ConsentManager = ConsentSnapshot & {
117
244
  isPreferencesOpen: boolean;
118
245
  subscribe: (listener: (state: ConsentSnapshot) => void) => () => void;
119
246
  registerScript: (entry: ScriptEntry) => void;
247
+ /** Current reload-notice state (see {@link ReloadNoticeState}). */
248
+ reloadNotice: ReloadNoticeState;
249
+ /** Dismiss the reload notice; it won't reappear until a new revoke needs one. */
250
+ dismissReloadNotice: () => void;
120
251
  };
121
252
  /**
122
- * Shape of the JSON body POSTed to the customer's `backendURL`
253
+ * Shape of the JSON body POSTed to the customer's `apiUrl`
123
254
  * on every consent decision (Accept All / Reject All / Save Preferences).
124
255
  *
125
256
  * Customers building a TypeScript backend can import this type to get
@@ -127,7 +258,7 @@ type ConsentManager = ConsentSnapshot & {
127
258
  */
128
259
  type ConsentPayload = {
129
260
  consentId: string;
130
- categories: Record<ConsentCategory, boolean>;
261
+ categories: Record<string, boolean>;
131
262
  regulation: Regulation;
132
263
  domain: string;
133
264
  };
@@ -135,7 +266,7 @@ type ConsentPayload = {
135
266
  * Customer-implemented adapter that decides how a consent decision
136
267
  * reaches their backend. Provide this when `mode: "self-hosted"` and you
137
268
  * need full control over the request shape, headers, auth, transport,
138
- * batching, retries, etc. — anything you can't express with `backendURL`.
269
+ * batching, retries, etc. — anything you can't express with `apiUrl`.
139
270
  *
140
271
  * The SDK hands you a standardised `ConsentPayload`; you transform and
141
272
  * dispatch it however your backend expects.
@@ -143,69 +274,396 @@ type ConsentPayload = {
143
274
  interface ConsentBackend {
144
275
  persist(payload: ConsentPayload): Promise<void> | void;
145
276
  }
146
- type ConsentRuntimeMode = "self-hosted" | "offline";
147
- type ConsentRuntimeOptions = {
148
- mode: ConsentRuntimeMode;
149
- backendURL?: string | undefined;
150
- apiKey?: string | undefined;
151
- backend?: ConsentBackend | undefined;
152
- consentCategories?: ConsentCategory[] | undefined;
153
- overrides?: {
154
- regulation?: Regulation | undefined;
155
- } | undefined;
156
- i18n?: I18nConfig | undefined;
157
- colorScheme?: "light" | "dark" | "system" | undefined;
277
+ /**
278
+ * @deprecated Use `"cookie-only"` instead — identical behavior, clearer name.
279
+ * `"offline"` still works but will be removed in a future release.
280
+ */
281
+ type DeprecatedOfflineMode = "offline";
282
+ type ConsentRuntimeMode = "self-hosted" | "cookie-only" | DeprecatedOfflineMode;
283
+ type ColorScheme = "light" | "dark" | "system";
284
+ /**
285
+ * Fields shared by every {@link CookieYesConfig} regardless of `mode`.
286
+ * This is the one canonical config surface — both `@cookieyes/core` and
287
+ * `@cookieyes/react` consume the exact same object, so a config is
288
+ * copy-pasteable between them with zero edits.
289
+ */
290
+ type CookieYesConfigCommon = {
291
+ /**
292
+ * Which privacy regulation applies. Top-level and identical across every
293
+ * package (replaces the builder's `.regulation()` and core's former
294
+ * nested `overrides.regulation`).
295
+ */
296
+ regulation?: Regulation | undefined;
297
+ colorScheme?: ColorScheme | undefined;
158
298
  theme?: ThemeConfig | undefined;
299
+ i18n?: I18nConfig | undefined;
300
+ consentCategories?: ConsentCategory[] | undefined;
301
+ /**
302
+ * Define your own category taxonomy. Omit to get the built-in five
303
+ * (necessary, functional, analytics, performance, advertisement) unchanged.
304
+ * At least one category must be `{ required: true }`. Invalid configs fall
305
+ * back to the built-in five with a console warning. See {@link CategoryDef}.
306
+ */
307
+ categories?: CategoryDef[] | undefined;
159
308
  networkBlocker?: NetworkBlockerConfig | undefined;
160
309
  reloadOnRevoke?: boolean | undefined;
310
+ /**
311
+ * Built-in, first-party integrations to stop cleanly (no reload) when their
312
+ * category is revoked — e.g. `{ vendor: "meta" }`. (Google Analytics/Tag
313
+ * Manager are handled automatically via the Consent Mode broadcast — no entry
314
+ * needed.) Integrations with no clean runtime stop fall back to the reload notice.
315
+ */
316
+ integrations?: BuiltInIntegration[] | undefined;
317
+ /**
318
+ * Your own scripts' stop instructions, for anything without a built-in
319
+ * integration. A handler that can stop cleanly provides `stop()`; one that
320
+ * can't should be registered as a reload-only handler instead so revoking it
321
+ * shows the reload notice rather than silently continuing to track.
322
+ */
323
+ customStopHandlers?: StopHandler[] | undefined;
324
+ /** Low-level: fires once, after the runtime's initial state is known (e.g. to conditionally load analytics on first load). For ongoing updates, use `consentStore.subscribeToConsentChanges` instead. */
161
325
  onConsentReady?: ((state: ConsentSnapshot) => void) | undefined;
326
+ /** Low-level: fires on every saved consent change, for the lifetime of this config. If you need to subscribe/unsubscribe dynamically after mount, use `consentStore.getState().subscribeToConsentChanges` instead. */
162
327
  onConsentUpdate?: ((state: ConsentSnapshot) => void) | undefined;
328
+ /**
329
+ * @deprecated Set `regulation` at the top level instead. This nested form
330
+ * still works and maps to the top-level field; if both are given, the
331
+ * top-level `regulation` wins. Retained for back-compat and removed after
332
+ * three release cycles, per the SDK deprecation policy.
333
+ */
334
+ overrides?: {
335
+ regulation?: Regulation | undefined;
336
+ } | undefined;
337
+ };
338
+ /**
339
+ * Cookie-only mode — consent is stored client-side only; no backend keys are
340
+ * permitted (they fail at the type level). `mode: "cookie-only"` is the
341
+ * canonical value; `mode: "offline"` is a deprecated alias with identical
342
+ * behavior that emits a one-time-per-page-load deprecation warning.
343
+ */
344
+ type CookieYesOfflineConfig = CookieYesConfigCommon & {
345
+ mode: "cookie-only" | DeprecatedOfflineMode;
163
346
  };
347
+ /** Self-hosted mode — consent decisions are persisted to your own backend. */
348
+ type CookieYesSelfHostedConfig = CookieYesConfigCommon & {
349
+ mode: "self-hosted";
350
+ /** Endpoint the {@link ConsentPayload} is POSTed to. Canonical key. */
351
+ apiUrl?: string | undefined;
352
+ apiKey?: string | undefined;
353
+ /** Custom persistence adapter — full control over transport/headers/retries. */
354
+ backend?: ConsentBackend | undefined;
355
+ /**
356
+ * @deprecated Renamed to `apiUrl`. This alias still works and maps to
357
+ * `apiUrl`; if both are given, `apiUrl` wins. Retained for back-compat and
358
+ * removed after three release cycles, per the SDK deprecation policy.
359
+ */
360
+ backendURL?: string | undefined;
361
+ };
362
+ /**
363
+ * The canonical configuration object for the CookieYes SDK, discriminated on
364
+ * `mode`. Passed identically to `initCookieYes()` /
365
+ * `getOrCreateConsentRuntime()` in `@cookieyes/core` and `initCookieYes()` in
366
+ * `@cookieyes/react`.
367
+ *
368
+ * The discriminated union guarantees invalid combinations fail at compile time
369
+ * — e.g. supplying `apiUrl`/`backend` under `mode: "cookie-only"` is a type error.
370
+ */
371
+ type CookieYesConfig = CookieYesOfflineConfig | CookieYesSelfHostedConfig;
372
+ /**
373
+ * @deprecated Renamed to {@link CookieYesConfig}. Retained as a type alias for
374
+ * back-compat and removed after three release cycles, per the SDK deprecation
375
+ * policy.
376
+ */
377
+ type ConsentRuntimeOptions = CookieYesConfig;
164
378
  type ConsentChangePayload = {
165
379
  allowedCategories: ConsentCategory[];
166
380
  deniedCategories: ConsentCategory[];
167
381
  };
382
+ /** Which consent event to listen for. See {@link ConsentStore.on}. */
383
+ type ConsentEventType = "save" | "change";
384
+ type ConsentEventPayload = {
385
+ /** The full committed consent map in effect when the event fired. */
386
+ categories: Record<string, boolean>;
387
+ /** Categories whose value differed from before. Empty on the initial replay. */
388
+ changedCategories: ConsentCategory[];
389
+ /**
390
+ * `true` when this is the one-off replay a listener gets on attach (here's
391
+ * the current state), `false` when the visitor actually just acted.
392
+ */
393
+ isInitial: boolean;
394
+ };
395
+ type ConsentEventListener = (payload: ConsentEventPayload) => void;
396
+ /** Restrict a listener to a single category (fires only when it changes). */
397
+ type ConsentEventOptions = {
398
+ category?: ConsentCategory;
399
+ };
168
400
  type ActiveUI = "banner" | "dialog" | null;
169
401
  type ConsentStoreState = ConsentSnapshot & {
170
402
  activeUI: ActiveUI;
171
- consents: Record<ConsentCategory, boolean>;
403
+ /** Live/working values — reflect in-progress dialog toggles. Drive checkboxes. */
404
+ consents: Record<string, boolean>;
405
+ /**
406
+ * Consent in effect — changes only on a saved decision, not a toggle. Gate
407
+ * scripts/embeds on this (or {@link ConsentStoreState.has}).
408
+ */
409
+ committedConsents: Record<string, boolean>;
410
+ /** True when `category` is committed-granted (a saved decision), not just toggled. */
172
411
  has: (category: ConsentCategory) => boolean;
173
412
  saveConsents: (target: "all" | "necessary" | ConsentCategory[]) => Promise<void>;
174
413
  setConsent: (category: ConsentCategory, value: boolean) => void;
414
+ /** Low-level: fires only on *saved* preference changes, not transient UI toggles — see `ConsentStore.subscribe` for the recommended, general-purpose subscription. */
175
415
  subscribeToConsentChanges: (listener: (payload: ConsentChangePayload) => void) => () => void;
176
416
  };
417
+ /**
418
+ * The recommended way to read consent state outside React. `subscribe` fires
419
+ * on every state change (including transient UI toggles, e.g. a checkbox
420
+ * flip before saving); for saved-changes-only, see
421
+ * `ConsentStoreState.subscribeToConsentChanges`.
422
+ */
177
423
  type ConsentStore = {
178
424
  subscribe: (listener: (state: ConsentStoreState) => void) => () => void;
179
425
  getState: () => ConsentStoreState;
426
+ /** Text for the active language (English fills gaps). Swaps on `setLanguage`. */
427
+ translations: TranslationMap;
428
+ /** The active language, its reading direction, and the languages loaded. */
429
+ getLanguageInfo: () => LanguageInfo;
430
+ /**
431
+ * Switch language live (no reload) — `subscribe` listeners fire so a custom UI
432
+ * can re-render. Loads the language via `i18n.loadLanguage` if not bundled.
433
+ */
434
+ setLanguage: (tag: string) => Promise<void>;
435
+ /** Customer-provided text for a category in the active language, if any. */
436
+ getCategoryText: (id: string) => Partial<CategoryText> | undefined;
437
+ /**
438
+ * The category taxonomy in effect (custom list or the built-in five) — its
439
+ * ids, which are `required`, etc. Use it to render categories in a custom UI
440
+ * so it follows whatever taxonomy is configured.
441
+ */
442
+ categories: ResolvedCategories;
443
+ /**
444
+ * React to consent decisions. `"save"` fires on every save (even an
445
+ * unchanged re-confirm); `"change"` fires only when a category actually
446
+ * differs — use it to (re)load a script without re-running on a re-confirm.
447
+ * The listener fires once immediately with the current state
448
+ * (`isInitial: true`). Pass `{ category }` to only hear about one category.
449
+ * Returns an unsubscribe function.
450
+ */
451
+ on: (type: ConsentEventType, listener: ConsentEventListener, options?: ConsentEventOptions) => () => void;
180
452
  };
181
453
  type ConsentRuntime = {
182
454
  consentManager: ConsentManager;
183
455
  consentStore: ConsentStore;
184
456
  };
185
457
 
458
+ /**
459
+ * Google Consent Mode v2 storage/signal types. A category can declare which of
460
+ * these it represents via {@link CategoryDef.gcm}; the SDK then broadcasts them
461
+ * (see google-consent-mode.ts). `security_storage` is always granted and is
462
+ * handled by the broadcast itself, so it never needs to be mapped.
463
+ */
464
+ type GoogleConsentSignal = "ad_storage" | "ad_user_data" | "ad_personalization" | "analytics_storage" | "functionality_storage" | "personalization_storage" | "security_storage";
465
+ /**
466
+ * A single consent category. `id` is the stable key stored in the cookie and
467
+ * used everywhere (banner, preferences, read APIs, integrations). Exactly one
468
+ * category should be marked `required` — the always-on, non-optional one (like
469
+ * the default "necessary") — flagged explicitly here, never inferred from a
470
+ * name, so it survives full renaming.
471
+ */
472
+ type CategoryDef = {
473
+ id: ConsentCategory;
474
+ /** The always-on, non-optional category. At least one is required. */
475
+ required?: boolean | undefined;
476
+ /** Display label. Falls back to the translation for built-in ids. */
477
+ label?: string | undefined;
478
+ /** Display description. Falls back to the translation for built-in ids. */
479
+ description?: string | undefined;
480
+ /** Google Consent Mode signals this category governs (see {@link GoogleConsentSignal}). */
481
+ gcm?: GoogleConsentSignal[] | undefined;
482
+ };
483
+ /**
484
+ * The built-in five, used verbatim when a customer configures nothing. GCM
485
+ * mapping mirrors production's `_ckySetGoogleConsentMode` (analytics →
486
+ * analytics_storage, advertisement → the ad_* signals, functional →
487
+ * functionality/personalization; performance maps to nothing; security_storage
488
+ * is always granted by the broadcast).
489
+ */
490
+ declare const DEFAULT_CATEGORIES: CategoryDef[];
491
+ type ResolvedCategories = {
492
+ /** Ordered category definitions actually in effect. */
493
+ list: CategoryDef[];
494
+ /** Ordered ids (fast access). */
495
+ ids: ConsentCategory[];
496
+ /** Ids marked `required` (always granted, never toggleable). */
497
+ requiredIds: Set<ConsentCategory>;
498
+ /** Stable signature of this taxonomy; a change here re-requests consent. */
499
+ taxonomyHash: string;
500
+ /** True when the built-in five are in effect (configured or fallback). */
501
+ isDefault: boolean;
502
+ };
503
+ /**
504
+ * Resolve the category list from config. Returns the built-in five when nothing
505
+ * is configured. On an invalid custom config (empty, duplicate/reserved ids, or
506
+ * no `required` category) it warns and falls back to the built-in five, rather
507
+ * than leaving the visitor a broken/empty or unprotected setup.
508
+ */
509
+ declare function resolveCategories(defs?: CategoryDef[]): ResolvedCategories;
510
+
511
+ /**
512
+ * The canonical config with every deprecated alias already collapsed into its
513
+ * top-level key. Both `@cookieyes/core` and `@cookieyes/react` consume the
514
+ * output of {@link _normalizeConfig}, so alias resolution lives in exactly one
515
+ * place and the two packages can never drift.
516
+ *
517
+ * @internal
518
+ */
519
+ type _NormalizedConfig = {
520
+ mode: ConsentRuntimeMode;
521
+ regulation?: Regulation | undefined;
522
+ colorScheme?: ColorScheme | undefined;
523
+ theme?: ThemeConfig | undefined;
524
+ i18n?: I18nConfig | undefined;
525
+ consentCategories?: ConsentCategory[] | undefined;
526
+ categories?: CategoryDef[] | undefined;
527
+ networkBlocker?: NetworkBlockerConfig | undefined;
528
+ reloadOnRevoke?: boolean | undefined;
529
+ integrations?: BuiltInIntegration[] | undefined;
530
+ customStopHandlers?: StopHandler[] | undefined;
531
+ onConsentReady?: ((state: ConsentSnapshot) => void) | undefined;
532
+ onConsentUpdate?: ((state: ConsentSnapshot) => void) | undefined;
533
+ apiUrl?: string | undefined;
534
+ apiKey?: string | undefined;
535
+ backend?: ConsentBackend | undefined;
536
+ };
537
+ /**
538
+ * Resolve a public {@link CookieYesConfig} into its canonical internal form.
539
+ *
540
+ * Deprecated aliases map silently to their canonical key when used alone; when
541
+ * an alias and its canonical key are both present, the canonical key wins and
542
+ * exactly one warning is logged for that collision.
543
+ *
544
+ * - `overrides.regulation` → `regulation`
545
+ * - `backendURL` → `apiUrl`
546
+ *
547
+ * @internal
548
+ */
549
+ declare function _normalizeConfig(config: CookieYesConfig): _NormalizedConfig;
550
+
186
551
  type RawCookieFields = {
187
552
  consentid?: string;
188
553
  consent?: string;
189
554
  action?: string;
190
- necessary?: string;
191
- functional?: string;
192
- analytics?: string;
193
- performance?: string;
194
- advertisement?: string;
555
+ /** Taxonomy signature stored with the consent (see ResolvedCategories.taxonomyHash). */
556
+ tax?: string;
195
557
  lastRenewedDate?: string;
558
+ /** Every non-meta pair: category id → "yes" | "no". */
559
+ categories: Record<string, string>;
196
560
  };
197
561
  declare function parseCookie(raw: string): RawCookieFields;
198
562
  declare function serializeCookie(snapshot: ConsentSnapshot): string;
199
563
  declare function generateConsentId(): string;
200
564
 
565
+ /**
566
+ * One-time-per-page-load console warning for `mode: "offline"`.
567
+ * Both @cookieyes/core and @cookieyes/react call this so the wording and the
568
+ * "once" behavior stay identical no matter which package reads the setting.
569
+ */
570
+ declare function _warnOfflineModeDeprecated(): void;
571
+ /** @internal test-only — resets the one-time warning guard between test cases. */
572
+ declare function _resetOfflineModeWarning(): void;
573
+
574
+ type ConsentEmitter = {
575
+ /**
576
+ * Listen for consent events. `"save"` fires on every saved decision (even an
577
+ * unchanged re-confirm); `"change"` fires only when a category actually
578
+ * differs. The listener fires once immediately with the current state
579
+ * (`isInitial: true`) so a late listener isn't blind to earlier choices.
580
+ * Pass `{ category }` to only be called when that one category changes.
581
+ * Returns an unsubscribe function.
582
+ */
583
+ on: (type: ConsentEventType, listener: ConsentEventListener, options?: ConsentEventOptions) => () => void;
584
+ /** Feed in the committed categories after a save; the emitter fans out events. */
585
+ push: (categories: Record<string, boolean>) => void;
586
+ };
587
+ /**
588
+ * The consent event fan-out, shared by the core and React runtimes so both
589
+ * behave identically. `getCommitted` returns the consent currently in effect,
590
+ * used for the immediate replay a new listener receives.
591
+ */
592
+ declare function createConsentEmitter(getCommitted: () => Record<string, boolean>): ConsentEmitter;
593
+
594
+ type GcmValue = "granted" | "denied";
595
+ /**
596
+ * Compute the granted/denied value for every GCM signal from the current
597
+ * category consent, using each category's `gcm` mapping.
598
+ *
599
+ * - A signal is `granted` if *any* granted category maps to it.
600
+ * - `security_storage` is always `granted` (it's strictly necessary and not
601
+ * consentable — this mirrors production's behaviour).
602
+ * - A signal that no category maps to defaults to `denied`.
603
+ */
604
+ declare function computeGoogleConsent(resolved: ResolvedCategories, categories: Record<string, boolean>): Record<GoogleConsentSignal, GcmValue>;
605
+ /**
606
+ * Push a Consent Mode `update` for all seven signals onto the dataLayer, if one
607
+ * is present. Safe to call on load and on every consent change; a no-op when no
608
+ * Google service is on the page.
609
+ *
610
+ * The customer is responsible for the Consent Mode *default* (the gtag snippet
611
+ * that must run before their Google tags, typically denying everything). This
612
+ * function owns the *update* that reflects the visitor's actual choice.
613
+ */
614
+ declare function broadcastGoogleConsent(resolved: ResolvedCategories, categories: Record<string, boolean>): void;
615
+
201
616
  declare const en: TranslationMap;
202
617
 
618
+ /** The base subtag of a language tag, lowercased: "en-GB" → "en". */
619
+ declare function primaryOf(tag: string): string;
620
+ /** Reading direction for a language tag, e.g. "ar" or "ar-EG" → "rtl". */
621
+ declare function getTextDirection(tag: string): TextDirection;
622
+ /** Deep-merge a (possibly partial) override onto a complete base map. */
623
+ declare function mergeTranslations(base: TranslationMap, override?: PartialTranslations): TranslationMap;
624
+ /**
625
+ * The language to start in, resolved in order: explicit `locale`, then the
626
+ * browser's language, then English. Only returns one we actually have text for
627
+ * (others can be brought in later via `loadLanguage`).
628
+ */
629
+ declare function pickLanguage(i18n?: I18nConfig): string;
630
+ /** Full translations for the resolved starting language, English filling any gaps. */
203
631
  declare function resolveTranslations(i18n?: I18nConfig): TranslationMap;
204
632
 
633
+ type LanguageController = {
634
+ /** Text for the active language (English fills any gaps). */
635
+ getTranslations: () => TranslationMap;
636
+ getLanguageInfo: () => LanguageInfo;
637
+ /** Switch language live; loads via `i18n.loadLanguage` if not already present. */
638
+ setLanguage: (tag: string) => Promise<void>;
639
+ /**
640
+ * The customer's own text for a category in the *active* language, if they
641
+ * provided it — kept separate from the English defaults so a translation can
642
+ * win over a category's config label without the English default masking it.
643
+ */
644
+ getCategoryText: (id: string) => Partial<CategoryText> | undefined;
645
+ };
646
+ /**
647
+ * Owns the active language: which one is showing, its (English-filled) text,
648
+ * and switching to another — loading it on demand when a loader is provided.
649
+ * `onChange` runs after every switch so the UI can re-render.
650
+ *
651
+ * Framework-agnostic: used by both the core and React runtimes, so they behave
652
+ * identically.
653
+ */
654
+ declare function createLanguageController(i18n: I18nConfig | undefined, onChange: () => void): LanguageController;
655
+
205
656
  declare function createConsentManager(config: ConsentConfig): ConsentManager;
206
657
 
207
- declare function getOrCreateConsentRuntime(options: ConsentRuntimeOptions): ConsentRuntime;
658
+ declare function getOrCreateConsentRuntime(config: CookieYesConfig): ConsentRuntime;
659
+ /**
660
+ * Canonical setup entry point. Alias of {@link getOrCreateConsentRuntime} that
661
+ * accepts the same {@link CookieYesConfig} and returns the same process-wide
662
+ * singleton — provided so documentation can use one setup name (`initCookieYes`)
663
+ * across every package.
664
+ */
665
+ declare function initCookieYes(config: CookieYesConfig): ConsentRuntime;
208
666
  declare function resetConsentRuntime(): void;
209
667
 
210
- export { createConsentManager, en as defaultTranslations, generateConsentId, getOrCreateConsentRuntime, installNetworkBlocker, parseCookie, resetConsentRuntime, resolveTranslations, serializeCookie, uninstallNetworkBlocker };
211
- export type { ActiveUI, BlockedRequestInfo, ConsentBackend, ConsentCategory, ConsentChangePayload, ConsentConfig, ConsentManager, ConsentPayload, ConsentRuntime, ConsentRuntimeMode, ConsentRuntimeOptions, ConsentSnapshot, ConsentStore, ConsentStoreState, I18nConfig, NetworkBlockerConfig, NetworkBlockerRule, Regulation, ScriptEntry, ThemeConfig, TranslationMap };
668
+ export { DEFAULT_CATEGORIES, _clearStopHandlers, _normalizeConfig, _resetOfflineModeWarning, _warnOfflineModeDeprecated, broadcastGoogleConsent, computeGoogleConsent, createConsentEmitter, createConsentManager, createLanguageController, en as defaultTranslations, generateConsentId, getOrCreateConsentRuntime, getTextDirection, initCookieYes, installNetworkBlocker, mergeTranslations, parseCookie, pickLanguage, primaryOf, registerStopHandler, resetConsentRuntime, resolveBuiltInIntegration, resolveCategories, resolveTranslations, serializeCookie, uninstallNetworkBlocker };
669
+ export type { ActiveUI, AnyStopHandler, BlockedRequestInfo, BuiltInIntegration, CategoryDef, CategoryText, ColorScheme, ConsentBackend, ConsentCategory, ConsentChangePayload, ConsentConfig, ConsentEmitter, ConsentEventListener, ConsentEventOptions, ConsentEventPayload, ConsentEventType, ConsentManager, ConsentPayload, ConsentRuntime, ConsentRuntimeMode, ConsentRuntimeOptions, ConsentSnapshot, ConsentStore, ConsentStoreState, CookieYesConfig, CookieYesOfflineConfig, CookieYesSelfHostedConfig, GoogleConsentSignal, I18nConfig, LanguageController, LanguageInfo, NetworkBlockerConfig, NetworkBlockerRule, PartialTranslations, Regulation, ReloadNoticeState, ReloadOnlyHandler, ResolvedCategories, ScriptEntry, StopHandler, TextDirection, ThemeConfig, TranslationMap, _NormalizedConfig };