keksi 0.1.5 → 0.1.7
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 +42 -1
- package/dist/index.js +10 -2
- package/dist/playwright.d.ts +1 -1
- package/dist/playwright.js +10 -2
- package/dist/providers.d.ts +4 -2
- package/dist/sveltekit.js +10 -2
- package/dist/types.d.ts +20 -1
- package/package.json +13 -2
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
|
|
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
|
|
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>`;
|
package/dist/playwright.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface TrackingOptions {
|
|
|
5
5
|
allow?: string[];
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
* Records requests to
|
|
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): {
|
package/dist/playwright.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 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;
|
package/dist/providers.d.ts
CHANGED
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
|
|
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
|
-
|
|
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,7 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keksi",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"description": "Superlight cookie consent for modern web frameworks",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cookie consent",
|
|
7
|
+
"cookie banner",
|
|
8
|
+
"gdpr",
|
|
9
|
+
"astro",
|
|
10
|
+
"sveltekit",
|
|
11
|
+
"google analytics",
|
|
12
|
+
"meta pixel",
|
|
13
|
+
"posthog",
|
|
14
|
+
"youtube"
|
|
15
|
+
],
|
|
5
16
|
"license": "MIT",
|
|
6
17
|
"type": "module",
|
|
7
18
|
"sideEffects": false,
|