@c15t/scripts 3.0.0-alpha.0 → 3.0.0-alpha.1

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/AGENTS.md CHANGED
@@ -47,14 +47,17 @@ These docs describe v3. Start with Inth hosted setup, identify the framework, ro
47
47
  - [Ahrefs Analytics](./docs/integrations/ahrefs-analytics.md): Configure Ahrefs Analytics with c15t v3, understand measurement permission and verify loading and revocation.
48
48
  - [Amplitude](./docs/integrations/amplitude.md): Configure Amplitude with c15t v3, understand measurement permission and verify loading and revocation.
49
49
  - [Custom integrations](./docs/integrations/building-integrations.md): Define loading, initialization and consent-change behavior for a vendor without a helper.
50
+ - [Clear on revocation](./docs/integrations/clear-on-revocation.md): Remove configured first-party cookies and Web Storage keys when their consent category is denied.
50
51
  - [Clearbit](./docs/integrations/clearbit.md): Configure Clearbit with c15t v3, understand marketing permission and verify loading and revocation.
51
52
  - [Cloudflare Web Analytics](./docs/integrations/cloudflare-web-analytics.md): Configure Cloudflare Web Analytics with c15t v3, understand measurement permission and verify loading and revocation.
53
+ - [Cloudflare Zaraz](./docs/integrations/cloudflare-zaraz.md): Synchronize c15t permissions with Zaraz purposes while Cloudflare manages your tools.
52
54
  - [Crisp](./docs/integrations/crisp.md): Configure Crisp with c15t v3, understand functionality permission and verify loading and revocation.
53
55
  - [Databuddy](./docs/integrations/databuddy.md): Configure Databuddy's initial and updated consent state with c15t v3.
54
56
  - [Fathom Analytics](./docs/integrations/fathom-analytics.md): Configure Fathom Analytics with c15t v3, understand measurement permission and verify loading and revocation.
55
57
  - [Google Maps](./docs/integrations/google-maps.md): Prevent a map iframe from mounting before the required permission.
56
58
  - [Google Tag](./docs/integrations/google-tag.md): Configure gtag with c15t Consent Mode signals and understand its loading behavior.
57
59
  - [Google Tag Manager](./docs/integrations/google-tag-manager.md): Load GTM with c15t consent signals and verify the tags inside your container.
60
+ - [Granular consent](./docs/integrations/granular-consent.md): Let visitors grant a category and still turn one vendor off, without adopting IAB TCF.
58
61
  - [Heap](./docs/integrations/heap.md): Configure Heap with c15t v3, understand measurement permission and verify loading and revocation.
59
62
  - [Hightouch](./docs/integrations/hightouch.md): Configure Hightouch with c15t v3, understand measurement permission and verify loading and revocation.
60
63
  - [Hotjar](./docs/integrations/hotjar.md): Configure Hotjar with c15t v3, understand measurement permission and verify loading and revocation.
package/README.md CHANGED
@@ -37,6 +37,7 @@ For further information, guides, and examples visit the [reference documentation
37
37
  ## Integrations
38
38
 
39
39
  - **Google Tag Manager**: Loads with Google Consent Mode v2 defaults set to denied; GTM-managed tags fire only once matching consent is granted ([guide](https://c15t.com/docs/integrations/google-tag-manager))
40
+ - **Cloudflare Zaraz**: Maps c15t effective permissions to Zaraz purpose IDs for tools managed in Cloudflare ([guide](https://c15t.com/docs/integrations/cloudflare-zaraz))
40
41
  - **Google Analytics 4 + Google Ads (gtag.js)**: Consent Mode v2 defaults and consent updates when users make a choice ([guide](https://c15t.com/docs/integrations/google-tag))
41
42
  - **Conversion pixels**: Meta Pixel, OpenAI Pixel, TikTok Pixel, LinkedIn Insights, Microsoft UET (Microsoft Ads), X Pixel, Reddit Pixel, Snapchat Pixel
42
43
  - **Analytics**: PostHog, Amplitude, Heap, Segment, RudderStack, Hightouch, Mixpanel, Microsoft Clarity, Hotjar, Plausible, Fathom, Matomo, Umami, Vercel Analytics
@@ -292,6 +292,16 @@ const runtime_partitionConsentIds = function(mapping, consents) {
292
292
  deniedConsentIds
293
293
  };
294
294
  };
295
+ const ALL_DENIED = {
296
+ experience: false,
297
+ functionality: false,
298
+ marketing: false,
299
+ measurement: false,
300
+ necessary: true
301
+ };
302
+ const runtime_signalConsents = function(info) {
303
+ return info.vendor?.granted === false ? ALL_DENIED : info.consents;
304
+ };
295
305
  const runtime_getConsentSignalSteps = function(resolvedManifest, mode, consents) {
296
306
  if (!resolvedManifest.consentMapping || !resolvedManifest.consentSignal) return [];
297
307
  switch(resolvedManifest.consentSignal){
@@ -346,7 +356,8 @@ const runtime_resolvedManifestToScript = function(resolvedManifest) {
346
356
  defer: resolvedManifest.loadScript?.defer,
347
357
  id: resolvedManifest.vendor,
348
358
  persistAfterConsentRevoked: resolvedManifest.persistAfterConsentRevoked,
349
- src: resolvedManifest.loadScript?.src
359
+ src: resolvedManifest.loadScript?.src,
360
+ vendor: resolvedManifest.vendor
350
361
  };
351
362
  if (resolvedManifest.bootstrapSteps.length > 0 || resolvedManifest.setupSteps.length > 0 || resolvedManifest.onBeforeLoadGrantedSteps.length > 0 || resolvedManifest.onBeforeLoadDeniedSteps.length > 0 || hasConsentMapping) script.onBeforeLoad = (info)=>{
352
363
  const baseContext = {
@@ -359,7 +370,7 @@ const runtime_resolvedManifestToScript = function(resolvedManifest) {
359
370
  ...baseContext,
360
371
  phase: 'bootstrap'
361
372
  });
362
- runtime_executePhaseSteps(runtime_getConsentSignalSteps(resolvedManifest, 'default', info.consents), {
373
+ runtime_executePhaseSteps(runtime_getConsentSignalSteps(resolvedManifest, 'default', runtime_signalConsents(info)), {
363
374
  ...baseContext,
364
375
  phase: 'consent-default'
365
376
  });
@@ -415,7 +426,7 @@ const runtime_resolvedManifestToScript = function(resolvedManifest) {
415
426
  hasConsent: info.hasConsent,
416
427
  scriptId: resolvedManifest.vendor
417
428
  };
418
- runtime_executePhaseSteps(runtime_getConsentSignalSteps(resolvedManifest, 'update', info.consents), {
429
+ runtime_executePhaseSteps(runtime_getConsentSignalSteps(resolvedManifest, 'update', runtime_signalConsents(info)), {
419
430
  ...baseContext,
420
431
  phase: 'consent-update'
421
432
  });
package/dist/registry.js CHANGED
@@ -17,6 +17,16 @@ const BUILT_IN_INTEGRATION_CATEGORIES = [
17
17
  }
18
18
  ];
19
19
  const builtInScriptIntegrations = [
20
+ {
21
+ consentCategory: 'necessary',
22
+ docsSlug: 'cloudflare-zaraz',
23
+ hint: 'Consent bridge for tools managed by Zaraz',
24
+ integrationCategory: 'tag-manager',
25
+ key: 'cloudflareZaraz',
26
+ label: 'Cloudflare Zaraz',
27
+ packageSubpath: 'cloudflare-zaraz',
28
+ vendor: 'cloudflare-zaraz'
29
+ },
20
30
  {
21
31
  consentCategory: 'necessary',
22
32
  docsSlug: 'google-tag-manager',
@@ -48,7 +48,10 @@ const microsoft_clarity_getClarityConsentPayload = function(consents, defaultCon
48
48
  return fallback;
49
49
  };
50
50
  const microsoft_clarity_syncClarityConsent = function(info, defaultConsent) {
51
- window.clarity?.('consentv2', microsoft_clarity_getClarityConsentPayload(info.consents, defaultConsent));
51
+ window.clarity?.('consentv2', info.vendor?.granted === false ? {
52
+ ad_Storage: 'denied',
53
+ analytics_Storage: 'denied'
54
+ } : microsoft_clarity_getClarityConsentPayload(info.consents, defaultConsent));
52
55
  };
53
56
  const clarityManifest = {
54
57
  ...vendorManifestContract,
@@ -0,0 +1,98 @@
1
+ const consentCategories = [
2
+ 'necessary',
3
+ 'functionality',
4
+ 'experience',
5
+ 'measurement',
6
+ 'marketing'
7
+ ];
8
+ const readyEvent = 'zarazConsentAPIReady';
9
+ const isConsentApi = (api)=>'object' == typeof api && null !== api && 'APIReady' in api && true === api.APIReady && 'getAll' in api && 'function' == typeof api.getAll && 'set' in api && 'function' == typeof api.set && 'sendQueuedEvents' in api && 'function' == typeof api.sendQueuedEvents;
10
+ const getConsentApi = ()=>{
11
+ if ("u" < typeof window || !('zaraz' in window)) return;
12
+ const { zaraz } = window;
13
+ if ('object' != typeof zaraz || null === zaraz || !('consent' in zaraz)) return;
14
+ return isConsentApi(zaraz.consent) ? zaraz.consent : void 0;
15
+ };
16
+ const cloudflareZaraz = (options)=>{
17
+ const mappings = new Map();
18
+ for (const category of consentCategories)for (const purpose of options.purposes[category] ?? []){
19
+ if (!purpose.trim() || purpose !== purpose.trim()) throw new Error('Zaraz purpose IDs must be non-empty and have no surrounding whitespace.');
20
+ if (mappings.has(purpose)) throw new Error(`Zaraz purpose '${purpose}' is mapped more than once.`);
21
+ mappings.set(purpose, category);
22
+ }
23
+ if (0 === mappings.size) throw new Error('Map at least one Zaraz purpose to a c15t category.');
24
+ let latest;
25
+ let listeningDocument;
26
+ let initialized = false;
27
+ const pendingReplay = new Set();
28
+ const synchronize = ()=>{
29
+ const api = getConsentApi();
30
+ if (!latest || !api) return false;
31
+ const previous = api.getAll();
32
+ const permissions = {};
33
+ let changed = false;
34
+ for (const purpose of Object.keys(previous)){
35
+ const category = mappings.get(purpose);
36
+ const allowed = void 0 !== category && true === latest[category];
37
+ Object.defineProperty(permissions, purpose, {
38
+ enumerable: true,
39
+ value: allowed
40
+ });
41
+ changed ||= previous[purpose] !== allowed;
42
+ if (allowed && true !== previous[purpose]) pendingReplay.add(purpose);
43
+ }
44
+ for (const purpose of pendingReplay)if (true !== permissions[purpose]) pendingReplay.delete(purpose);
45
+ if (false !== options.hideBuiltInModal && true === api.modal) api.modal = false;
46
+ if (changed) api.set(permissions);
47
+ if (pendingReplay.size > 0 && false !== options.sendQueuedEvents) api.sendQueuedEvents();
48
+ pendingReplay.clear();
49
+ return true;
50
+ };
51
+ const apply = ()=>{
52
+ try {
53
+ if (!synchronize()) return false;
54
+ } catch (error) {
55
+ if (!options.onError) throw error;
56
+ options.onError(error);
57
+ return false;
58
+ }
59
+ listeningDocument?.removeEventListener(readyEvent, apply);
60
+ listeningDocument = void 0;
61
+ if (!initialized) {
62
+ initialized = true;
63
+ options.onReady?.();
64
+ }
65
+ return true;
66
+ };
67
+ const update = ({ consents })=>{
68
+ latest = consents;
69
+ if ("u" < typeof document) return;
70
+ let applied = false;
71
+ try {
72
+ applied = apply();
73
+ } finally{
74
+ if (!applied && !listeningDocument) {
75
+ listeningDocument = document;
76
+ document.addEventListener(readyEvent, apply);
77
+ }
78
+ }
79
+ };
80
+ return {
81
+ alwaysLoad: true,
82
+ callbackOnly: true,
83
+ category: 'necessary',
84
+ id: 'cloudflare-zaraz',
85
+ onConsentChange: (info)=>{
86
+ if (info.hasConsent) update(info);
87
+ },
88
+ onDispose: ()=>{
89
+ listeningDocument?.removeEventListener(readyEvent, apply);
90
+ listeningDocument = void 0;
91
+ latest = void 0;
92
+ initialized = false;
93
+ pendingReplay.clear();
94
+ },
95
+ onLoad: update
96
+ };
97
+ };
98
+ export { cloudflareZaraz };
@@ -1,5 +1,5 @@
1
1
  import type { ConsentState, Script, ScriptCallbackInfo } from '@c15t/core';
2
- import type { BuiltInScriptIntegrationKey } from '../registry';
2
+ import type { BuiltInScriptIntegrationKey } from '../registry.js';
3
3
  /**
4
4
  * Typed view of `globalThis` used by script helper tests.
5
5
  *
@@ -31,7 +31,7 @@ export interface ExpectedScriptSnapshot {
31
31
  /** Expected consent-revocation persistence flag from the generated script. */
32
32
  persistAfterConsentRevoked: boolean | undefined;
33
33
  /** Expected remote script URL from the generated helper script. */
34
- src: string;
34
+ src: string | undefined;
35
35
  }
36
36
  /**
37
37
  * Consent state with only required storage enabled.
@@ -1,3 +1,3 @@
1
- import type { ResolvedManifest, VendorManifest } from '../types';
1
+ import type { ResolvedManifest, VendorManifest } from '../types.js';
2
2
  export declare const interpolateValue: (value: unknown, config: Record<string, unknown>) => unknown;
3
3
  export declare const compileManifest: (manifest: VendorManifest, config?: Record<string, unknown>) => ResolvedManifest;
@@ -1,3 +1,3 @@
1
1
  import type { Script } from '@c15t/core';
2
- import type { ResolvedManifest } from '../types';
2
+ import type { ResolvedManifest } from '../types.js';
3
3
  export declare const resolvedManifestToScript: (resolvedManifest: ResolvedManifest) => Script;
@@ -77,6 +77,15 @@ export declare const BUILT_IN_INTEGRATION_CATEGORIES: readonly [{
77
77
  * the CLI can discover it from the same source.
78
78
  */
79
79
  export declare const builtInScriptIntegrations: readonly [{
80
+ readonly consentCategory: 'necessary';
81
+ readonly docsSlug: 'cloudflare-zaraz';
82
+ readonly hint: 'Consent bridge for tools managed by Zaraz';
83
+ readonly integrationCategory: 'tag-manager';
84
+ readonly key: 'cloudflareZaraz';
85
+ readonly label: 'Cloudflare Zaraz';
86
+ readonly packageSubpath: 'cloudflare-zaraz';
87
+ readonly vendor: 'cloudflare-zaraz';
88
+ }, {
80
89
  readonly consentCategory: 'necessary';
81
90
  readonly docsSlug: 'google-tag-manager';
82
91
  readonly hint: 'GTM container script';
@@ -1,5 +1,5 @@
1
1
  import type { Script } from '@c15t/core';
2
- import type { VendorManifest } from './types';
2
+ import type { VendorManifest } from './types.js';
3
3
  /**
4
4
  * Compiles a `VendorManifest` + config into a `Script` object.
5
5
  *
@@ -1,4 +1,4 @@
1
- import type { VendorManifest } from '../../types';
1
+ import type { VendorManifest } from '../../types.js';
2
2
  /**
3
3
  * Optional tracking call inserted between the vendor init call and script load.
4
4
  */
@@ -1,5 +1,5 @@
1
1
  import type { Script } from '@c15t/core';
2
- import type { VendorManifest } from '../../types';
2
+ import type { VendorManifest } from '../../types.js';
3
3
  declare global {
4
4
  interface Window {
5
5
  adobeDataLayer?: unknown[];
@@ -1,5 +1,5 @@
1
1
  import type { Script } from '@c15t/core';
2
- import type { VendorManifest } from '../../types';
2
+ import type { VendorManifest } from '../../types.js';
3
3
  declare global {
4
4
  interface Window {
5
5
  _paq?: unknown[];
@@ -0,0 +1,39 @@
1
+ import type { AllConsentNames, Script } from '@c15t/core';
2
+ /** The Zaraz Consent API methods used by the bridge. */
3
+ export interface ZarazConsentApi {
4
+ APIReady: boolean;
5
+ modal?: unknown;
6
+ getAll: () => Record<string, boolean>;
7
+ set: (permissions: Record<string, boolean>) => void;
8
+ sendQueuedEvents: () => void;
9
+ }
10
+ export interface CloudflareZarazOptions {
11
+ /** Map c15t categories to purpose IDs from the Zaraz dashboard. */
12
+ purposes: Partial<Record<AllConsentNames, readonly string[]>>;
13
+ /** Hide the currently visible Zaraz modal. Disable auto-display in Zaraz too. @default true */
14
+ hideBuiltInModal?: boolean;
15
+ /** Replay Zaraz's queued pageview events after a denied purpose becomes allowed. @default true */
16
+ sendQueuedEvents?: boolean;
17
+ /**
18
+ * Called once after the first successful synchronization. With automatic
19
+ * pageviews disabled in Zaraz, send the initial Pageview from this callback.
20
+ */
21
+ onReady?: () => void;
22
+ /** Called when synchronization fails. Retried on the next consent update or readiness event. */
23
+ onError?: (error: unknown) => void;
24
+ }
25
+ /**
26
+ * Synchronize c15t effective permissions with externally loaded Zaraz tools.
27
+ * Does not load Zaraz or configure tools. Unmapped Zaraz purposes are denied.
28
+ * Disable automatic pageviews and send the first Pageview from onReady to avoid
29
+ * running tools with a stale Zaraz consent cookie before synchronization.
30
+ *
31
+ * @param options - Purpose mapping and synchronization callbacks.
32
+ * @returns A callback-only script for the existing c15t script loader.
33
+ * @throws {Error} If the mapping is empty, contains blank IDs, or maps an ID twice.
34
+ * @example
35
+ * ```ts
36
+ * cloudflareZaraz({ purposes: { measurement: ['analytics-purpose'] } });
37
+ * ```
38
+ */
39
+ export declare const cloudflareZaraz: (options: CloudflareZarazOptions) => Script;
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.
@@ -45,3 +45,50 @@ assuming all helpers have the same network behavior.
45
45
  The loader exposes `updateScripts`, `getLoadedScriptIds` and `dispose`.
46
46
  Unloading an element cannot reverse requests or code that already ran. Test
47
47
  revocation and vendor cleanup with [verification](../../guides/verify-consent.md).
48
+
49
+ ## Dispose integration resources
50
+
51
+ Ordinary scripts keep their mounted resource when `updateScripts` receives a
52
+ fresh object with the same ID and unchanged element configuration. New callback
53
+ functions alone do not reload the vendor or send a temporary denial. Changing
54
+ the source, inline code or element attributes starts a new loading lifecycle.
55
+ Consent conditions are reevaluated on every configuration update. Pending
56
+ load and error events use the latest registered callbacks and consent state.
57
+ Setting `persistAfterConsentRevoked` to `false` removes an owned retained element
58
+ when the script no longer has consent.
59
+
60
+ Custom script configurations can use `onDispose(info)` to release event
61
+ listeners or other resources. Adding this hook opts the configuration into an
62
+ object-owned lifecycle: replacing the object disposes its resources and starts
63
+ again, even with the same ID. Keep these objects stable across framework
64
+ rerenders. The loader also calls the hook when the configuration is removed or
65
+ the loader is disposed, including configurations that never loaded.
66
+ `info.element` contains the last loaded or retained element when available,
67
+ even when configuration removal has already detached it.
68
+
69
+ Duplicate references receive one cleanup per registration. Re-registering a
70
+ removed object starts a new lifecycle. Updates requested from lifecycle
71
+ callbacks run after the current pass; if several are requested, the latest
72
+ configuration wins. Disposal stops further reconciliation. A callback feedback
73
+ loop exceeding 100 consecutive passes disposes the loader and reports an
74
+ `error` debug event. Avoid callbacks that keep changing consent or replacing
75
+ their own configuration.
76
+
77
+ `onBeforeLoad` prepares a loading attempt. If a callback changes consent or
78
+ replaces configurations, the loader cancels that attempt before loading and
79
+ reevaluates the latest state. A still-eligible script can retry preparation
80
+ with a new element and updated consent. Make `onBeforeLoad` safe to repeat;
81
+ use `onLoad` for initialization that requires a completed load. This also
82
+ applies to callback-only scripts, whose `onLoad` is skipped when preparation
83
+ invalidates the current pass.
84
+
85
+ Consent revocation alone does not call `onDispose`. Use `onConsentChange` for
86
+ vendor opt-out commands. Cleanup errors are reported through the loader's debug
87
+ events and do not prevent other configurations from being cleaned up.
88
+
89
+ ## Clear stored tracking data
90
+
91
+ Script gating does not remove cookies or Web Storage entries that a script
92
+ already wrote. Configure [clear on revocation](../../integrations/clear-on-revocation.md)
93
+ on your runtime, or attach its module to your existing kernel, to remove
94
+ declared data when its category is denied.
@@ -166,3 +166,45 @@ with DevTools, or follow [verification](../../guides/verify-consent.md) in your
166
166
  Use [custom integrations](../../integrations/building-integrations.md) for an
167
167
  unlisted vendor. Google helpers have a separate
168
168
  [Consent Mode contract](../../integrations/google-tag-manager.md).
169
+
170
+ ## Granular consent
171
+
172
+ A visitor can grant marketing and still turn one vendor off. Declare the
173
+ vendors next to the scripts in `lib/scripts.ts`, with the `vendor` slug each
174
+ script already carries, and pass both to `ConsentRoot`:
175
+
176
+ ```ts title="lib/scripts.ts"
177
+ import type { Vendor } from 'c15t';
178
+
179
+ export const vendors: Vendor[] = [
180
+ {
181
+ id: 'x-pixel',
182
+ name: 'X Pixel',
183
+ category: 'marketing',
184
+ privacyPolicyUrl: 'https://x.com/privacy',
185
+ },
186
+ ];
187
+ ```
188
+
189
+ ```tsx title="components/consent.tsx"
190
+ import { scripts, vendors } from '../lib/scripts';
191
+
192
+ <ConsentRoot state={state} config={consentConfig} scripts={scripts} vendors={vendors}>
193
+ {children}
194
+ </ConsentRoot>
195
+ ```
196
+
197
+ The preference center lists each vendor under its category with a switch.
198
+ Integrations from `@c15t/scripts` set `vendor` to their manifest slug, so
199
+ `xPixel()` needs no extra wiring; give a hand-written script the same slug
200
+ in its `vendor` field. A backend manifest can declare vendors too. See
201
+ [granular consent](../../integrations/granular-consent.md) for storage,
202
+ bulk actions and the hooks a custom control uses.
203
+
204
+ ## Clear stored tracking data
205
+
206
+ Script gating does not remove cookies or Web Storage entries that a script
207
+ already wrote. Add `clearOnRevocation` to your `ConsentRoot` or provider
208
+ options to remove declared data when its category is denied. See
209
+ [clear on revocation](../../integrations/clear-on-revocation.md) for configuration
210
+ and browser limits.
@@ -53,3 +53,11 @@ Use [custom integrations](../../integrations/building-integrations.md) for an
53
53
  unlisted vendor and [verification](../../guides/verify-consent.md) for the network
54
54
  checks. Google helpers have a separate
55
55
  [Consent Mode contract](../../integrations/google-tag-manager.md).
56
+
57
+ ## Clear stored tracking data
58
+
59
+ Script gating does not remove cookies or Web Storage entries that a script
60
+ already wrote. Add `clearOnRevocation` to `ConsentProvider.options` to remove
61
+ declared data when its category is denied. See
62
+ [clear on revocation](../../integrations/clear-on-revocation.md) for configuration
63
+ and browser limits.
@@ -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.