@c15t/svelte 3.0.0-alpha.0 → 3.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/AGENTS.md +3 -0
  2. package/dist/components/{frame.svelte.d.ts → consent-gate.svelte.d.ts} +3 -3
  3. package/dist/components/iab-purpose-item.svelte +3 -1
  4. package/dist/components/iab-stack-item.svelte +1 -1
  5. package/dist/components/iab-vendor-list.svelte +2 -0
  6. package/dist/components/manager-provider.svelte +207 -34
  7. package/dist/components/preferences.svelte +10 -10
  8. package/dist/components/vendor-list.svelte +190 -0
  9. package/dist/components/vendor-list.svelte.d.ts +18 -0
  10. package/dist/context.svelte.d.ts +36 -2
  11. package/dist/context.svelte.js +19 -1
  12. package/dist/index.d.ts +3 -1
  13. package/dist/index.js +3 -1
  14. package/dist/kit/routes.js +26 -1
  15. package/dist/kit/types.d.ts +10 -0
  16. package/dist/primitives/preference-item/index.d.ts +1 -0
  17. package/dist/primitives/preference-item/preference-item-content.svelte +17 -3
  18. package/dist/primitives/preference-item/preference-item-content.svelte.d.ts +10 -0
  19. package/dist/version.d.ts +1 -1
  20. package/dist/version.js +1 -1
  21. package/docs/README.md +3 -0
  22. package/docs/frameworks/sveltekit/quickstart.md +4 -0
  23. package/docs/guides/deployment-modes.md +12 -0
  24. package/docs/integrations/building-integrations.md +5 -0
  25. package/docs/integrations/clear-on-revocation.md +167 -0
  26. package/docs/integrations/cloudflare-zaraz.md +399 -0
  27. package/docs/integrations/google-maps.md +20 -20
  28. package/docs/integrations/granular-consent.md +208 -0
  29. package/docs/integrations/overview.md +5 -4
  30. package/docs/integrations/youtube.md +26 -21
  31. package/docs/upgrade-v3.md +47 -0
  32. package/package.json +7 -7
  33. package/readme.json +0 -36
  34. /package/dist/components/{frame.svelte → consent-gate.svelte} +0 -0
@@ -1,4 +1,4 @@
1
- import type { ActiveUI, AllConsentNames, ConsentKernel, ConsentPresentation, ResolvedConsentPresentation, ConsentSnapshot, ConsentState, ConsentType, HasCondition, KernelIABState, Model, TranslationConfig } from '@c15t/core';
1
+ import type { ActiveUI, AllConsentNames, ConsentKernel, ConsentPresentation, ResolvedConsentPresentation, ConsentSnapshot, ConsentState, ConsentType, HasCondition, KernelIABState, Model, ResolvedVendor, TranslationConfig } from '@c15t/core';
2
2
  import type { Theme, UIOptions } from '@c15t/ui/theme';
3
3
  import type { ConsentManagerOptions } from './types';
4
4
  export type SaveType = 'all' | 'custom' | 'necessary';
@@ -22,16 +22,34 @@ export interface SvelteIABState extends KernelIABState {
22
22
  }
23
23
  export interface ConsentDraftState {
24
24
  readonly values: Partial<ConsentState>;
25
+ /**
26
+ * Granted flag per declared vendor. Seeded from the denials the gate
27
+ * honors, so a vendor declared `disabled` reads `true` whatever an older
28
+ * record says; every vendor not denied is `true`. Empty under an `iab`
29
+ * policy.
30
+ */
31
+ readonly vendors: Readonly<Record<string, boolean>>;
25
32
  readonly isStale: boolean;
26
33
  set: (name: AllConsentNames, value: boolean) => void;
34
+ /**
35
+ * Stage one vendor's grant. Recorded by the next save. Ignored for a
36
+ * vendor that is not declared or is declared `disabled`, since the kernel
37
+ * would drop the grant on save.
38
+ *
39
+ * @param vendorId - Vendor slug as declared in `vendors` or on a script.
40
+ * @param granted - Whether the vendor may load once the draft is saved.
41
+ */
42
+ setVendor: (vendorId: string, granted: boolean) => void;
27
43
  reset: () => void;
28
44
  save: (categories: readonly AllConsentNames[]) => Promise<void>;
29
45
  }
30
- export interface ConsentManagerState extends Pick<ConsentSnapshot, 'explicitChoice' | 'effectivePermissions' | 'promptRequirement' | 'noticeDismissal' | 'privacySignals' | 'optOutDirectives' | 'resolution' | 'policyRule' | 'restrictions' | 'nextDeadline' | 'subject' | 'evaluatedAt' | 'evaluationPolicy' | 'policyPending' | 'location' | 'overrides' | 'revision' | 'translations' | 'user'> {
46
+ export interface ConsentManagerState extends Pick<ConsentSnapshot, 'explicitChoice' | 'effectivePermissions' | 'promptRequirement' | 'noticeDismissal' | 'privacySignals' | 'optOutDirectives' | 'resolution' | 'policyRule' | 'restrictions' | 'nextDeadline' | 'subject' | 'evaluatedAt' | 'evaluationPolicy' | 'policyPending' | 'location' | 'overrides' | 'revision' | 'translations' | 'user' | 'vendors' | 'vendorChoice'> {
31
47
  activeUI: ActiveUI;
32
48
  branding: NonNullable<ConsentSnapshot['branding']>;
33
49
  selectedConsents: Partial<ConsentState>;
34
50
  selectedConsentTypes: Partial<ConsentState>;
51
+ /** Granted flag per declared vendor in the draft. */
52
+ selectedVendors: Readonly<Record<string, boolean>>;
35
53
  presentation?: ConsentPresentation;
36
54
  readonly draft: ConsentDraftState;
37
55
  consentCategories: AllConsentNames[];
@@ -56,6 +74,15 @@ export interface ConsentManagerState extends Pick<ConsentSnapshot, 'explicitChoi
56
74
  legalLinks: ConsentManagerOptions['legalLinks'];
57
75
  translationConfig: TranslationConfig;
58
76
  getDisplayedConsents: () => ConsentType[];
77
+ /**
78
+ * The vendors listed under one category: presentable, naming that
79
+ * category, without a negation. Empty under an `iab` policy, where the
80
+ * TC string decides and vendor rows are not shown.
81
+ *
82
+ * @param category - The category row being rendered.
83
+ * @returns The vendors to list, in declared order.
84
+ */
85
+ getDisplayedVendors: (category: AllConsentNames) => ResolvedVendor[];
59
86
  has: (condition: HasCondition<AllConsentNames>) => boolean;
60
87
  dismissNotice: () => Promise<unknown>;
61
88
  saveConsents: (type: SaveType) => Promise<void>;
@@ -65,6 +92,13 @@ export interface ConsentManagerState extends Pick<ConsentSnapshot, 'explicitChoi
65
92
  setConsent: (name: AllConsentNames, value: boolean) => void;
66
93
  setLanguage: (code: string) => void;
67
94
  setSelectedConsent: (name: AllConsentNames, value: boolean) => void;
95
+ /**
96
+ * Stage one vendor's grant on the draft. Recorded by the next save.
97
+ *
98
+ * @param vendorId - Vendor slug as declared in `vendors` or on a script.
99
+ * @param granted - Whether the vendor may load once the draft is saved.
100
+ */
101
+ setSelectedVendor: (vendorId: string, granted: boolean) => void;
68
102
  subscribeToConsentChanges: (listener: (state: ConsentState) => void) => () => void;
69
103
  }
70
104
  export interface ConsentContextValue {
@@ -1,4 +1,4 @@
1
- import { allConsentNames, consentTypes as defaultConsentTypes, defaultTranslationConfig, has as evaluateHas, resolveConsentPresentation, } from '@c15t/core';
1
+ import { allConsentNames, consentTypes as defaultConsentTypes, defaultTranslationConfig, has as evaluateHas, resolveConsentPresentation, vendorsListedUnder, } from '@c15t/core';
2
2
  import { getContext, setContext } from 'svelte';
3
3
  const CONSENT_CONTEXT_KEY = Symbol('c15t-v3-consent');
4
4
  const THEME_CONTEXT_KEY = Symbol('c15t-v3-theme');
@@ -64,6 +64,12 @@ const createConsentState = function createConsentState(kernel, options) {
64
64
  getDisplayedConsents() {
65
65
  return displayedConsentTypes(controller.consentCategories);
66
66
  },
67
+ getDisplayedVendors(category) {
68
+ const snapshot = getSnapshotLocal();
69
+ return snapshot.model === 'iab'
70
+ ? []
71
+ : vendorsListedUnder(snapshot.vendors?.declared ?? [], category);
72
+ },
67
73
  has(condition) {
68
74
  const snapshot = getSnapshotLocal();
69
75
  return evaluateHas(condition, snapshot.effectivePermissions);
@@ -200,6 +206,9 @@ const createConsentState = function createConsentState(kernel, options) {
200
206
  get selectedConsentTypes() {
201
207
  return options.getDraft().values;
202
208
  },
209
+ get selectedVendors() {
210
+ return options.getDraft().vendors;
211
+ },
203
212
  setActiveUI(ui) {
204
213
  actionSequence += 1;
205
214
  kernel.set.activeUI(ui);
@@ -214,6 +223,9 @@ const createConsentState = function createConsentState(kernel, options) {
214
223
  setSelectedConsent(name, value) {
215
224
  options.getDraft().set(name, value);
216
225
  },
226
+ setSelectedVendor(vendorId, granted) {
227
+ options.getDraft().setVendor(vendorId, granted);
228
+ },
217
229
  subscribeToConsentChanges(listener) {
218
230
  return kernel.subscribe((snapshot) => listener(snapshot.effectivePermissions));
219
231
  },
@@ -223,6 +235,12 @@ const createConsentState = function createConsentState(kernel, options) {
223
235
  get translations() {
224
236
  return getSnapshotLocal().translations;
225
237
  },
238
+ get vendorChoice() {
239
+ return getSnapshotLocal().vendorChoice;
240
+ },
241
+ get vendors() {
242
+ return getSnapshotLocal().vendors;
243
+ },
226
244
  get user() {
227
245
  return getSnapshotLocal().user;
228
246
  },
package/dist/index.d.ts CHANGED
@@ -15,7 +15,9 @@ export { default as ConsentDialogLink } from './components/panel-link.svelte';
15
15
  export { default as ConsentDialogTrigger } from './components/panel-trigger.svelte';
16
16
  export { default as ConsentManagerProvider } from './components/manager-provider.svelte';
17
17
  export { default as ConsentWidget } from './components/preferences.svelte';
18
- export { default as Frame } from './components/frame.svelte';
18
+ export { default as ConsentGate } from './components/consent-gate.svelte';
19
+ /** @deprecated Renamed to `ConsentGate`. */
20
+ export { default as Frame } from './components/consent-gate.svelte';
19
21
  export { default as IABConsentBanner } from './components/iab-prompt.svelte';
20
22
  export { default as IABConsentDialog } from './components/iab-panel.svelte';
21
23
  export { getConsentKernel, getConsentManager, getHeadlessConsent, getIAB, getSnapshot, type HeadlessConsentSurfaceState, type SvelteIABState, } from './context.svelte';
package/dist/index.js CHANGED
@@ -12,7 +12,9 @@ export { default as ConsentDialogLink } from './components/panel-link.svelte';
12
12
  export { default as ConsentDialogTrigger } from './components/panel-trigger.svelte';
13
13
  export { default as ConsentManagerProvider } from './components/manager-provider.svelte';
14
14
  export { default as ConsentWidget } from './components/preferences.svelte';
15
- export { default as Frame } from './components/frame.svelte';
15
+ export { default as ConsentGate } from './components/consent-gate.svelte';
16
+ /** @deprecated Renamed to `ConsentGate`. */
17
+ export { default as Frame } from './components/consent-gate.svelte';
16
18
  export { default as IABConsentBanner } from './components/iab-prompt.svelte';
17
19
  export { default as IABConsentDialog } from './components/iab-panel.svelte';
18
20
  export { getConsentKernel, getConsentManager, getHeadlessConsent, getIAB, getSnapshot, } from './context.svelte';
@@ -15,7 +15,7 @@ import { deferInitGvlToRoute, serveGvlReference } from '@c15t/core';
15
15
  * - The init route is per-request (geo, language, GPC) and therefore
16
16
  * `private, no-store`.
17
17
  */
18
- import { fetchCachedManifest, getManifestAge, MANIFEST_PASSTHROUGH_HEADERS, } from '@c15t/core/server';
18
+ import { fetchCachedManifest, getManifestAge, MANIFEST_PASSTHROUGH_HEADERS, reportConsentSession, resolveSessionReportBackendURL, } from '@c15t/core/server';
19
19
  import { consentInputsToOverrides, extractConsentRequestInputs, resolveBackendURL, resolveInitFromManifest, } from '@c15t/schema/types';
20
20
  import { baseTranslations } from '@c15t/translations/all';
21
21
  const INIT_CACHE_CONTROL = 'private, no-store';
@@ -84,6 +84,17 @@ const resolveManifestSource = function resolveManifestSource(event, options) {
84
84
  }
85
85
  return { manifestURL: `${resolved}${MANIFEST_ROUTE_SUFFIX}` };
86
86
  };
87
+ /**
88
+ * Where the init route reports sessions, when it can: an absolute backend,
89
+ * read as configured rather than resolved against the request. A relative
90
+ * backend resolved to this app's origin is its own route, not a backend,
91
+ * and means no report; nothing is inferred from a manifest URL.
92
+ */
93
+ const resolveReportBackendURL = function resolveReportBackendURL(options) {
94
+ return resolveSessionReportBackendURL({
95
+ backendURL: options.backendURL ?? getEnv('C15T_BACKEND_URL'),
96
+ });
97
+ };
87
98
  const shouldFetchGvl = function shouldFetchGvl(manifest, payload) {
88
99
  return (manifest.iab?.enabled === true &&
89
100
  manifest.iab.gvl !== undefined &&
@@ -156,6 +167,20 @@ export const createSvelteKitConsentRouteHandlers = function createSvelteKitConse
156
167
  reference: manifest.iab.gvl,
157
168
  });
158
169
  }
170
+ if (options.reportSessions !== false) {
171
+ reportConsentSession({
172
+ adapter: '@c15t/svelte',
173
+ backendURL: resolveReportBackendURL(options),
174
+ fetch: options.fetch,
175
+ headers: event.request.headers,
176
+ init: payload,
177
+ inputs,
178
+ manifest,
179
+ method: event.request.method,
180
+ source: 'route',
181
+ waitUntil: bindBackgroundRevalidate(options, event),
182
+ });
183
+ }
159
184
  // The resolver's inputs are the only place GPC survives on the SSR
160
185
  // path — the browser never sends `Sec-GPC` to this route when the
161
186
  // page was server-rendered. Echo them back so the kernel folds the
@@ -53,4 +53,14 @@ export interface ConsentManifestOptions extends ManifestSourceConfig {
53
53
  * manifest is fresh or the request itself waits on the upstream.
54
54
  */
55
55
  onBackgroundRevalidate?: (revalidation: Promise<void>, event: RequestEvent) => void;
56
+ /**
57
+ * Report each init the route resolves to the backend's `POST /sessions`,
58
+ * server-to-server and detached from the response, so the backend still
59
+ * counts visitors it never served `/init` to. The report is handed to
60
+ * `onBackgroundRevalidate` like a manifest refresh. Set `false` to send
61
+ * none.
62
+ *
63
+ * @default true
64
+ */
65
+ reportSessions?: boolean;
56
66
  }
@@ -16,6 +16,7 @@ export declare const PreferenceItem: {
16
16
  children?: import("svelte").Snippet;
17
17
  class?: string;
18
18
  innerClassName?: string;
19
+ noStyle?: boolean;
19
20
  viewportClassName?: string;
20
21
  }, {}, "">;
21
22
  Control: import("svelte").Component<import("svelte/elements").HTMLAttributes<HTMLDivElement> & {
@@ -17,12 +17,23 @@
17
17
  children,
18
18
  class: localClassName,
19
19
  innerClassName,
20
+ noStyle: localNoStyle,
20
21
  viewportClassName,
21
22
  ...restProps
22
23
  }: HTMLAttributes<HTMLDivElement> & {
23
24
  children?: Snippet;
24
25
  class?: string;
25
26
  innerClassName?: string;
27
+ /**
28
+ * Drop the primitive's built-in classes. Falls back to the root's
29
+ * `noStyle`, like the React primitive and the sibling trigger, so a
30
+ * headless root strips the whole item. The IAB items set `noStyle`
31
+ * on their root only to own the item class and pass an explicit
32
+ * `false` here to keep the collapse rules; the consent widget
33
+ * inherits the root's `noStyle` and supplies the accordion classes
34
+ * itself, as React's does.
35
+ */
36
+ noStyle?: boolean;
26
37
  /** Presentation class for the consent widget viewport.
27
38
  * @internal
28
39
  */
@@ -33,14 +44,17 @@
33
44
  const triggerId = $derived(context.triggerId);
34
45
  const contentId = $derived(context.contentId);
35
46
  const dataState = $derived(getPreferenceItemState(open));
47
+ const noStyle = $derived(localNoStyle ?? context.noStyle);
36
48
  const contentClassName = $derived.by(() =>
37
- variants.content({ class: localClassName })
49
+ noStyle ? localClassName : variants.content({ class: localClassName })
38
50
  );
39
51
  const viewportClassNameValue = $derived.by(() =>
40
- variants.contentViewport({ class: viewportClassName })
52
+ noStyle
53
+ ? viewportClassName
54
+ : variants.contentViewport({ class: viewportClassName })
41
55
  );
42
56
  const innerClassNameValue = $derived.by(() =>
43
- variants.contentInner({ class: innerClassName })
57
+ noStyle ? innerClassName : variants.contentInner({ class: innerClassName })
44
58
  );
45
59
  </script>
46
60
 
@@ -4,6 +4,16 @@ type $$ComponentProps = HTMLAttributes<HTMLDivElement> & {
4
4
  children?: Snippet;
5
5
  class?: string;
6
6
  innerClassName?: string;
7
+ /**
8
+ * Drop the primitive's built-in classes. Falls back to the root's
9
+ * `noStyle`, like the React primitive and the sibling trigger, so a
10
+ * headless root strips the whole item. The IAB items set `noStyle`
11
+ * on their root only to own the item class and pass an explicit
12
+ * `false` here to keep the collapse rules; the consent widget
13
+ * inherits the root's `noStyle` and supplies the accordion classes
14
+ * itself, as React's does.
15
+ */
16
+ noStyle?: boolean;
7
17
  /** Presentation class for the consent widget viewport.
8
18
  * @internal
9
19
  */
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "3.0.0-alpha.0";
1
+ export declare const version = "3.0.0-alpha.2";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '3.0.0-alpha.0';
2
+ export const version = '3.0.0-alpha.2';
package/docs/README.md CHANGED
@@ -47,14 +47,17 @@ These docs describe v3. Start with Inth hosted setup, identify the framework, ro
47
47
  - [Ahrefs Analytics](./integrations/ahrefs-analytics.md): Configure Ahrefs Analytics with c15t v3, understand measurement permission and verify loading and revocation.
48
48
  - [Amplitude](./integrations/amplitude.md): Configure Amplitude with c15t v3, understand measurement permission and verify loading and revocation.
49
49
  - [Custom integrations](./integrations/building-integrations.md): Define loading, initialization and consent-change behavior for a vendor without a helper.
50
+ - [Clear on revocation](./integrations/clear-on-revocation.md): Remove configured first-party cookies and Web Storage keys when their consent category is denied.
50
51
  - [Clearbit](./integrations/clearbit.md): Configure Clearbit with c15t v3, understand marketing permission and verify loading and revocation.
51
52
  - [Cloudflare Web Analytics](./integrations/cloudflare-web-analytics.md): Configure Cloudflare Web Analytics with c15t v3, understand measurement permission and verify loading and revocation.
53
+ - [Cloudflare Zaraz](./integrations/cloudflare-zaraz.md): Synchronize c15t permissions with Zaraz purposes while Cloudflare manages your tools.
52
54
  - [Crisp](./integrations/crisp.md): Configure Crisp with c15t v3, understand functionality permission and verify loading and revocation.
53
55
  - [Databuddy](./integrations/databuddy.md): Configure Databuddy's initial and updated consent state with c15t v3.
54
56
  - [Fathom Analytics](./integrations/fathom-analytics.md): Configure Fathom Analytics with c15t v3, understand measurement permission and verify loading and revocation.
55
57
  - [Google Maps](./integrations/google-maps.md): Prevent a map iframe from mounting before the required permission.
56
58
  - [Google Tag](./integrations/google-tag.md): Configure gtag with c15t Consent Mode signals and understand its loading behavior.
57
59
  - [Google Tag Manager](./integrations/google-tag-manager.md): Load GTM with c15t consent signals and verify the tags inside your container.
60
+ - [Granular consent](./integrations/granular-consent.md): Let visitors grant a category and still turn one vendor off, without adopting IAB TCF.
58
61
  - [Heap](./integrations/heap.md): Configure Heap with c15t v3, understand measurement permission and verify loading and revocation.
59
62
  - [Hightouch](./integrations/hightouch.md): Configure Hightouch with c15t v3, understand measurement permission and verify loading and revocation.
60
63
  - [Hotjar](./integrations/hotjar.md): Configure Hotjar with c15t v3, understand measurement permission and verify loading and revocation.
@@ -116,6 +116,10 @@ the background. On adapters that expose `event.platform.context.waitUntil`
116
116
  (Cloudflare, Vercel edge) the handlers hand the refresh to it so it is not cut
117
117
  short when the response is sent; nothing is needed from you. For another
118
118
  lifetime API, pass `onBackgroundRevalidate(refresh, event)` to the handlers.
119
+ The init route hands its session report to the same hook: after each
120
+ resolution it posts a small report to the backend's `POST /sessions`,
121
+ server-to-server, so the backend still counts visitors it never served
122
+ `/init` to. Pass `reportSessions: false` to send none.
119
123
 
120
124
  ## Use static hosting
121
125
 
@@ -30,6 +30,18 @@ in your application while consent writes still go to Inth. Regular backend
30
30
  browser setup. See [data fetching and transports](./data-fetching.md) for
31
31
  the comparison, including custom transports and offline mode.
32
32
 
33
+ Manifest resolution removes the per-visitor `/init` request, and with it the
34
+ backend's only count of visitors. The server adapters replace it with a session
35
+ report: after each resolution, on a server-rendered page or the same-origin
36
+ init route, the host posts a small report to the backend's `POST /sessions`,
37
+ server-to-server and detached from the response. The browser makes no request
38
+ and the report stores no identity; the visitor's IP and user agent travel as
39
+ forwarded headers under the backend's usual IP handling. Each report is one
40
+ resolution; a page view can produce a `render` and a `route` report, and the
41
+ consuming side groups them into sessions by address and user agent within a
42
+ window. Static output resolves in the browser and sends none. Set `reportSessions: false` on an adapter to turn
43
+ it off.
44
+
33
45
  ## Match initialization to your application output
34
46
 
35
47
  | Application output | Initial state | Required setup |
@@ -31,6 +31,11 @@ Replace the example URL and implement the vendor's initialization. This is a
31
31
  loader template, not a functioning analytics SDK. The script stays blocked
32
32
  while measurement permission is denied.
33
33
 
34
+ Add `vendor: 'example-analytics'` and declare the vendor in the runtime's
35
+ `vendors` option or in the backend manifest when visitors should be able to
36
+ turn this vendor off inside a granted category. See
37
+ [granular consent](./granular-consent.md).
38
+
34
39
  ## Define revocation deliberately
35
40
 
36
41
  `onConsentChange` receives current permission information. Use it to update the
@@ -0,0 +1,167 @@
1
+ ---
2
+ title: Clear on revocation
3
+ description: Remove configured first-party cookies and Web Storage keys when
4
+ their consent category is denied.
5
+ group: integrations
6
+ ---
7
+
8
+ ## Configure cleanup
9
+
10
+ Add `clearOnRevocation` to your provider or runtime options. Declare only the
11
+ data owned by each optional category:
12
+
13
+ ```ts
14
+ import { hosted, type ClearOnRevocationConfig } from 'c15t';
15
+ import { createConsentRuntime } from 'c15t/runtime';
16
+
17
+ const clearOnRevocation = {
18
+ measurement: {
19
+ cookies: ['_ga', '_ga_*'],
20
+ localStorage: ['analytics:*'],
21
+ },
22
+ marketing: {
23
+ cookies: ['_fbp'],
24
+ sessionStorage: ['campaign-id'],
25
+ },
26
+ } satisfies ClearOnRevocationConfig;
27
+
28
+ const runtime = createConsentRuntime({
29
+ mode: hosted({ url: '/api/c15t' }),
30
+ clearOnRevocation,
31
+ });
32
+
33
+ // Start in the browser after mount. The endpoint must serve your c15t backend.
34
+ runtime.start();
35
+
36
+ // Call runtime.dispose() when the app no longer needs consent management.
37
+ ```
38
+
39
+ For React, pass the same configuration through `ConsentProvider.options`:
40
+
41
+ ```tsx
42
+ import type { ReactNode } from 'react';
43
+ import { ConsentProvider, hosted } from 'c15t/react';
44
+
45
+ const mode = hosted({ url: '/api/c15t' });
46
+
47
+ export function Consent({ children }: { children: ReactNode }) {
48
+ return (
49
+ <ConsentProvider
50
+ options={{
51
+ mode,
52
+ clearOnRevocation: {
53
+ measurement: { cookies: ['_ga', '_ga_*'] },
54
+ },
55
+ }}
56
+ >
57
+ {children}
58
+ </ConsentProvider>
59
+ );
60
+ }
61
+ ```
62
+
63
+ The same option is available in Next.js and TanStack Start `ConsentRoot` props, Vue and
64
+ Nuxt configuration, Svelte providers, and Astro integration options. Solid and
65
+ other headless integrations can use `createConsentRuntime` as shown above.
66
+ Every adapter uses the same cleanup module.
67
+
68
+ Omitting `clearOnRevocation` leaves cleanup disabled. The provider option is
69
+ initial-only. Remount the provider to replace its cleanup configuration. For
70
+ a shared runtime, configure the runtime owner rather than a borrowing provider.
71
+
72
+ ## Matching names and cookie scopes
73
+
74
+ Use an exact string or a nonempty prefix followed by `*`. `_ga_*` matches
75
+ `_ga_ABC123`; it does not match `_ga`. Regular expressions, wildcards in other
76
+ positions, and a bare `*` are unsupported. Web Storage keys may contain spaces,
77
+ Unicode, and punctuation. Cookie names use their raw spelling, without URL
78
+ decoding.
79
+
80
+ Cookies can share a name while having different domains or paths. Cleanup
81
+ tries the current host and its parent domains, and the current path and its
82
+ ancestors. To target a specific scope, use an object:
83
+
84
+ ```ts
85
+ const clearOnRevocation = {
86
+ measurement: {
87
+ cookies: [
88
+ { name: 'analytics-id', domain: 'example.com', path: '/' },
89
+ { name: 'checkout-metrics', domain: '', path: '/checkout' },
90
+ { name: 'partitioned-metrics', partitioned: true },
91
+ ],
92
+ },
93
+ };
94
+ ```
95
+
96
+ An empty `domain` means host-only. Explicit domains and paths replace the
97
+ automatic attempts for that field. Exact names can be deleted at a configured
98
+ path even when the current page cannot read that cookie. Prefix matching can
99
+ only discover cookie names visible to the current page, so use an exact name
100
+ for a cookie on another path.
101
+
102
+ Partitioned cookies require `partitioned: true`; ordinary targets remove
103
+ unpartitioned cookies. Cookie deletion preserves the browser's `__Secure-`
104
+ and `__Host-` prefix requirements. c15t protects its own consent, notice,
105
+ privacy, pending-save, and IAB consent records in cookies and localStorage,
106
+ including configured custom storage keys, even if your patterns match them.
107
+ c15t does not store consent in sessionStorage, so targeted entries there are
108
+ removed even when their names match consent storage keys.
109
+
110
+ ## When cleanup runs
111
+
112
+ The runtime attaches cleanup after persistence and the script loader. Cleanup
113
+ waits while the policy is pending. On the first settled snapshot, it removes
114
+ configured data for every denied category. This includes a new opt-in visitor
115
+ who has not made a choice and a returning visitor whose permission expired.
116
+
117
+ After that first pass, cleanup runs when a category changes from allowed to
118
+ denied. Saving a refusal, expiry, a policy change, Global Privacy Control,
119
+ or synchronized records can cause that transition. Under an opt-out policy,
120
+ an expired grant that remains effectively allowed does not trigger deletion.
121
+ The `necessary` category cannot be configured for cleanup.
122
+
123
+ Cleanup keeps waiting if a failed initial request leaves the policy pending.
124
+ If a previously settled policy falls back to denial after an initialization
125
+ failure, cleanup removes its configured data. A later successful retry cannot
126
+ restore deleted data.
127
+
128
+ Runtime construction, server rendering, draft checkbox edits, opening the
129
+ dialog, and disposal do not clear data. Cleanup does not poll storage or repeat
130
+ on unrelated UI updates.
131
+
132
+ ## Use an existing kernel
133
+
134
+ For a manually assembled integration, attach the module in the browser after
135
+ persistence hydration and script-loader setup:
136
+
137
+ ```ts
138
+ import { createClearOnRevocation } from 'c15t/modules/clear-on-revocation';
139
+
140
+ const cleanup = createClearOnRevocation({
141
+ kernel,
142
+ config: { measurement: { cookies: ['_ga', '_ga_*'] } },
143
+ storageConfig,
144
+ });
145
+
146
+ // Stop observing consent when this integration is torn down.
147
+ cleanup.dispose();
148
+ ```
149
+
150
+ Here `kernel` is your existing consent kernel. Pass the same `storageConfig`
151
+ used by persistence so cleanup protects custom record keys. Attaching the
152
+ module can immediately clear denied categories if the policy is already
153
+ settled. Do not also attach it when your provider or runtime owns cleanup.
154
+
155
+ ## Browser limits
156
+
157
+ Cleanup can remove JavaScript-accessible first-party cookies and keys in the
158
+ current origin's `localStorage` and `sessionStorage`. It cannot remove
159
+ `HttpOnly` cookies or another origin's data. Keep `HttpOnly` protections and
160
+ use your server to expire cookies that require server access. Cookie deletion
161
+ must match the cookie's scope. See the
162
+ [browser cookie documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies).
163
+
164
+ Browser restrictions can prevent reads or deletions. Cleanup failures do not
165
+ block consent updates. A running SDK may write data again after a sweep, so
166
+ keep script gating and the integration's consent-change or teardown behavior
167
+ configured. Deleting a script element cannot undo JavaScript it already ran.