@c15t/scripts 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 (88) hide show
  1. package/AGENTS.md +7 -0
  2. package/README.md +4 -3
  3. package/dist/e2e-test-utils.js +5 -3
  4. package/dist/engine/runtime.js +14 -3
  5. package/dist/events.js +218 -0
  6. package/dist/registry.js +40 -0
  7. package/dist/vendors/ads-and-pixels/pinterest-tag.js +123 -0
  8. package/dist/vendors/analytics/google-tag.js +14 -2
  9. package/dist/vendors/analytics/microsoft-clarity.js +4 -1
  10. package/dist/vendors/analytics/one-dollar-stats.js +30 -0
  11. package/dist/vendors/analytics/segment.js +10 -1
  12. package/dist/vendors/functional/front-chat.js +64 -0
  13. package/dist/vendors/tag-managers/cloudflare-zaraz.js +98 -0
  14. package/dist/vendors/tag-managers/google-tag-manager.js +17 -3
  15. package/dist-types/__tests__/helpers.d.ts +2 -2
  16. package/dist-types/engine/compile.d.ts +1 -1
  17. package/dist-types/engine/runtime.d.ts +1 -1
  18. package/dist-types/events.d.ts +46 -0
  19. package/dist-types/registry.d.ts +36 -0
  20. package/dist-types/resolve.d.ts +1 -1
  21. package/dist-types/vendors/_shared/install-builders.d.ts +1 -1
  22. package/dist-types/vendors/ads-and-pixels/pinterest-tag.d.ts +295 -0
  23. package/dist-types/vendors/analytics/adobe-analytics.d.ts +1 -1
  24. package/dist-types/vendors/analytics/google-tag.d.ts +3 -1
  25. package/dist-types/vendors/analytics/matomo-analytics.d.ts +1 -1
  26. package/dist-types/vendors/analytics/one-dollar-stats.d.ts +39 -0
  27. package/dist-types/vendors/analytics/segment.d.ts +7 -1
  28. package/dist-types/vendors/functional/front-chat.d.ts +62 -0
  29. package/dist-types/vendors/tag-managers/cloudflare-zaraz.d.ts +39 -0
  30. package/dist-types/vendors/tag-managers/google-tag-manager.d.ts +3 -1
  31. package/docs/README.md +7 -0
  32. package/docs/customization/overview.md +4 -3
  33. package/docs/customization/recipes.md +4 -2
  34. package/docs/customization/tokens.md +66 -3
  35. package/docs/frameworks/javascript/script-loader.md +53 -0
  36. package/docs/frameworks/next/script-loader.md +60 -12
  37. package/docs/frameworks/react/script-loader.md +14 -0
  38. package/docs/guides/consent-state.md +327 -0
  39. package/docs/guides/deployment-modes.md +12 -0
  40. package/docs/guides/shared-consent-controls.md +158 -0
  41. package/docs/integrations/adobe-analytics.md +1 -1
  42. package/docs/integrations/ahrefs-analytics.md +1 -1
  43. package/docs/integrations/amplitude.md +1 -1
  44. package/docs/integrations/building-integrations.md +5 -0
  45. package/docs/integrations/clear-on-revocation.md +167 -0
  46. package/docs/integrations/clearbit.md +1 -1
  47. package/docs/integrations/cloudflare-web-analytics.md +1 -1
  48. package/docs/integrations/cloudflare-zaraz.md +399 -0
  49. package/docs/integrations/crisp.md +1 -1
  50. package/docs/integrations/databuddy.md +1 -1
  51. package/docs/integrations/fathom-analytics.md +1 -1
  52. package/docs/integrations/front-chat.md +322 -0
  53. package/docs/integrations/google-maps.md +21 -21
  54. package/docs/integrations/google-tag-manager.md +1 -1
  55. package/docs/integrations/google-tag.md +1 -1
  56. package/docs/integrations/granular-consent.md +210 -0
  57. package/docs/integrations/heap.md +1 -1
  58. package/docs/integrations/hightouch.md +1 -1
  59. package/docs/integrations/hotjar.md +1 -1
  60. package/docs/integrations/intercom.md +1 -1
  61. package/docs/integrations/linkedin-insights.md +1 -1
  62. package/docs/integrations/logrocket.md +1 -1
  63. package/docs/integrations/matomo-analytics.md +1 -1
  64. package/docs/integrations/meta-pixel.md +1 -1
  65. package/docs/integrations/microsoft-clarity.md +1 -1
  66. package/docs/integrations/microsoft-uet.md +1 -1
  67. package/docs/integrations/mixpanel-analytics.md +1 -1
  68. package/docs/integrations/one-dollar-stats.md +305 -0
  69. package/docs/integrations/openai-pixel.md +1 -1
  70. package/docs/integrations/overview.md +22 -18
  71. package/docs/integrations/pinterest-tag.md +321 -0
  72. package/docs/integrations/pirsch.md +1 -1
  73. package/docs/integrations/plausible-analytics.md +1 -1
  74. package/docs/integrations/posthog.md +1 -1
  75. package/docs/integrations/promptwatch.md +1 -1
  76. package/docs/integrations/reddit-pixel.md +1 -1
  77. package/docs/integrations/rudderstack.md +1 -1
  78. package/docs/integrations/rybbit-analytics.md +1 -1
  79. package/docs/integrations/segment.md +1 -1
  80. package/docs/integrations/snapchat-pixel.md +1 -1
  81. package/docs/integrations/tiktok-pixel.md +1 -1
  82. package/docs/integrations/umami-analytics.md +1 -1
  83. package/docs/integrations/vercel-analytics.md +1 -1
  84. package/docs/integrations/x-pixel.md +1 -1
  85. package/docs/integrations/youtube.md +27 -22
  86. package/docs/upgrade-v3.md +176 -1
  87. package/package.json +30 -2
  88. package/readme.json +0 -19
@@ -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.
@@ -194,7 +194,7 @@ clientEntrypoint: fileURLToPath(new URL('./src/c15t.client.ts', import.meta.url)
194
194
  Export the scripts from that module:
195
195
 
196
196
  ```ts title="src/c15t.client.ts"
197
- import type { C15tClientOptionsExtension } from '@c15t/astro';
197
+ import type { C15tClientOptionsExtension } from 'c15t/astro';
198
198
  import { scripts } from './consent-scripts';
199
199
 
200
200
  export default { scripts } satisfies C15tClientOptionsExtension;
@@ -194,7 +194,7 @@ clientEntrypoint: fileURLToPath(new URL('./src/c15t.client.ts', import.meta.url)
194
194
  Export the scripts from that module:
195
195
 
196
196
  ```ts title="src/c15t.client.ts"
197
- import type { C15tClientOptionsExtension } from '@c15t/astro';
197
+ import type { C15tClientOptionsExtension } from 'c15t/astro';
198
198
  import { scripts } from './consent-scripts';
199
199
 
200
200
  export default { scripts } satisfies C15tClientOptionsExtension;
@@ -0,0 +1,399 @@
1
+ ---
2
+ title: Cloudflare Zaraz
3
+ description: Synchronize c15t permissions with Zaraz purposes while Cloudflare
4
+ manages your tools.
5
+ group: integrations
6
+ ---
7
+
8
+ ## Configure Zaraz before registering the bridge
9
+
10
+ This helper connects c15t to an existing Zaraz installation. It does not insert a
11
+ script, configure Cloudflare tools, or turn a standalone SDK into a server-side
12
+ integration. Configure each tool in Zaraz and remove its previous standalone
13
+ loader, including any duplicate `@c15t/scripts` helper.
14
+
15
+ In the Zaraz dashboard:
16
+
17
+ 1. Enable Consent Management and create purposes for the categories you use.
18
+ 2. Assign a purpose to every tool requiring permission. Zaraz tools without a
19
+ purpose bypass consent checks.
20
+ 3. Disable automatic display of the Zaraz consent modal. c15t owns the UI.
21
+ 4. Disable **Automatic Pageview Tracking**. Disable automatic SPA pageviews too
22
+ if your application will emit them itself.
23
+ 5. Copy the purpose IDs into the mapping below. Every active purpose returned by `zaraz.consent.getAll()` and omitted
24
+ from the mapping is denied by this bridge. Register one bridge per application.
25
+
26
+ Zaraz keeps a separate consent cookie. Its automatic pageview can run before
27
+ c15t resolves the current permissions, using a grant from a previous visit.
28
+ Disabling that pageview and emitting it from `onReady` prevents this particular
29
+ startup race. Do not send other events before synchronization, and audit any
30
+ custom triggers that run independently. DOM-ready, timer or click triggers can
31
+ run with stale permissions before the bridge mounts. The bridge cannot undo
32
+ requests sent before it starts.
33
+
34
+ Cloudflare documents [purpose assignment](https://developers.cloudflare.com/zaraz/consent-management/)
35
+ and [automatic pageview settings](https://developers.cloudflare.com/zaraz/reference/settings/).
36
+
37
+ ## Register the consent bridge
38
+
39
+ | Package manager | Command |
40
+ | :-------------- | :-------------------------- |
41
+ | npm | `npm install @c15t/scripts` |
42
+ | pnpm | `pnpm add @c15t/scripts` |
43
+ | yarn | `yarn add @c15t/scripts` |
44
+ | bun | `bun add @c15t/scripts` |
45
+
46
+ ```ts title="src/consent-scripts.ts"
47
+ import { cloudflareZaraz } from '@c15t/scripts/cloudflare-zaraz';
48
+
49
+ // Zaraz provides this global after its loader runs.
50
+ declare const zaraz: { track: (event: string) => void };
51
+
52
+ export const scripts = [
53
+ cloudflareZaraz({
54
+ purposes: {
55
+ measurement: ['your-measurement-purpose-id'],
56
+ marketing: ['your-marketing-purpose-id'],
57
+ },
58
+ onReady: () => {
59
+ zaraz.track('Pageview');
60
+ },
61
+ }),
62
+ ];
63
+ ```
64
+
65
+ Use actual IDs from your dashboard, not the purpose names. A category may map to
66
+ several purposes, but a purpose cannot appear more than once. Empty mappings,
67
+ blank IDs and duplicate IDs throw during construction. Zaraz only returns purposes attached to enabled tools from `getAll()`. IDs absent
68
+ from that result cannot receive a grant; compare the mapping with
69
+ `zaraz.consent.getAll()` when troubleshooting.
70
+
71
+ Include the mapped categories in your c15t policy. The bridge reads effective
72
+ permissions, including policy restrictions, rather than treating every allowed
73
+ category as a recorded visitor choice.
74
+
75
+ For the shared registration examples below, keep Zaraz's own loader and its
76
+ configured dashboard tools. Remove duplicate standalone vendor loaders only.
77
+ For this integration, c15t owns permission synchronization and Zaraz owns tool
78
+ loading.
79
+
80
+ ## Register the scripts
81
+
82
+ Complete your [framework quickstart](https://c15t.com/docs/frameworks) first. Keep its Inth
83
+ endpoint, policy, styles and consent UI. Remove the vendor's original script,
84
+ SDK initializer or tag-manager entry so c15t owns loading once.
85
+
86
+ The `scripts` export in `src/consent-scripts.ts` is a configuration, not an
87
+ initializer. Add it to your existing consent owner using the registration point
88
+ below. These are partial edits to that owner, not additional providers.
89
+
90
+ **Next.js**
91
+
92
+ Import the configuration into the client boundary from your router guide:
93
+
94
+ ```ts
95
+ import { ConsentRoot } from 'c15t/next';
96
+ import { scripts } from './consent-scripts';
97
+ ```
98
+
99
+ Keep the server-resolved `state` and shared `consentConfig` from your
100
+ router guide. Its manifest, init and save URLs stay in effect. Add
101
+ `scripts` as a top-level prop on the existing root:
102
+
103
+ ```tsx
104
+ <ConsentRoot state={state} config={consentConfig} scripts={scripts}>
105
+ {children}
106
+ </ConsentRoot>
107
+ ```
108
+
109
+ For a Pages Router or static-export setup using `ConsentProvider`, add
110
+ `scripts` to its existing `options` instead. Keep the router-specific setup
111
+ from [Next.js script loading](../frameworks/next/script-loader.md).
112
+
113
+ **TanStack Start**
114
+
115
+ In your existing root route component, import the scripts alongside
116
+ `ConsentRoot`. Keep the server loader from the [TanStack Start quickstart](https://c15t.com/docs/frameworks/tanstack-start/quickstart).
117
+
118
+ ```tsx
119
+ import { Outlet } from '@tanstack/react-router';
120
+ import { ConsentRoot } from 'c15t/tanstack-start';
121
+ import { scripts } from '../consent-scripts';
122
+
123
+ function Root() {
124
+ const state = Route.useLoaderData();
125
+ return (
126
+ <ConsentRoot state={state} backendURL={backendURL} initRoute={false} scripts={scripts}>
127
+ <Outlet />
128
+ {/* Keep your consent banner, dialog and preferences link here. */}
129
+ </ConsentRoot>
130
+ );
131
+ }
132
+ ```
133
+
134
+ This edits the existing route. `Route` and `backendURL` come from its setup;
135
+ keep the document shell and head components if they are part of your root.
136
+ `initRoute={false}` keeps the quickstart's direct-backend initialization.
137
+ If your app mounts a consent server route, retain its existing `initRoute`
138
+ instead. Do not return script callbacks from a server function or route loader.
139
+
140
+ **React**
141
+
142
+ Import the scripts into your existing provider component:
143
+
144
+ ```ts
145
+ import { ConsentProvider } from 'c15t/react';
146
+ import { scripts } from './consent-scripts';
147
+ ```
148
+
149
+ Keep the existing options and add `scripts`:
150
+
151
+ ```tsx
152
+ <ConsentProvider options={{ ...consentOptions, scripts }}>
153
+ {children}
154
+ </ConsentProvider>
155
+ ```
156
+
157
+ Here `consentOptions` is your existing configuration, including
158
+ `mode: hosted({ url: backendURL })`. Keep the banner, dialog and preferences
159
+ link inside the provider. See [React script loading](../frameworks/react/script-loader.md).
160
+
161
+ **Nuxt**
162
+
163
+ Attach one loader from the root `app.vue`, after the Nuxt module has
164
+ started its browser runtime. This keeps vendor callbacks in application code rather
165
+ than serialized `nuxt.config.ts` runtime configuration.
166
+
167
+ ```vue title="app/app.vue"
168
+ <script setup lang="ts">
169
+ import { onUnmounted } from 'vue';
170
+ import { createScriptLoader } from 'c15t/modules/script-loader';
171
+ import { scripts } from '../src/consent-scripts';
172
+
173
+ const nuxtApp = useNuxtApp();
174
+ const kernel = useConsentKernel();
175
+ let loader: ReturnType<typeof createScriptLoader> | undefined;
176
+
177
+ const removeMountedHook = nuxtApp.hook('app:mounted', () => {
178
+ loader = createScriptLoader({ kernel, scripts });
179
+ });
180
+ onUnmounted(() => {
181
+ removeMountedHook();
182
+ loader?.dispose();
183
+ });
184
+ </script>
185
+
186
+ <template>
187
+ <ConsentRoot />
188
+ <NuxtPage />
189
+ </template>
190
+ ```
191
+
192
+ Merge the setup code into your root and retain its footer and preferences
193
+ link. `useConsentKernel` is auto-imported by the c15t Nuxt module. Adjust the
194
+ relative script import if your `app.vue` is at the project root. This loader
195
+ waits until the module has applied browser persistence and privacy signals,
196
+ then reads the current snapshot and observes future changes. Do not also register these scripts
197
+ in another loader. See the [Nuxt quickstart](https://c15t.com/docs/frameworks/nuxt/quickstart).
198
+
199
+ **Vue**
200
+
201
+ Use the kernel already provided by the Vue plugin. Merge this setup into
202
+ `App.vue`, whose lifetime covers the application:
203
+
204
+ ```vue title="src/App.vue"
205
+ <script setup lang="ts">
206
+ import { onMounted, onUnmounted } from 'vue';
207
+ import { createScriptLoader } from 'c15t/modules/script-loader';
208
+ import { useConsentKernel } from 'c15t/vue/vue-plugin';
209
+ import ConsentRoot from 'c15t/vue/consent-root';
210
+ import { scripts } from './consent-scripts';
211
+
212
+ const kernel = useConsentKernel();
213
+ let loader: ReturnType<typeof createScriptLoader> | undefined;
214
+
215
+ onMounted(() => {
216
+ loader = createScriptLoader({ kernel, scripts });
217
+ });
218
+ onUnmounted(() => loader?.dispose());
219
+ </script>
220
+
221
+ <template>
222
+ <ConsentRoot />
223
+ <main>Your application</main>
224
+ </template>
225
+ ```
226
+
227
+ Keep your existing page content and preferences link. The plugin still owns
228
+ the kernel and persistence; this component owns only the vendor loader.
229
+ Do not register the same scripts in plugin configuration as well. See the
230
+ [Vue quickstart](https://c15t.com/docs/frameworks/vue/quickstart).
231
+
232
+ **Astro**
233
+
234
+ Point the existing Astro integration at a client module. Keep its `mode`,
235
+ `ui` and framework integration from the [Astro quickstart](https://c15t.com/docs/frameworks/astro/quickstart).
236
+ Import `fileURLToPath` in your Astro configuration:
237
+
238
+ ```js title="astro.config.mjs"
239
+ import { fileURLToPath } from 'node:url';
240
+ ```
241
+
242
+ Add this option to the existing `c15t({ ... })` call. Resolve the path from
243
+ the configuration file because Astro injects the import into a virtual module:
244
+
245
+ ```js
246
+ clientEntrypoint: fileURLToPath(new URL('./src/c15t.client.ts', import.meta.url)),
247
+ ```
248
+
249
+ Export the scripts from that module:
250
+
251
+ ```ts title="src/c15t.client.ts"
252
+ import type { C15tClientOptionsExtension } from 'c15t/astro';
253
+ import { scripts } from './consent-scripts';
254
+
255
+ export default { scripts } satisfies C15tClientOptionsExtension;
256
+ ```
257
+
258
+ The integration passes this extension to its shared browser runtime. Vendor
259
+ helpers contain callbacks, so do not put them in the serialized `scripts`
260
+ option in `astro.config.mjs`. Keep one runtime across consent islands and
261
+ `ClientRouter` navigation.
262
+
263
+ **Svelte**
264
+
265
+ Import the scripts in the component that owns your existing provider and
266
+ pass them as a top-level prop:
267
+
268
+ ```svelte title="src/App.svelte"
269
+ <script lang="ts">
270
+ import { ConsentManagerProvider, hosted } from '@c15t/svelte';
271
+ import { scripts } from './consent-scripts';
272
+
273
+ const backendURL = import.meta.env.VITE_C15T_BACKEND_URL;
274
+ if (!backendURL) throw new Error('Set VITE_C15T_BACKEND_URL');
275
+ const mode = hosted({ url: backendURL });
276
+ </script>
277
+
278
+ <ConsentManagerProvider {mode} {scripts}>
279
+ <!-- Keep your application, consent UI and preferences link here. -->
280
+ </ConsentManagerProvider>
281
+ ```
282
+
283
+ Retain the styles and consent UI from the [Svelte quickstart](https://c15t.com/docs/frameworks/svelte/quickstart).
284
+ The provider owns the loader and disposes it on unmount.
285
+
286
+ **SvelteKit**
287
+
288
+ Add the scripts to the existing root layout provider. Keep the server load
289
+ and its serializable prefetch data from the [SvelteKit quickstart](https://c15t.com/docs/frameworks/sveltekit/quickstart).
290
+
291
+ ```svelte title="src/routes/+layout.svelte"
292
+ <script lang="ts">
293
+ import { ConsentManagerProvider, hosted } from '@c15t/svelte';
294
+ import { scripts } from '../consent-scripts';
295
+
296
+ let { children, data } = $props();
297
+ const mode = hosted({ url: data.backendURL });
298
+ </script>
299
+
300
+ <ConsentManagerProvider {mode} {scripts} prefetch={data.prefetch}>
301
+ {@render children()}
302
+ <!-- Keep your consent UI and preferences link here. -->
303
+ </ConsentManagerProvider>
304
+ ```
305
+
306
+ Import vendor helpers in the layout component, not in `+layout.server.ts`.
307
+ For static hosting, keep your browser-only `mode` setup and omit request
308
+ prefetch; the `scripts` prop stays the same. If you pass an externally owned
309
+ `runtime` to the provider, register scripts when creating that runtime instead.
310
+
311
+ **JavaScript**
312
+
313
+ Attach the loader to your existing kernel before calling
314
+ `kernel.commands.init()`:
315
+
316
+ ```ts
317
+ import { createScriptLoader } from 'c15t/modules/script-loader';
318
+ import { scripts } from './consent-scripts';
319
+
320
+ const loader = createScriptLoader({ kernel, scripts });
321
+ ```
322
+
323
+ Call `loader.dispose()` when that application instance is destroyed.
324
+ `kernel` is the hosted kernel from your quickstart. A provider-owned kernel
325
+ already has a loader; do not attach a second one. See
326
+ [JavaScript script loading](../frameworks/javascript/script-loader.md).
327
+
328
+ ## Loading and updates
329
+
330
+ The helper returns an `alwaysLoad`, `callbackOnly` configuration with category
331
+ `necessary`. This lets consent synchronization run for every visitor. It does
332
+ not make the downstream analytics or advertising tools necessary.
333
+
334
+ Zaraz normally injects its own loader. If auto-injection is disabled, install
335
+ [Zaraz manually](https://developers.cloudflare.com/zaraz/advanced/load-zaraz-manually/)
336
+ once. The bridge supports either loading order: an already-ready API is updated
337
+ immediately; otherwise it waits for `zarazConsentAPIReady` and applies the latest
338
+ c15t permissions. No polling is used.
339
+
340
+ On a change, the bridge calls `zaraz.consent.set()` before
341
+ `zaraz.consent.sendQueuedEvents()`. It flushes Zaraz's queued pageviews only when
342
+ a purpose changes from denied to allowed. Revocation updates purposes to false
343
+ and does not flush events. Repeated identical permissions do not rewrite the
344
+ Zaraz cookie. `onReady` runs once after the first successful synchronization,
345
+ including when all optional purposes are denied. A pageview sent there remains
346
+ subject to Zaraz's purpose checks.
347
+
348
+ | Option | Default | Behavior |
349
+ | ------------------ | -------- | ---------------------------------------------------------------------------- |
350
+ | `purposes` | Required | Maps categories to Zaraz purpose IDs; unmapped purposes are denied |
351
+ | `hideBuiltInModal` | `true` | Hides the currently visible modal; also disable auto-display in Cloudflare |
352
+ | `sendQueuedEvents` | `true` | Replays Zaraz's queued pageviews after new grants |
353
+ | `onReady` | Unset | Runs after initial permission synchronization |
354
+ | `onError` | Unset | Receives synchronization errors so the application can report or handle them |
355
+
356
+ If a Zaraz API call throws, `onReady` does not run until synchronization
357
+ succeeds. Use `onError(error)` to report the failure and prevent application
358
+ events from relying on permissions that were not applied. The bridge retries
359
+ with the latest permissions on the next consent update or readiness event;
360
+ it does not poll or schedule automatic retries. Readiness retries remain
361
+ registered when `onError` is omitted and an error reaches the loader debug hook. A failed queued-event replay
362
+ remains pending until synchronization succeeds while that purpose is still
363
+ allowed. Revoking the purpose cancels its pending replay. Zaraz can partially
364
+ process a queue before throwing, so retries cannot guarantee exactly-once delivery. Without `onError`, synchronous
365
+ failures reach the script loader's debug events and readiness-event failures
366
+ reach the browser's error handler. A failed revocation can leave the previous
367
+ Zaraz grant in place.
368
+
369
+ If the bridge starts before saved consent or policy resolution is available,
370
+ it applies the kernel's current effective permissions and updates them when
371
+ initialization completes. Pass restored state during setup when available.
372
+ The bridge does not force a denial when the kernel already permits a purpose.
373
+
374
+ Set `sendQueuedEvents: false` if your application deliberately discards
375
+ pre-consent pageviews. Send subsequent route events only after readiness and
376
+ avoid combining manual route events with Zaraz's automatic SPA pageviews.
377
+
378
+ Removing or replacing the configuration, or disposing its loader, detaches the
379
+ readiness listener. Disposal does not revoke consent, clear vendor storage, or
380
+ stop a tool that has already initialized. Save the denied permissions before
381
+ teardown when revocation is required. Zaraz controls subsequent tool execution;
382
+ test vendor-specific behavior for scripts with their own ongoing activity.
383
+
384
+ ## Verify the configured tools
385
+
386
+ Use a test environment with isolated destinations. Check a first visit, a return
387
+ visit with stale Zaraz grants, measurement-only acceptance, marketing-only
388
+ acceptance, rejection, and revocation. Inspect both `zaraz.consent.getAll()` and
389
+ actual tool activity. An updated consent object alone does not prove that a
390
+ misconfigured tool stopped sending data.
391
+
392
+ This integration maps c15t categories to Zaraz purposes. It does not translate
393
+ IAB TCF vendor and purpose choices or replace Zaraz's separate TCF configuration.
394
+
395
+ [Cloudflare Web Analytics](./cloudflare-web-analytics.md) is a
396
+ separate analytics product with its own loader. Zaraz manages multiple tools,
397
+ which can have different consent requirements and execution costs. Moving a
398
+ tool to Zaraz can reduce browser work, but this bridge alone does not establish
399
+ a performance improvement for that tool.
@@ -194,7 +194,7 @@ clientEntrypoint: fileURLToPath(new URL('./src/c15t.client.ts', import.meta.url)
194
194
  Export the scripts from that module:
195
195
 
196
196
  ```ts title="src/c15t.client.ts"
197
- import type { C15tClientOptionsExtension } from '@c15t/astro';
197
+ import type { C15tClientOptionsExtension } from 'c15t/astro';
198
198
  import { scripts } from './consent-scripts';
199
199
 
200
200
  export default { scripts } satisfies C15tClientOptionsExtension;
@@ -200,7 +200,7 @@ clientEntrypoint: fileURLToPath(new URL('./src/c15t.client.ts', import.meta.url)
200
200
  Export the scripts from that module:
201
201
 
202
202
  ```ts title="src/c15t.client.ts"
203
- import type { C15tClientOptionsExtension } from '@c15t/astro';
203
+ import type { C15tClientOptionsExtension } from 'c15t/astro';
204
204
  import { scripts } from './consent-scripts';
205
205
 
206
206
  export default { scripts } satisfies C15tClientOptionsExtension;
@@ -194,7 +194,7 @@ clientEntrypoint: fileURLToPath(new URL('./src/c15t.client.ts', import.meta.url)
194
194
  Export the scripts from that module:
195
195
 
196
196
  ```ts title="src/c15t.client.ts"
197
- import type { C15tClientOptionsExtension } from '@c15t/astro';
197
+ import type { C15tClientOptionsExtension } from 'c15t/astro';
198
198
  import { scripts } from './consent-scripts';
199
199
 
200
200
  export default { scripts } satisfies C15tClientOptionsExtension;