keksi 0.1.5 → 0.1.6

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/README.md CHANGED
@@ -110,6 +110,47 @@ posthog.capture('signup');
110
110
  - `poster` is optional and should be self-hosted. YouTube's thumbnails come from `i.ytimg.com`, which would contact
111
111
  Google before consent.
112
112
 
113
+ ## Other providers
114
+
115
+ For a provider keksi doesn't have, describe it in `providers`. It's listed in the banner, the settings and the
116
+ policy page, and handled like the built-in ones.
117
+
118
+ ```ts
119
+ export default defineConfig({
120
+ ga: 'G-XXXXXXX',
121
+ providers: [
122
+ {
123
+ name: 'Leadfeeder',
124
+ company: 'Dealfront GmbH',
125
+ privacyPolicy: 'https://www.dealfront.com/privacy-policy/',
126
+ category: 'marketing',
127
+ purpose: { fi: 'Vierailevien yritysten tunnistus.', en: 'Identifying visiting companies.' },
128
+ cookies: [{ name: '_lfa', duration: '2y' }],
129
+ hosts: ['lfeeder.com'],
130
+ script: 'https://sc.lfeeder.com/lftracker_v1_XXXX.js',
131
+ },
132
+ ],
133
+ });
134
+ ```
135
+
136
+ - `script` is loaded after consent in its category.
137
+ - First-party `cookies` are deleted on withdrawal; `*` works as a wildcard. Add `host` to ones set on the provider's
138
+ domain.
139
+ - `hosts` are what `watchTracking` checks for.
140
+ - Adding a provider asks returning visitors again.
141
+
142
+ If the provider needs setup code, leave out `script` and load it yourself:
143
+
144
+ ```ts
145
+ const load = () => {
146
+ if (window.keksi?.consent?.marketing) {
147
+ // inject the script and run its setup
148
+ }
149
+ };
150
+ load();
151
+ document.addEventListener('keksi:change', load);
152
+ ```
153
+
113
154
  ## API
114
155
 
115
156
  ```ts
@@ -170,5 +211,5 @@ pnpm test
170
211
  pnpm test:e2e # Astro and SvelteKit examples
171
212
  ```
172
213
 
173
- To add a provider: add its data to `src/providers.ts`, a config key with the same id to `KeksiConfig`, and its
214
+ To build in a provider that's used across projects: add its data to `src/providers.ts`, a config key with the same id to `KeksiConfig`, and its
174
215
  loading to `load()` in `src/runtime.ts`.
package/dist/index.js CHANGED
@@ -230,7 +230,11 @@ var posthogHost = (config) => (config.posthog?.host ?? "https://eu.i.posthog.com
230
230
  function activeProviders(config) {
231
231
  return providers.filter(
232
232
  (p) => p.id === "youtube" ? config.youtube : p.id === "posthog" ? config.posthog?.key : list(config[p.id]).length
233
- ).map((p) => p.id === "posthog" && /\/\/us\./.test(posthogHost(config)) ? { ...p, transfer: "us" } : p);
233
+ ).map((p) => p.id === "posthog" && /\/\/us\./.test(posthogHost(config)) ? { ...p, transfer: "us" } : p).concat((config.providers ?? []).map(custom));
234
+ }
235
+ function custom({ purpose, hosts = [], script: _, ...p }) {
236
+ const text = (l) => typeof purpose === "string" ? purpose : purpose[l] ?? Object.values(purpose).find(Boolean) ?? "";
237
+ return { ...p, id: `custom:${p.name}`, purpose: { fi: text("fi"), sv: text("sv"), en: text("en") }, hosts };
234
238
  }
235
239
  function categoriesOf(config) {
236
240
  return ["analytics", "marketing"].filter((c) => activeProviders(config).some((p) => p.category === c));
@@ -262,6 +266,10 @@ function runtimeData(config, locale) {
262
266
  ads: list(config.googleAds),
263
267
  meta: list(config.meta),
264
268
  ph: config.posthog?.key ? { key: config.posthog.key, host: posthogHost(config) } : void 0,
269
+ scripts: {
270
+ analytics: (config.providers ?? []).filter((p) => p.script && p.category === "analytics").map((p) => p.script),
271
+ marketing: (config.providers ?? []).filter((p) => p.script && p.category === "marketing").map((p) => p.script)
272
+ },
265
273
  yt: !!config.youtube,
266
274
  banner: needsBanner(config),
267
275
  policy: policyOf(config, locale),
@@ -336,7 +344,7 @@ function renderKeksi(config, { lang = "fi", nonce } = {}) {
336
344
  const btn = (action, label, type = "button") => `<button type="${type}" class="keksi-btn" data-keksi="${action}">${e(label)}</button>`;
337
345
  const uses = cats.map((c) => `<li><strong>${e(t[c])}:</strong> ${e(active.filter((p) => p.category === c).map((p) => p.name).join(", "))}</li>`).join("");
338
346
  const json = JSON.stringify(runtimeData(config, lang)).replace(/</g, "\\u003c");
339
- return `<style${n}>${css}</style><section id="keksi-banner" class="keksi" aria-labelledby="keksi-title" hidden><div class="keksi-main"><div class="keksi-head"><h2 id="keksi-title">${e(t.title)}</h2></div><div class="keksi-body"><p>${e(t.intro)}</p><ul>${uses}</ul><p>${e(t.change)}${policy ? ` <a href="${e(policy)}">${e(t.more)}</a>` : ""}</p></div><div class="keksi-actions"><button type="button" class="keksi-text" data-keksi-open>${e(t.settings)}</button>${btn("reject", t.reject)}${btn("accept", t.accept)}</div></div><form id="keksi-settings" class="keksi-settings" aria-labelledby="keksi-settings-title" hidden><div class="keksi-head"><h2 id="keksi-settings-title">${e(t.settingsTitle)}</h2><button type="button" class="keksi-close" data-keksi="close" aria-label="${e(t.close)}">\xD7</button></div><div class="keksi-body">${sections(config, lang, 3, true)}</div><div class="keksi-actions">${btn("save", t.save)}${btn("reject", t.reject)}${btn("accept", t.accept)}</div></form></section><script type="application/json" id="keksi-data">${json}</script><script${n}>${'"use strict";(()=>{var F=(n,s)=>new RegExp(`^${n.split("*").map(i=>i.replace(/[.+?^${}()|[\\]\\\\]/g,"\\\\$&")).join(".*")}$`).test(s);var _=["analytics","marketing"],v={analytics:!1,marketing:!1};function f(...n){return arguments}function H(n,s=window){if(typeof s.keksi?.sync=="function")return s.keksi.sync(),s.keksi;let i=s.document,g=e=>i.getElementById(e),c=I(),A=!1,w=!1,L=!1,E=new Set,x=f("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied"});if(n.ga.length||n.ads.length){let e=s.dataLayer??(s.dataLayer=[]);s.gtag=function(){e.push(arguments)},e.push(x)}if(n.meta.length){let e=function(){e.callMethod?e.callMethod.apply(e,[...arguments]):e.queue.push(arguments)};Object.assign(e,{push:e,loaded:!0,version:"2.0",queue:[]}),s.fbq=s._fbq=e}if(n.ph){let e=Object.assign([],{_i:[],people:[]});for(let o of"capture identify alias reset register register_once unregister group setPersonProperties getFeatureFlag isFeatureEnabled onFeatureFlags reloadFeatureFlags get_distinct_id startSessionRecording stopSessionRecording captureException on".split(" "))e[o]=(...t)=>e.push([o,...t]);s.posthog=e}function I(){let e=i.cookie.split("; ").find(l=>l.startsWith(`${n.name}=`))?.slice(n.name.length+1),[o,,t,a]=e?.split(".")??[];if(o!==n.v||!t||!a)return null;let r={analytics:a.includes("a"),marketing:a.includes("m")};return{id:t,c:r,yt:r.marketing||a.includes("y"),done:!a.includes("u")}}function T(e,o=e.marketing,t=!0){let a=c?.id??Array.from(s.crypto.getRandomValues(new Uint8Array(8)),l=>l.toString(16).padStart(2,"0")).join(""),r=(t?"":"u")+(e.analytics?"a":"")+(e.marketing?"m":"")+(o&&!e.marketing?"y":"")||"-";return i.cookie=`${n.name}=${n.v}.${Date.now().toString(36)}.${a}.${r}; Path=/; Max-Age=${n.days*86400}; SameSite=Lax`+(n.domain?`; Domain=${n.domain}`:"")+(s.location.protocol==="https:"?"; Secure":""),{id:a,c:e,yt:o,done:t}}function $(e){let o=i.createElement("script");return o.async=!0,o.src=e,i.head.append(o),o}function C(e){let o=[...e.analytics?n.ga:[],...e.marketing?n.ads:[]].filter(t=>!E.has(t));if(o.length){let t=e.marketing?"granted":"denied",a=[f("consent","update",{analytics_storage:e.analytics?"granted":"denied",ad_storage:t,ad_user_data:t,ad_personalization:t})];E.size||a.push(f("js",new Date));for(let r of o)a.push(f("config",r)),E.add(r);if(A)for(let r of a)s.dataLayer.push(r);else{let r=s.dataLayer;r.splice(r.indexOf(x)+1,0,...a),$(`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(o[0])}`),A=!0}}if(e.marketing&&n.meta.length&&!w&&(w=!0,s.fbq.queue.unshift(...n.meta.map(t=>f("init",t)),f("track","PageView")),$("https://connect.facebook.net/en_US/fbevents.js")),e.analytics&&n.ph&&!L&&(L=!0,s.posthog._i.push([n.ph.key,{api_host:n.ph.host,defaults:"2026-08-30"},"posthog"]),$(`${n.ph.host.replace(".i.posthog.com","-assets.i.posthog.com")}/static/array.js`).crossOrigin="anonymous"),c?.yt)for(let t of i.querySelectorAll("keksi-youtube"))t.show(!0)}function O(){c?.yt||(c=T(c?.c??v,!0,c?.done??!1)),C(c.c)}function R(e){let o=e.flatMap(l=>n.del[l]),t=l=>o.some(u=>F(u,l)),a=s.location.hostname.split("."),r=["",...a.map((l,u)=>`; Domain=${a.slice(u).join(".")}`)];for(let l of i.cookie.split("; ")){let u=l.split("=")[0];if(t(u))for(let b of r)i.cookie=`${u}=; Max-Age=0; Path=/${b}`}try{for(let l of[s.localStorage,s.sessionStorage])for(let u of Object.keys(l))t(u)&&l.removeItem(u)}catch{}}function p(e){let o=c;c=T(e),h(),i.dispatchEvent(new s.CustomEvent("keksi:change",{detail:{...e}})),R(_.filter(t=>!e[t])),_.some(t=>o?.c[t]&&!e[t])||o?.yt&&!c.yt?s.location.reload():C(e)}function y(e=!1,o=n.banner&&!c?.done||e){let t=g("keksi-banner"),a=g("keksi-settings");!t||!a||(t.hidden=!o,a.hidden=!e,t.firstElementChild.hidden=e)}function h(){y()}function M(){let e=g("keksi-settings");for(let o of _){let t=e?.querySelector(`[name=${o}]`);t&&(t.checked=!!c?.c[o])}y(!0),e?.querySelector(".keksi-close")?.focus()}i.addEventListener("click",e=>{let o=e.target.closest?.("[data-keksi],[data-keksi-open]");if(!o)return;if(o.hasAttribute("data-keksi-open"))return e.preventDefault(),M();let t=o.getAttribute("data-keksi");if(t==="close")y();else if(t==="accept")p({analytics:!0,marketing:!0});else if(t==="reject")p(v);else if(t==="save"){let a=r=>!!g("keksi-settings")?.querySelector(`[name=${r}]`)?.checked;p({analytics:a("analytics"),marketing:a("marketing")})}}),i.addEventListener("keydown",e=>{e.key==="Escape"&&g("keksi-settings")?.hidden===!1&&y()}),i.addEventListener("astro:after-swap",h);class D extends s.HTMLElement{connectedCallback(){this.show(!!c?.yt)}show(o,t=!1){let a=encodeURIComponent(this.getAttribute("video")??""),r=this.getAttribute("label");if(o){if(this.querySelector("iframe"))return;let d=i.createElement("iframe");d.src=`https://www.youtube-nocookie.com/embed/${a}${t?"?autoplay=1":""}`,d.title=r??"YouTube",d.allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",d.allowFullscreen=!0,d.referrerPolicy="strict-origin-when-cross-origin",this.replaceChildren(d),t&&d.focus();return}let l=i.createElement("div");l.className="keksi-yt";let u=this.getAttribute("poster");u&&l.style.setProperty("--keksi-poster",`url(${JSON.stringify(u)})`);let b=i.createElement("p");b.textContent=n.t.ytText;let m=i.createElement("button");m.type="button",m.textContent=n.t.ytButton,r&&m.setAttribute("aria-label",`${n.t.ytButton}: ${r}`),m.onclick=()=>{this.show(!0,!0),O()};let S=i.createElement("p"),q=(d,N,P=!1)=>{let k=i.createElement("a");k.href=d,k.textContent=N,P&&(k.target="_blank",k.rel="noopener"),S.append(S.childNodes.length?" \\xB7 ":"",k)};q(`https://www.youtube.com/watch?v=${a}`,n.t.ytOpen,!0),n.policy&&q(n.policy,n.t.more),l.append(b,m,S),this.replaceChildren(l)}}n.yt&&!s.customElements.get("keksi-youtube")&&s.customElements.define("keksi-youtube",D);let j={get consent(){return c?.done?{...c.c}:null},open:M,save:e=>p({...c?.c??v,...e}),acceptAll:()=>p({analytics:!0,marketing:!0}),rejectAll:()=>p(v),sync:h};return s.keksi=j,h(),c&&C(c.c),j}H(JSON.parse(document.getElementById("keksi-data").textContent));})();'}</script>`;
347
+ return `<style${n}>${css}</style><section id="keksi-banner" class="keksi" aria-labelledby="keksi-title" hidden><div class="keksi-main"><div class="keksi-head"><h2 id="keksi-title">${e(t.title)}</h2></div><div class="keksi-body"><p>${e(t.intro)}</p><ul>${uses}</ul><p>${e(t.change)}${policy ? ` <a href="${e(policy)}">${e(t.more)}</a>` : ""}</p></div><div class="keksi-actions"><button type="button" class="keksi-text" data-keksi-open>${e(t.settings)}</button>${btn("reject", t.reject)}${btn("accept", t.accept)}</div></div><form id="keksi-settings" class="keksi-settings" aria-labelledby="keksi-settings-title" hidden><div class="keksi-head"><h2 id="keksi-settings-title">${e(t.settingsTitle)}</h2><button type="button" class="keksi-close" data-keksi="close" aria-label="${e(t.close)}">\xD7</button></div><div class="keksi-body">${sections(config, lang, 3, true)}</div><div class="keksi-actions">${btn("save", t.save)}${btn("reject", t.reject)}${btn("accept", t.accept)}</div></form></section><script type="application/json" id="keksi-data">${json}</script><script${n}>${'"use strict";(()=>{var H=(n,s)=>new RegExp(`^${n.split("*").map(i=>i.replace(/[.+?^${}()|[\\]\\\\]/g,"\\\\$&")).join(".*")}$`).test(s);var E=["analytics","marketing"],$={analytics:!1,marketing:!1};function f(...n){return arguments}function I(n,s=window){if(typeof s.keksi?.sync=="function")return s.keksi.sync(),s.keksi;let i=s.document,g=e=>i.getElementById(e),c=O(),A=!1,w=!1,L=!1,S=new Set,x=new Set,T=f("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied"});if(n.ga.length||n.ads.length){let e=s.dataLayer??(s.dataLayer=[]);s.gtag=function(){e.push(arguments)},e.push(T)}if(n.meta.length){let e=function(){e.callMethod?e.callMethod.apply(e,[...arguments]):e.queue.push(arguments)};Object.assign(e,{push:e,loaded:!0,version:"2.0",queue:[]}),s.fbq=s._fbq=e}if(n.ph){let e=Object.assign([],{_i:[],people:[]});for(let o of"capture identify alias reset register register_once unregister group setPersonProperties getFeatureFlag isFeatureEnabled onFeatureFlags reloadFeatureFlags get_distinct_id startSessionRecording stopSessionRecording captureException on".split(" "))e[o]=(...t)=>e.push([o,...t]);s.posthog=e}function O(){let e=i.cookie.split("; ").find(l=>l.startsWith(`${n.name}=`))?.slice(n.name.length+1),[o,,t,a]=e?.split(".")??[];if(o!==n.v||!t||!a)return null;let r={analytics:a.includes("a"),marketing:a.includes("m")};return{id:t,c:r,yt:r.marketing||a.includes("y"),done:!a.includes("u")}}function M(e,o=e.marketing,t=!0){let a=c?.id??Array.from(s.crypto.getRandomValues(new Uint8Array(8)),l=>l.toString(16).padStart(2,"0")).join(""),r=(t?"":"u")+(e.analytics?"a":"")+(e.marketing?"m":"")+(o&&!e.marketing?"y":"")||"-";return i.cookie=`${n.name}=${n.v}.${Date.now().toString(36)}.${a}.${r}; Path=/; Max-Age=${n.days*86400}; SameSite=Lax`+(n.domain?`; Domain=${n.domain}`:"")+(s.location.protocol==="https:"?"; Secure":""),{id:a,c:e,yt:o,done:t}}function h(e){let o=i.createElement("script");return o.async=!0,o.src=e,i.head.append(o),o}function C(e){let o=[...e.analytics?n.ga:[],...e.marketing?n.ads:[]].filter(t=>!S.has(t));if(o.length){let t=e.marketing?"granted":"denied",a=[f("consent","update",{analytics_storage:e.analytics?"granted":"denied",ad_storage:t,ad_user_data:t,ad_personalization:t})];S.size||a.push(f("js",new Date));for(let r of o)a.push(f("config",r)),S.add(r);if(A)for(let r of a)s.dataLayer.push(r);else{let r=s.dataLayer;r.splice(r.indexOf(T)+1,0,...a),h(`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(o[0])}`),A=!0}}e.marketing&&n.meta.length&&!w&&(w=!0,s.fbq.queue.unshift(...n.meta.map(t=>f("init",t)),f("track","PageView")),h("https://connect.facebook.net/en_US/fbevents.js")),e.analytics&&n.ph&&!L&&(L=!0,s.posthog._i.push([n.ph.key,{api_host:n.ph.host,defaults:"2026-08-30"},"posthog"]),h(`${n.ph.host.replace(".i.posthog.com","-assets.i.posthog.com")}/static/array.js`).crossOrigin="anonymous");for(let t of E.flatMap(a=>e[a]?n.scripts[a]:[]))x.has(t)||(x.add(t),h(t));if(c?.yt)for(let t of i.querySelectorAll("keksi-youtube"))t.show(!0)}function R(){c?.yt||(c=M(c?.c??$,!0,c?.done??!1)),C(c.c)}function D(e){let o=e.flatMap(l=>n.del[l]),t=l=>o.some(u=>H(u,l)),a=s.location.hostname.split("."),r=["",...a.map((l,u)=>`; Domain=${a.slice(u).join(".")}`)];for(let l of i.cookie.split("; ")){let u=l.split("=")[0];if(t(u))for(let v of r)i.cookie=`${u}=; Max-Age=0; Path=/${v}`}try{for(let l of[s.localStorage,s.sessionStorage])for(let u of Object.keys(l))t(u)&&l.removeItem(u)}catch{}}function p(e){let o=c;c=M(e),b(),i.dispatchEvent(new s.CustomEvent("keksi:change",{detail:{...e}})),D(E.filter(t=>!e[t])),E.some(t=>o?.c[t]&&!e[t])||o?.yt&&!c.yt?s.location.reload():C(e)}function y(e=!1,o=n.banner&&!c?.done||e){let t=g("keksi-banner"),a=g("keksi-settings");!t||!a||(t.hidden=!o,a.hidden=!e,t.firstElementChild.hidden=e)}function b(){y()}function j(){let e=g("keksi-settings");for(let o of E){let t=e?.querySelector(`[name=${o}]`);t&&(t.checked=!!c?.c[o])}y(!0),e?.querySelector(".keksi-close")?.focus()}i.addEventListener("click",e=>{let o=e.target.closest?.("[data-keksi],[data-keksi-open]");if(!o)return;if(o.hasAttribute("data-keksi-open"))return e.preventDefault(),j();let t=o.getAttribute("data-keksi");if(t==="close")y();else if(t==="accept")p({analytics:!0,marketing:!0});else if(t==="reject")p($);else if(t==="save"){let a=r=>!!g("keksi-settings")?.querySelector(`[name=${r}]`)?.checked;p({analytics:a("analytics"),marketing:a("marketing")})}}),i.addEventListener("keydown",e=>{e.key==="Escape"&&g("keksi-settings")?.hidden===!1&&y()}),i.addEventListener("astro:after-swap",b);class N extends s.HTMLElement{connectedCallback(){this.show(!!c?.yt)}show(o,t=!1){let a=encodeURIComponent(this.getAttribute("video")??""),r=this.getAttribute("label");if(o){if(this.querySelector("iframe"))return;let d=i.createElement("iframe");d.src=`https://www.youtube-nocookie.com/embed/${a}${t?"?autoplay=1":""}`,d.title=r??"YouTube",d.allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",d.allowFullscreen=!0,d.referrerPolicy="strict-origin-when-cross-origin",this.replaceChildren(d),t&&d.focus();return}let l=i.createElement("div");l.className="keksi-yt";let u=this.getAttribute("poster");u&&l.style.setProperty("--keksi-poster",`url(${JSON.stringify(u)})`);let v=i.createElement("p");v.textContent=n.t.ytText;let m=i.createElement("button");m.type="button",m.textContent=n.t.ytButton,r&&m.setAttribute("aria-label",`${n.t.ytButton}: ${r}`),m.onclick=()=>{this.show(!0,!0),R()};let _=i.createElement("p"),F=(d,P,B=!1)=>{let k=i.createElement("a");k.href=d,k.textContent=P,B&&(k.target="_blank",k.rel="noopener"),_.append(_.childNodes.length?" \\xB7 ":"",k)};F(`https://www.youtube.com/watch?v=${a}`,n.t.ytOpen,!0),n.policy&&F(n.policy,n.t.more),l.append(v,m,_),this.replaceChildren(l)}}n.yt&&!s.customElements.get("keksi-youtube")&&s.customElements.define("keksi-youtube",N);let q={get consent(){return c?.done?{...c.c}:null},open:j,save:e=>p({...c?.c??$,...e}),acceptAll:()=>p({analytics:!0,marketing:!0}),rejectAll:()=>p($),sync:b};return s.keksi=q,b(),c&&C(c.c),q}I(JSON.parse(document.getElementById("keksi-data").textContent));})();'}</script>`;
340
348
  }
341
349
  function cookieTable(config, { lang = "fi", headingLevel = 2 } = {}) {
342
350
  return `<div class="keksi-policy">${sections(config, lang, headingLevel, false)}</div>`;
@@ -5,7 +5,7 @@ export interface TrackingOptions {
5
5
  allow?: string[];
6
6
  }
7
7
  /**
8
- * Records requests to Google, Meta and YouTube from now on. `check()` throws when any were made or when the page
8
+ * Records requests to the built-in and configured providers from now on. `check()` throws when any were made or when the page
9
9
  * stored anything besides the consent cookie and `allow`.
10
10
  */
11
11
  export declare function watchTracking(page: Page, config: KeksiConfig, { allow }?: TrackingOptions): {
@@ -227,7 +227,11 @@ var posthogHost = (config) => (config.posthog?.host ?? "https://eu.i.posthog.com
227
227
  function activeProviders(config) {
228
228
  return providers.filter(
229
229
  (p) => p.id === "youtube" ? config.youtube : p.id === "posthog" ? config.posthog?.key : list(config[p.id]).length
230
- ).map((p) => p.id === "posthog" && /\/\/us\./.test(posthogHost(config)) ? { ...p, transfer: "us" } : p);
230
+ ).map((p) => p.id === "posthog" && /\/\/us\./.test(posthogHost(config)) ? { ...p, transfer: "us" } : p).concat((config.providers ?? []).map(custom));
231
+ }
232
+ function custom({ purpose, hosts = [], script: _, ...p }) {
233
+ const text = (l) => typeof purpose === "string" ? purpose : purpose[l] ?? Object.values(purpose).find(Boolean) ?? "";
234
+ return { ...p, id: `custom:${p.name}`, purpose: { fi: text("fi"), sv: text("sv"), en: text("en") }, hosts };
231
235
  }
232
236
  function needsBanner(config) {
233
237
  return activeProviders(config).some((p) => p.id !== "youtube");
@@ -256,6 +260,10 @@ function runtimeData(config, locale) {
256
260
  ads: list(config.googleAds),
257
261
  meta: list(config.meta),
258
262
  ph: config.posthog?.key ? { key: config.posthog.key, host: posthogHost(config) } : void 0,
263
+ scripts: {
264
+ analytics: (config.providers ?? []).filter((p) => p.script && p.category === "analytics").map((p) => p.script),
265
+ marketing: (config.providers ?? []).filter((p) => p.script && p.category === "marketing").map((p) => p.script)
266
+ },
259
267
  yt: !!config.youtube,
260
268
  banner: needsBanner(config),
261
269
  policy: policyOf(config, locale),
@@ -265,8 +273,8 @@ function runtimeData(config, locale) {
265
273
  }
266
274
 
267
275
  // src/playwright.ts
268
- var trackerHosts = [...new Set(providers.flatMap((p) => p.hosts))];
269
276
  function watchTracking(page, config, { allow = [] } = {}) {
277
+ const trackerHosts = [...providers, ...config.providers ?? []].flatMap((p) => p.hosts ?? []);
270
278
  const requests = [];
271
279
  page.on("request", (r) => {
272
280
  const host = new URL(r.url()).hostname;
@@ -1,2 +1,4 @@
1
- import type { Provider } from './types.js';
2
- export declare const providers: Provider[];
1
+ import type { Provider, ProviderId } from './types.js';
2
+ export declare const providers: (Provider & {
3
+ id: ProviderId;
4
+ })[];
package/dist/sveltekit.js CHANGED
@@ -227,7 +227,11 @@ var posthogHost = (config) => (config.posthog?.host ?? "https://eu.i.posthog.com
227
227
  function activeProviders(config) {
228
228
  return providers.filter(
229
229
  (p) => p.id === "youtube" ? config.youtube : p.id === "posthog" ? config.posthog?.key : list(config[p.id]).length
230
- ).map((p) => p.id === "posthog" && /\/\/us\./.test(posthogHost(config)) ? { ...p, transfer: "us" } : p);
230
+ ).map((p) => p.id === "posthog" && /\/\/us\./.test(posthogHost(config)) ? { ...p, transfer: "us" } : p).concat((config.providers ?? []).map(custom));
231
+ }
232
+ function custom({ purpose, hosts = [], script: _, ...p }) {
233
+ const text = (l) => typeof purpose === "string" ? purpose : purpose[l] ?? Object.values(purpose).find(Boolean) ?? "";
234
+ return { ...p, id: `custom:${p.name}`, purpose: { fi: text("fi"), sv: text("sv"), en: text("en") }, hosts };
231
235
  }
232
236
  function categoriesOf(config) {
233
237
  return ["analytics", "marketing"].filter((c) => activeProviders(config).some((p) => p.category === c));
@@ -259,6 +263,10 @@ function runtimeData(config, locale) {
259
263
  ads: list(config.googleAds),
260
264
  meta: list(config.meta),
261
265
  ph: config.posthog?.key ? { key: config.posthog.key, host: posthogHost(config) } : void 0,
266
+ scripts: {
267
+ analytics: (config.providers ?? []).filter((p) => p.script && p.category === "analytics").map((p) => p.script),
268
+ marketing: (config.providers ?? []).filter((p) => p.script && p.category === "marketing").map((p) => p.script)
269
+ },
262
270
  yt: !!config.youtube,
263
271
  banner: needsBanner(config),
264
272
  policy: policyOf(config, locale),
@@ -333,7 +341,7 @@ function renderKeksi(config, { lang = "fi", nonce } = {}) {
333
341
  const btn = (action, label, type = "button") => `<button type="${type}" class="keksi-btn" data-keksi="${action}">${e(label)}</button>`;
334
342
  const uses = cats.map((c) => `<li><strong>${e(t[c])}:</strong> ${e(active.filter((p) => p.category === c).map((p) => p.name).join(", "))}</li>`).join("");
335
343
  const json = JSON.stringify(runtimeData(config, lang)).replace(/</g, "\\u003c");
336
- return `<style${n}>${css}</style><section id="keksi-banner" class="keksi" aria-labelledby="keksi-title" hidden><div class="keksi-main"><div class="keksi-head"><h2 id="keksi-title">${e(t.title)}</h2></div><div class="keksi-body"><p>${e(t.intro)}</p><ul>${uses}</ul><p>${e(t.change)}${policy ? ` <a href="${e(policy)}">${e(t.more)}</a>` : ""}</p></div><div class="keksi-actions"><button type="button" class="keksi-text" data-keksi-open>${e(t.settings)}</button>${btn("reject", t.reject)}${btn("accept", t.accept)}</div></div><form id="keksi-settings" class="keksi-settings" aria-labelledby="keksi-settings-title" hidden><div class="keksi-head"><h2 id="keksi-settings-title">${e(t.settingsTitle)}</h2><button type="button" class="keksi-close" data-keksi="close" aria-label="${e(t.close)}">\xD7</button></div><div class="keksi-body">${sections(config, lang, 3, true)}</div><div class="keksi-actions">${btn("save", t.save)}${btn("reject", t.reject)}${btn("accept", t.accept)}</div></form></section><script type="application/json" id="keksi-data">${json}</script><script${n}>${'"use strict";(()=>{var F=(n,s)=>new RegExp(`^${n.split("*").map(i=>i.replace(/[.+?^${}()|[\\]\\\\]/g,"\\\\$&")).join(".*")}$`).test(s);var _=["analytics","marketing"],v={analytics:!1,marketing:!1};function f(...n){return arguments}function H(n,s=window){if(typeof s.keksi?.sync=="function")return s.keksi.sync(),s.keksi;let i=s.document,g=e=>i.getElementById(e),c=I(),A=!1,w=!1,L=!1,E=new Set,x=f("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied"});if(n.ga.length||n.ads.length){let e=s.dataLayer??(s.dataLayer=[]);s.gtag=function(){e.push(arguments)},e.push(x)}if(n.meta.length){let e=function(){e.callMethod?e.callMethod.apply(e,[...arguments]):e.queue.push(arguments)};Object.assign(e,{push:e,loaded:!0,version:"2.0",queue:[]}),s.fbq=s._fbq=e}if(n.ph){let e=Object.assign([],{_i:[],people:[]});for(let o of"capture identify alias reset register register_once unregister group setPersonProperties getFeatureFlag isFeatureEnabled onFeatureFlags reloadFeatureFlags get_distinct_id startSessionRecording stopSessionRecording captureException on".split(" "))e[o]=(...t)=>e.push([o,...t]);s.posthog=e}function I(){let e=i.cookie.split("; ").find(l=>l.startsWith(`${n.name}=`))?.slice(n.name.length+1),[o,,t,a]=e?.split(".")??[];if(o!==n.v||!t||!a)return null;let r={analytics:a.includes("a"),marketing:a.includes("m")};return{id:t,c:r,yt:r.marketing||a.includes("y"),done:!a.includes("u")}}function T(e,o=e.marketing,t=!0){let a=c?.id??Array.from(s.crypto.getRandomValues(new Uint8Array(8)),l=>l.toString(16).padStart(2,"0")).join(""),r=(t?"":"u")+(e.analytics?"a":"")+(e.marketing?"m":"")+(o&&!e.marketing?"y":"")||"-";return i.cookie=`${n.name}=${n.v}.${Date.now().toString(36)}.${a}.${r}; Path=/; Max-Age=${n.days*86400}; SameSite=Lax`+(n.domain?`; Domain=${n.domain}`:"")+(s.location.protocol==="https:"?"; Secure":""),{id:a,c:e,yt:o,done:t}}function $(e){let o=i.createElement("script");return o.async=!0,o.src=e,i.head.append(o),o}function C(e){let o=[...e.analytics?n.ga:[],...e.marketing?n.ads:[]].filter(t=>!E.has(t));if(o.length){let t=e.marketing?"granted":"denied",a=[f("consent","update",{analytics_storage:e.analytics?"granted":"denied",ad_storage:t,ad_user_data:t,ad_personalization:t})];E.size||a.push(f("js",new Date));for(let r of o)a.push(f("config",r)),E.add(r);if(A)for(let r of a)s.dataLayer.push(r);else{let r=s.dataLayer;r.splice(r.indexOf(x)+1,0,...a),$(`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(o[0])}`),A=!0}}if(e.marketing&&n.meta.length&&!w&&(w=!0,s.fbq.queue.unshift(...n.meta.map(t=>f("init",t)),f("track","PageView")),$("https://connect.facebook.net/en_US/fbevents.js")),e.analytics&&n.ph&&!L&&(L=!0,s.posthog._i.push([n.ph.key,{api_host:n.ph.host,defaults:"2026-08-30"},"posthog"]),$(`${n.ph.host.replace(".i.posthog.com","-assets.i.posthog.com")}/static/array.js`).crossOrigin="anonymous"),c?.yt)for(let t of i.querySelectorAll("keksi-youtube"))t.show(!0)}function O(){c?.yt||(c=T(c?.c??v,!0,c?.done??!1)),C(c.c)}function R(e){let o=e.flatMap(l=>n.del[l]),t=l=>o.some(u=>F(u,l)),a=s.location.hostname.split("."),r=["",...a.map((l,u)=>`; Domain=${a.slice(u).join(".")}`)];for(let l of i.cookie.split("; ")){let u=l.split("=")[0];if(t(u))for(let b of r)i.cookie=`${u}=; Max-Age=0; Path=/${b}`}try{for(let l of[s.localStorage,s.sessionStorage])for(let u of Object.keys(l))t(u)&&l.removeItem(u)}catch{}}function p(e){let o=c;c=T(e),h(),i.dispatchEvent(new s.CustomEvent("keksi:change",{detail:{...e}})),R(_.filter(t=>!e[t])),_.some(t=>o?.c[t]&&!e[t])||o?.yt&&!c.yt?s.location.reload():C(e)}function y(e=!1,o=n.banner&&!c?.done||e){let t=g("keksi-banner"),a=g("keksi-settings");!t||!a||(t.hidden=!o,a.hidden=!e,t.firstElementChild.hidden=e)}function h(){y()}function M(){let e=g("keksi-settings");for(let o of _){let t=e?.querySelector(`[name=${o}]`);t&&(t.checked=!!c?.c[o])}y(!0),e?.querySelector(".keksi-close")?.focus()}i.addEventListener("click",e=>{let o=e.target.closest?.("[data-keksi],[data-keksi-open]");if(!o)return;if(o.hasAttribute("data-keksi-open"))return e.preventDefault(),M();let t=o.getAttribute("data-keksi");if(t==="close")y();else if(t==="accept")p({analytics:!0,marketing:!0});else if(t==="reject")p(v);else if(t==="save"){let a=r=>!!g("keksi-settings")?.querySelector(`[name=${r}]`)?.checked;p({analytics:a("analytics"),marketing:a("marketing")})}}),i.addEventListener("keydown",e=>{e.key==="Escape"&&g("keksi-settings")?.hidden===!1&&y()}),i.addEventListener("astro:after-swap",h);class D extends s.HTMLElement{connectedCallback(){this.show(!!c?.yt)}show(o,t=!1){let a=encodeURIComponent(this.getAttribute("video")??""),r=this.getAttribute("label");if(o){if(this.querySelector("iframe"))return;let d=i.createElement("iframe");d.src=`https://www.youtube-nocookie.com/embed/${a}${t?"?autoplay=1":""}`,d.title=r??"YouTube",d.allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",d.allowFullscreen=!0,d.referrerPolicy="strict-origin-when-cross-origin",this.replaceChildren(d),t&&d.focus();return}let l=i.createElement("div");l.className="keksi-yt";let u=this.getAttribute("poster");u&&l.style.setProperty("--keksi-poster",`url(${JSON.stringify(u)})`);let b=i.createElement("p");b.textContent=n.t.ytText;let m=i.createElement("button");m.type="button",m.textContent=n.t.ytButton,r&&m.setAttribute("aria-label",`${n.t.ytButton}: ${r}`),m.onclick=()=>{this.show(!0,!0),O()};let S=i.createElement("p"),q=(d,N,P=!1)=>{let k=i.createElement("a");k.href=d,k.textContent=N,P&&(k.target="_blank",k.rel="noopener"),S.append(S.childNodes.length?" \\xB7 ":"",k)};q(`https://www.youtube.com/watch?v=${a}`,n.t.ytOpen,!0),n.policy&&q(n.policy,n.t.more),l.append(b,m,S),this.replaceChildren(l)}}n.yt&&!s.customElements.get("keksi-youtube")&&s.customElements.define("keksi-youtube",D);let j={get consent(){return c?.done?{...c.c}:null},open:M,save:e=>p({...c?.c??v,...e}),acceptAll:()=>p({analytics:!0,marketing:!0}),rejectAll:()=>p(v),sync:h};return s.keksi=j,h(),c&&C(c.c),j}H(JSON.parse(document.getElementById("keksi-data").textContent));})();'}</script>`;
344
+ return `<style${n}>${css}</style><section id="keksi-banner" class="keksi" aria-labelledby="keksi-title" hidden><div class="keksi-main"><div class="keksi-head"><h2 id="keksi-title">${e(t.title)}</h2></div><div class="keksi-body"><p>${e(t.intro)}</p><ul>${uses}</ul><p>${e(t.change)}${policy ? ` <a href="${e(policy)}">${e(t.more)}</a>` : ""}</p></div><div class="keksi-actions"><button type="button" class="keksi-text" data-keksi-open>${e(t.settings)}</button>${btn("reject", t.reject)}${btn("accept", t.accept)}</div></div><form id="keksi-settings" class="keksi-settings" aria-labelledby="keksi-settings-title" hidden><div class="keksi-head"><h2 id="keksi-settings-title">${e(t.settingsTitle)}</h2><button type="button" class="keksi-close" data-keksi="close" aria-label="${e(t.close)}">\xD7</button></div><div class="keksi-body">${sections(config, lang, 3, true)}</div><div class="keksi-actions">${btn("save", t.save)}${btn("reject", t.reject)}${btn("accept", t.accept)}</div></form></section><script type="application/json" id="keksi-data">${json}</script><script${n}>${'"use strict";(()=>{var H=(n,s)=>new RegExp(`^${n.split("*").map(i=>i.replace(/[.+?^${}()|[\\]\\\\]/g,"\\\\$&")).join(".*")}$`).test(s);var E=["analytics","marketing"],$={analytics:!1,marketing:!1};function f(...n){return arguments}function I(n,s=window){if(typeof s.keksi?.sync=="function")return s.keksi.sync(),s.keksi;let i=s.document,g=e=>i.getElementById(e),c=O(),A=!1,w=!1,L=!1,S=new Set,x=new Set,T=f("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied"});if(n.ga.length||n.ads.length){let e=s.dataLayer??(s.dataLayer=[]);s.gtag=function(){e.push(arguments)},e.push(T)}if(n.meta.length){let e=function(){e.callMethod?e.callMethod.apply(e,[...arguments]):e.queue.push(arguments)};Object.assign(e,{push:e,loaded:!0,version:"2.0",queue:[]}),s.fbq=s._fbq=e}if(n.ph){let e=Object.assign([],{_i:[],people:[]});for(let o of"capture identify alias reset register register_once unregister group setPersonProperties getFeatureFlag isFeatureEnabled onFeatureFlags reloadFeatureFlags get_distinct_id startSessionRecording stopSessionRecording captureException on".split(" "))e[o]=(...t)=>e.push([o,...t]);s.posthog=e}function O(){let e=i.cookie.split("; ").find(l=>l.startsWith(`${n.name}=`))?.slice(n.name.length+1),[o,,t,a]=e?.split(".")??[];if(o!==n.v||!t||!a)return null;let r={analytics:a.includes("a"),marketing:a.includes("m")};return{id:t,c:r,yt:r.marketing||a.includes("y"),done:!a.includes("u")}}function M(e,o=e.marketing,t=!0){let a=c?.id??Array.from(s.crypto.getRandomValues(new Uint8Array(8)),l=>l.toString(16).padStart(2,"0")).join(""),r=(t?"":"u")+(e.analytics?"a":"")+(e.marketing?"m":"")+(o&&!e.marketing?"y":"")||"-";return i.cookie=`${n.name}=${n.v}.${Date.now().toString(36)}.${a}.${r}; Path=/; Max-Age=${n.days*86400}; SameSite=Lax`+(n.domain?`; Domain=${n.domain}`:"")+(s.location.protocol==="https:"?"; Secure":""),{id:a,c:e,yt:o,done:t}}function h(e){let o=i.createElement("script");return o.async=!0,o.src=e,i.head.append(o),o}function C(e){let o=[...e.analytics?n.ga:[],...e.marketing?n.ads:[]].filter(t=>!S.has(t));if(o.length){let t=e.marketing?"granted":"denied",a=[f("consent","update",{analytics_storage:e.analytics?"granted":"denied",ad_storage:t,ad_user_data:t,ad_personalization:t})];S.size||a.push(f("js",new Date));for(let r of o)a.push(f("config",r)),S.add(r);if(A)for(let r of a)s.dataLayer.push(r);else{let r=s.dataLayer;r.splice(r.indexOf(T)+1,0,...a),h(`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(o[0])}`),A=!0}}e.marketing&&n.meta.length&&!w&&(w=!0,s.fbq.queue.unshift(...n.meta.map(t=>f("init",t)),f("track","PageView")),h("https://connect.facebook.net/en_US/fbevents.js")),e.analytics&&n.ph&&!L&&(L=!0,s.posthog._i.push([n.ph.key,{api_host:n.ph.host,defaults:"2026-08-30"},"posthog"]),h(`${n.ph.host.replace(".i.posthog.com","-assets.i.posthog.com")}/static/array.js`).crossOrigin="anonymous");for(let t of E.flatMap(a=>e[a]?n.scripts[a]:[]))x.has(t)||(x.add(t),h(t));if(c?.yt)for(let t of i.querySelectorAll("keksi-youtube"))t.show(!0)}function R(){c?.yt||(c=M(c?.c??$,!0,c?.done??!1)),C(c.c)}function D(e){let o=e.flatMap(l=>n.del[l]),t=l=>o.some(u=>H(u,l)),a=s.location.hostname.split("."),r=["",...a.map((l,u)=>`; Domain=${a.slice(u).join(".")}`)];for(let l of i.cookie.split("; ")){let u=l.split("=")[0];if(t(u))for(let v of r)i.cookie=`${u}=; Max-Age=0; Path=/${v}`}try{for(let l of[s.localStorage,s.sessionStorage])for(let u of Object.keys(l))t(u)&&l.removeItem(u)}catch{}}function p(e){let o=c;c=M(e),b(),i.dispatchEvent(new s.CustomEvent("keksi:change",{detail:{...e}})),D(E.filter(t=>!e[t])),E.some(t=>o?.c[t]&&!e[t])||o?.yt&&!c.yt?s.location.reload():C(e)}function y(e=!1,o=n.banner&&!c?.done||e){let t=g("keksi-banner"),a=g("keksi-settings");!t||!a||(t.hidden=!o,a.hidden=!e,t.firstElementChild.hidden=e)}function b(){y()}function j(){let e=g("keksi-settings");for(let o of E){let t=e?.querySelector(`[name=${o}]`);t&&(t.checked=!!c?.c[o])}y(!0),e?.querySelector(".keksi-close")?.focus()}i.addEventListener("click",e=>{let o=e.target.closest?.("[data-keksi],[data-keksi-open]");if(!o)return;if(o.hasAttribute("data-keksi-open"))return e.preventDefault(),j();let t=o.getAttribute("data-keksi");if(t==="close")y();else if(t==="accept")p({analytics:!0,marketing:!0});else if(t==="reject")p($);else if(t==="save"){let a=r=>!!g("keksi-settings")?.querySelector(`[name=${r}]`)?.checked;p({analytics:a("analytics"),marketing:a("marketing")})}}),i.addEventListener("keydown",e=>{e.key==="Escape"&&g("keksi-settings")?.hidden===!1&&y()}),i.addEventListener("astro:after-swap",b);class N extends s.HTMLElement{connectedCallback(){this.show(!!c?.yt)}show(o,t=!1){let a=encodeURIComponent(this.getAttribute("video")??""),r=this.getAttribute("label");if(o){if(this.querySelector("iframe"))return;let d=i.createElement("iframe");d.src=`https://www.youtube-nocookie.com/embed/${a}${t?"?autoplay=1":""}`,d.title=r??"YouTube",d.allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",d.allowFullscreen=!0,d.referrerPolicy="strict-origin-when-cross-origin",this.replaceChildren(d),t&&d.focus();return}let l=i.createElement("div");l.className="keksi-yt";let u=this.getAttribute("poster");u&&l.style.setProperty("--keksi-poster",`url(${JSON.stringify(u)})`);let v=i.createElement("p");v.textContent=n.t.ytText;let m=i.createElement("button");m.type="button",m.textContent=n.t.ytButton,r&&m.setAttribute("aria-label",`${n.t.ytButton}: ${r}`),m.onclick=()=>{this.show(!0,!0),R()};let _=i.createElement("p"),F=(d,P,B=!1)=>{let k=i.createElement("a");k.href=d,k.textContent=P,B&&(k.target="_blank",k.rel="noopener"),_.append(_.childNodes.length?" \\xB7 ":"",k)};F(`https://www.youtube.com/watch?v=${a}`,n.t.ytOpen,!0),n.policy&&F(n.policy,n.t.more),l.append(v,m,_),this.replaceChildren(l)}}n.yt&&!s.customElements.get("keksi-youtube")&&s.customElements.define("keksi-youtube",N);let q={get consent(){return c?.done?{...c.c}:null},open:j,save:e=>p({...c?.c??$,...e}),acceptAll:()=>p({analytics:!0,marketing:!0}),rejectAll:()=>p($),sync:b};return s.keksi=q,b(),c&&C(c.c),q}I(JSON.parse(document.getElementById("keksi-data").textContent));})();'}</script>`;
337
345
  }
338
346
  function sections(config, lang, h, dialog) {
339
347
  const t = textsFor(config, lang);
package/dist/types.d.ts CHANGED
@@ -25,6 +25,8 @@ export interface KeksiConfig {
25
25
  domain?: string;
26
26
  days?: number;
27
27
  };
28
+ /** Providers keksi doesn't have built in, e.g. one client's chat widget. */
29
+ providers?: CustomProvider[];
28
30
  /** Cookies your site or framework sets without consent, usually from `keksi scan`. Listed under "Necessary". */
29
31
  necessary?: NecessaryCookie[];
30
32
  texts?: Partial<Record<Locale, Partial<Texts>>>;
@@ -38,6 +40,20 @@ export interface NecessaryCookie {
38
40
  /** `localStorage` or `sessionStorage`; omitted for cookies. */
39
41
  storage?: string;
40
42
  }
43
+ export interface CustomProvider {
44
+ name: string;
45
+ company: string;
46
+ privacyPolicy: string;
47
+ category: Category;
48
+ /** One text for all languages, or per language. */
49
+ purpose: string | Partial<Record<Locale, string>>;
50
+ cookies: CookieInfo[];
51
+ /** Hosts it contacts, for the Playwright check. */
52
+ hosts?: string[];
53
+ transfer?: 'dpf' | 'us';
54
+ /** Loaded after consent in `category`. Leave out to load it yourself on `keksi:change`. */
55
+ script?: string;
56
+ }
41
57
  /** `session`, `persistent`, or a number with unit: `2y`, `13mo`, `90d`, `15min`. */
42
58
  export type Duration = 'session' | 'persistent' | `${number}${'y' | 'mo' | 'd' | 'min'}`;
43
59
  export interface CookieInfo {
@@ -48,7 +64,8 @@ export interface CookieInfo {
48
64
  storage?: 'localStorage' | 'sessionStorage';
49
65
  }
50
66
  export interface Provider {
51
- id: ProviderId;
67
+ /** Custom providers get `custom:` and their name. */
68
+ id: ProviderId | `custom:${string}`;
52
69
  name: string;
53
70
  category: Category;
54
71
  company: string;
@@ -73,6 +90,8 @@ export interface RuntimeData {
73
90
  key: string;
74
91
  host: string;
75
92
  };
93
+ /** Custom provider scripts per category. */
94
+ scripts: Record<Category, string[]>;
76
95
  yt: boolean;
77
96
  /** False with YouTube only: the settings exist but the banner doesn't show by itself. */
78
97
  banner: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keksi",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Lean cookie consent for Google and Meta trackers",
5
5
  "license": "MIT",
6
6
  "type": "module",