keksi 0.0.0-stage → 0.1.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.
package/README.md CHANGED
@@ -1,3 +1,167 @@
1
- # Temporary Holding Version
1
+ # keksi
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Lean cookie consent for Astro, SvelteKit and other server-rendered sites. Built around Finnish and EU law, with
4
+ texts in English, Finnish and Swedish.
5
+
6
+ keksi loads the trackers itself, and only after consent. You don't add their script tags: you put their ids in the
7
+ config, so there's nothing to block.
8
+
9
+ - Google Analytics 4, Google Ads, Meta Pixel and YouTube. The short list is deliberate.
10
+ - About 5 KB gzipped, inline. No extra requests, no dependencies.
11
+ - Rendered on the server. Works on static pages, with no layout shift.
12
+ - The cookie policy table is generated from the same data.
13
+
14
+ ## Setup
15
+
16
+ ```sh
17
+ pnpm add keksi
18
+ ```
19
+
20
+ ```ts
21
+ // keksi.config.ts
22
+ import { defineConfig } from 'keksi';
23
+
24
+ export default defineConfig({
25
+ ga: 'G-XXXXXXX',
26
+ googleAds: 'AW-123456789',
27
+ meta: '1234567890',
28
+ youtube: true,
29
+ policyUrl: '/evasteet',
30
+ });
31
+ ```
32
+
33
+ **Astro:** add the component right after `<body>` in your layout.
34
+
35
+ ```astro
36
+ ---
37
+ import { Keksi } from 'keksi/astro';
38
+ import config from '../../keksi.config';
39
+ ---
40
+ <body>
41
+ <Keksi config={config} />
42
+ ...
43
+ </body>
44
+ ```
45
+
46
+ **SvelteKit:** add the hook. It renders keksi outside the Svelte root and keeps it out of the client bundle.
47
+
48
+ ```ts
49
+ // src/hooks.server.ts
50
+ import { keksiHandle } from 'keksi/sveltekit';
51
+ import config from '../keksi.config';
52
+
53
+ export const handle = keksiHandle(config);
54
+ ```
55
+
56
+ Any element with `data-keksi-open` reopens the settings. Put one in the footer:
57
+
58
+ ```html
59
+ <button type="button" data-keksi-open>Evästeasetukset</button>
60
+ ```
61
+
62
+ **Necessary cookies** from your framework or own code, such as a session, are listed under "Necessary". Find them
63
+ with the scanner, which visits the site without consent:
64
+
65
+ ```sh
66
+ npx keksi scan https://staging.example.fi
67
+ ```
68
+
69
+ It writes `keksi.cookies.json`, with purposes for common cookies filled in. Write the rest, then import the file:
70
+
71
+ ```ts
72
+ import necessary from './keksi.cookies.json' with { type: 'json' };
73
+
74
+ export default defineConfig({ ga: 'G-XXXXXXX', necessary });
75
+ ```
76
+
77
+ Rerunning keeps your purposes. `keksi scan <url> --check` fails when there are new entries, for CI. The scanner only
78
+ lists; it never blocks anything. Cookies set after login aren't visited, so add those by hand.
79
+
80
+ **Cookie policy page:** `<CookieTable config={config} />` from `keksi/astro`, or `cookieTable(config)` for an HTML
81
+ string.
82
+
83
+ ## Tracking events
84
+
85
+ `gtag()` and `fbq()` are always defined, so you can call them anytime:
86
+
87
+ ```ts
88
+ gtag('event', 'generate_lead');
89
+ fbq('track', 'Lead');
90
+ ```
91
+
92
+ - Calls made before consent are queued. They're sent only if the visitor consents on that page.
93
+ - Google gets Consent Mode v2 signals (basic mode).
94
+ - GA4 and Meta Pixel track single-page navigation themselves.
95
+
96
+ ## YouTube
97
+
98
+ ```html
99
+ <keksi-youtube video="aqz-KE-bpKQ" label="Esittelyvideo" poster="/img/poster.jpg"></keksi-youtube>
100
+ ```
101
+
102
+ - Shows a placeholder until the visitor clicks it, then loads that one video from `youtube-nocookie.com`.
103
+ - With marketing consent, videos load directly.
104
+ - `poster` is optional and should be self-hosted. Thumbnails from YouTube would contact Google before consent.
105
+
106
+ ## API
107
+
108
+ ```ts
109
+ window.keksi.consent; // { analytics, marketing } or null
110
+ window.keksi.open();
111
+ window.keksi.save({ marketing: true });
112
+ window.keksi.acceptAll();
113
+ window.keksi.rejectAll();
114
+ document.addEventListener('keksi:change', (e) => e.detail);
115
+ ```
116
+
117
+ ## Testing
118
+
119
+ ```ts
120
+ import { consentCookie, watchTracking } from 'keksi/playwright';
121
+
122
+ test('nothing tracks before consent', async ({ page }) => {
123
+ const tracking = watchTracking(page, config, { allow: ['session'] });
124
+ await page.goto('/');
125
+ await page.waitForLoadState('networkidle');
126
+ await tracking.check(); // throws on tracker requests or stored data
127
+ });
128
+
129
+ // Skip the banner in other tests
130
+ await context.addCookies([consentCookie(config, baseURL, { analytics: true })]);
131
+ ```
132
+
133
+ ## Customizing
134
+
135
+ - **Styles** have zero specificity, so your CSS wins. Variables: `--keksi-bg`, `--keksi-fg`, `--keksi-line`,
136
+ `--keksi-radius`, `--keksi-focus`.
137
+ - **Language:** follows the page's language, otherwise Finnish. Pass `lang` to set it.
138
+ - **Texts:** `texts: { fi: { intro: '…' } }`.
139
+ - **CSP:** pass `nonce`.
140
+ - **Other frameworks:** `renderKeksi(config, { lang })` returns the HTML to put after `<body>`.
141
+ - **Consent cookie:** `cookie: { name, domain, days }`. Kept for 365 days by default.
142
+
143
+ ## Legal basis
144
+
145
+ | Requirement | Basis | keksi |
146
+ |---|---|---|
147
+ | No non-essential storage before consent | Act 917/2014 §205 | Trackers load only after consent |
148
+ | Purpose, duration and third parties disclosed | §205, CJEU *Planet49* | Providers on the banner, cookies and durations in settings and on the policy page |
149
+ | Separate choices, rejecting as easy as accepting | GDPR Art. 4(11), 7; Traficom 2024 | A switch per category, "Reject all" on the banner |
150
+ | Withdrawal is easy and has real effect | GDPR Art. 7(3) | Deletes the category's cookies and localStorage on your domain, then reloads |
151
+ | Consent can be demonstrated | GDPR Art. 7(1) | Consent cookie with id, time and version. Adding or removing a provider asks again |
152
+ | Transfers outside the EU disclosed | GDPR Art. 13(1)(f) | DPF transfer stated per provider |
153
+
154
+ Cookies that Google and Meta set on their own domains can't be deleted from your site. The policy page lists them as
155
+ third-party cookies.
156
+
157
+ ## Development
158
+
159
+ ```sh
160
+ pnpm install
161
+ pnpm build # prints the runtime size
162
+ pnpm test
163
+ pnpm test:e2e # Astro and SvelteKit examples
164
+ ```
165
+
166
+ To add a provider: add its data to `src/providers.ts`, a config key with the same id to `KeksiConfig`, and its
167
+ loading to `load()` in `src/runtime.ts`.
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { cookieTable, type KeksiConfig, type Locale } from 'keksi';
3
+
4
+ interface Props {
5
+ config: KeksiConfig;
6
+ lang?: Locale;
7
+ headingLevel?: 2 | 3 | 4;
8
+ }
9
+
10
+ const { config, headingLevel } = Astro.props;
11
+ const locale = Astro.currentLocale?.slice(0, 2);
12
+ const lang = Astro.props.lang ?? (['fi', 'sv', 'en'].includes(locale!) ? (locale as Locale) : 'fi');
13
+ ---
14
+
15
+ <Fragment set:html={cookieTable(config, { lang, headingLevel })} />
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { renderKeksi, type KeksiConfig, type Locale } from 'keksi';
3
+
4
+ interface Props {
5
+ config: KeksiConfig;
6
+ /** Defaults to Astro's current locale, then `fi`. */
7
+ lang?: Locale;
8
+ nonce?: string;
9
+ }
10
+
11
+ const { config, nonce } = Astro.props;
12
+ const locale = Astro.currentLocale?.slice(0, 2);
13
+ const lang = Astro.props.lang ?? (['fi', 'sv', 'en'].includes(locale!) ? (locale as Locale) : 'fi');
14
+ ---
15
+
16
+ <Fragment set:html={renderKeksi(config, { lang, nonce })} />
@@ -0,0 +1,2 @@
1
+ export { default as Keksi } from './Keksi.astro';
2
+ export { default as CookieTable } from './CookieTable.astro';
package/astro/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { default as Keksi } from './Keksi.astro';
2
+ export { default as CookieTable } from './CookieTable.astro';
package/dist/cli.js ADDED
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { parseArgs } from "node:util";
6
+
7
+ // src/providers.ts
8
+ var GOOGLE = { company: "Google Ireland Limited", privacyPolicy: "https://policies.google.com/privacy", transfer: "dpf" };
9
+ var META = { company: "Meta Platforms Ireland Limited", privacyPolicy: "https://www.facebook.com/privacy/policy/", transfer: "dpf" };
10
+ var providers = [
11
+ {
12
+ id: "ga",
13
+ name: "Google Analytics",
14
+ ...GOOGLE,
15
+ category: "analytics",
16
+ purpose: {
17
+ fi: "K\xE4vij\xE4tilastot: k\xE4vij\xE4m\xE4\xE4r\xE4t, liikenteen l\xE4hteet ja sivujen k\xE4ytt\xF6.",
18
+ sv: "Bes\xF6ksstatistik: antal bes\xF6kare, trafikk\xE4llor och hur sidorna anv\xE4nds.",
19
+ en: "Visitor statistics: visitor numbers, traffic sources and page usage."
20
+ },
21
+ cookies: [
22
+ { name: "_ga", duration: "2y" },
23
+ { name: "_ga_*", duration: "2y" }
24
+ ],
25
+ hosts: ["google-analytics.com", "analytics.google.com", "googletagmanager.com"]
26
+ },
27
+ {
28
+ id: "googleAds",
29
+ name: "Google Ads",
30
+ ...GOOGLE,
31
+ category: "marketing",
32
+ purpose: {
33
+ fi: "Mainonnan tehon mittaaminen (konversiot) ja mainosten kohdentaminen sivuston k\xE4vij\xF6ille.",
34
+ sv: "M\xE4tning av annonsernas effekt (konverteringar) och riktad annonsering till bes\xF6kare.",
35
+ en: "Measuring ad performance (conversions) and showing ads to site visitors."
36
+ },
37
+ cookies: [
38
+ { name: "_gcl_au", duration: "90d" },
39
+ { name: "_gcl_aw", duration: "90d" },
40
+ { name: "_gcl_dc", duration: "90d" },
41
+ { name: "_gcl_gs", duration: "90d" },
42
+ { name: "_gac_*", duration: "90d" },
43
+ { name: "_gcl_ls", duration: "persistent", storage: "localStorage" },
44
+ { name: "IDE", duration: "13mo", host: "doubleclick.net" },
45
+ { name: "DSID", duration: "14d", host: "doubleclick.net" },
46
+ { name: "test_cookie", duration: "15min", host: "doubleclick.net" }
47
+ ],
48
+ hosts: ["googletagmanager.com", "googleadservices.com", "doubleclick.net", "googlesyndication.com"]
49
+ },
50
+ {
51
+ id: "meta",
52
+ name: "Meta Pixel",
53
+ ...META,
54
+ category: "marketing",
55
+ purpose: {
56
+ fi: "Mainonnan tehon mittaaminen ja mainosten kohdentaminen Facebookissa ja Instagramissa.",
57
+ sv: "M\xE4tning av annonsernas effekt och riktad annonsering p\xE5 Facebook och Instagram.",
58
+ en: "Measuring ad performance and targeting ads on Facebook and Instagram."
59
+ },
60
+ cookies: [
61
+ { name: "_fbp", duration: "90d" },
62
+ { name: "_fbc", duration: "90d" },
63
+ { name: "fr", duration: "90d", host: "facebook.com" }
64
+ ],
65
+ hosts: ["facebook.net", "facebook.com"]
66
+ },
67
+ {
68
+ id: "youtube",
69
+ name: "YouTube",
70
+ ...GOOGLE,
71
+ category: "marketing",
72
+ purpose: {
73
+ fi: "Upotettujen videoiden toistaminen. YouTube voi k\xE4ytt\xE4\xE4 tietoja mainontaan.",
74
+ sv: "Uppspelning av inb\xE4ddade videor. YouTube kan anv\xE4nda uppgifterna f\xF6r annonsering.",
75
+ en: "Playing embedded videos. YouTube may use the data for advertising."
76
+ },
77
+ cookies: [
78
+ { name: "YSC", duration: "session", host: "youtube.com" },
79
+ { name: "VISITOR_INFO1_LIVE", duration: "6mo", host: "youtube.com" },
80
+ { name: "VISITOR_PRIVACY_METADATA", duration: "6mo", host: "youtube.com" },
81
+ { name: "__Secure-YNID", duration: "6mo", host: "youtube.com" },
82
+ { name: "__Secure-ROLLOUT_TOKEN", duration: "6mo", host: "youtube.com" },
83
+ { name: "PREF", duration: "8mo", host: "youtube.com" },
84
+ { name: "yt-remote-*, ytidb::*, yt-player-*", duration: "persistent", host: "youtube-nocookie.com", storage: "localStorage" }
85
+ ],
86
+ hosts: ["youtube.com", "youtube-nocookie.com", "ytimg.com", "googlevideo.com"]
87
+ }
88
+ ];
89
+
90
+ // src/scan.ts
91
+ var known = (purpose, fi, sv) => ({ en: purpose, fi, sv });
92
+ var SESSION = known("Keeps you signed in.", "Pit\xE4\xE4 sinut kirjautuneena.", "H\xE5ller dig inloggad.");
93
+ var CSRF = known("Protects forms against forgery.", "Suojaa lomakkeita v\xE4\xE4renn\xF6ksilt\xE4.", "Skyddar formul\xE4r mot f\xF6rfalskning.");
94
+ var SECURITY = known(
95
+ "Protects the site against bots and attacks.",
96
+ "Suojaa sivustoa boteilta ja hy\xF6kk\xE4yksilt\xE4.",
97
+ "Skyddar webbplatsen mot botar och attacker."
98
+ );
99
+ var purposes = {
100
+ NEXT_LOCALE: known("Remembers your language.", "Muistaa valitsemasi kielen.", "Kommer ih\xE5g ditt spr\xE5k."),
101
+ "next-auth.session-token": SESSION,
102
+ "__Secure-next-auth.session-token": SESSION,
103
+ "authjs.session-token": SESSION,
104
+ "__Secure-authjs.session-token": SESSION,
105
+ "next-auth.csrf-token": CSRF,
106
+ "__Host-next-auth.csrf-token": CSRF,
107
+ "authjs.csrf-token": CSRF,
108
+ "__Host-authjs.csrf-token": CSRF,
109
+ "next-auth.callback-url": known("Returns you to the right page after signing in.", "Palauttaa oikealle sivulle kirjautumisen j\xE4lkeen.", "\xC5terf\xF6r dig till r\xE4tt sida efter inloggning."),
110
+ __cf_bm: SECURITY,
111
+ cf_clearance: SECURITY,
112
+ _cfuvid: SECURITY,
113
+ PHPSESSID: SESSION,
114
+ "connect.sid": SESSION,
115
+ session: SESSION,
116
+ csrftoken: CSRF,
117
+ _csrf: CSRF,
118
+ "sveltekit:*": known("Restores scroll position and page state when you navigate back.", "Palauttaa vierityskohdan ja sivun tilan, kun palaat takaisin.", "\xC5terst\xE4ller scrollposition och sidans l\xE4ge n\xE4r du g\xE5r tillbaka.")
119
+ };
120
+ var trackerNames = providers.flatMap((p) => p.cookies.flatMap((c) => c.name.split(", ")));
121
+ var trackerHosts = providers.flatMap((p) => p.hosts);
122
+ var matches = (patterns, name) => patterns.some((p) => p.endsWith("*") ? name.startsWith(p.slice(0, -1)) : name === p);
123
+ function purposeOf(name) {
124
+ const k = Object.keys(purposes).find((p) => matches([p], name));
125
+ return k ? purposes[k] : void 0;
126
+ }
127
+ function durationOf(expires, now = Date.now() / 1e3) {
128
+ if (expires < 0) return "session";
129
+ const minutes = (expires - now) / 60;
130
+ const days = minutes / 1440;
131
+ if (days >= 365) return `${Math.round(days / 365)}y`;
132
+ if (days >= 30) return `${Math.round(days / 30.4)}mo`;
133
+ if (days >= 1) return `${Math.round(days)}d`;
134
+ return `${Math.max(1, Math.round(minutes))}min`;
135
+ }
136
+ async function scan(url2, { pages = 20, cookieName = "keksi" } = {}) {
137
+ const { chromium } = await import("@playwright/test");
138
+ const browser = await chromium.launch();
139
+ const origin = new URL(url2).origin;
140
+ const host = new URL(url2).hostname;
141
+ const found2 = /* @__PURE__ */ new Map();
142
+ try {
143
+ const context = await browser.newContext();
144
+ const page = await context.newPage();
145
+ const queue = [new URL(url2).href];
146
+ const seen = new Set(queue);
147
+ for (let i = 0; i < queue.length && i < pages; i++) {
148
+ await page.goto(queue[i], { waitUntil: "networkidle", timeout: 3e4 }).catch(() => {
149
+ });
150
+ const { links, local, session } = await page.evaluate(() => ({
151
+ links: [...document.querySelectorAll("a[href]")].map((a) => a.href),
152
+ local: Object.keys(localStorage),
153
+ session: Object.keys(sessionStorage)
154
+ }));
155
+ for (const key2 of local) found2.set(`l:${key2}`, { name: key2, duration: "persistent", storage: "localStorage" });
156
+ for (const key2 of session) found2.set(`s:${key2}`, { name: key2, duration: "session", storage: "sessionStorage" });
157
+ for (const link of links) {
158
+ const u = new URL(link);
159
+ u.hash = "";
160
+ if (u.origin === origin && !seen.has(u.href) && !/\.(pdf|zip|jpe?g|png|webp|svg|mp4)$/i.test(u.pathname)) {
161
+ seen.add(u.href);
162
+ queue.push(u.href);
163
+ }
164
+ }
165
+ }
166
+ for (const c of await context.cookies()) {
167
+ const domain = c.domain.replace(/^\./, "");
168
+ const own = host === domain || host.endsWith(`.${domain}`);
169
+ found2.set(`c:${c.name}`, { name: c.name, duration: durationOf(c.expires), ...own ? {} : { host: domain } });
170
+ }
171
+ } finally {
172
+ await browser.close();
173
+ }
174
+ return [...found2.values()].filter((c) => c.name !== cookieName && !matches(trackerNames, c.name)).filter((c) => !c.host || !trackerHosts.some((t) => c.host === t || c.host.endsWith(`.${t}`))).map((c) => ({ ...c, purpose: purposeOf(c.name) ?? { en: "", fi: "", sv: "" } }));
175
+ }
176
+ var key = (c) => `${c.storage ?? "cookie"}:${c.name}`;
177
+ function merge(existing2, found2) {
178
+ const old = new Map(existing2.map((c) => [key(c), c]));
179
+ const now = new Set(found2.map(key));
180
+ const added2 = found2.filter((c) => !old.has(key(c)));
181
+ const missing2 = existing2.filter((c) => !now.has(key(c)));
182
+ const list2 = [
183
+ ...existing2.map((c) => {
184
+ const f = found2.find((x) => key(x) === key(c));
185
+ return f ? { ...c, duration: f.duration } : c;
186
+ }),
187
+ ...added2
188
+ ];
189
+ return { list: list2, added: added2, missing: missing2 };
190
+ }
191
+
192
+ // src/cli.ts
193
+ var HELP = `Usage: keksi scan <url> [options]
194
+
195
+ Lists the cookies and storage your site sets without consent, for the "Necessary" section.
196
+
197
+ Options:
198
+ --out <file> Output file (default: keksi.cookies.json)
199
+ --pages <n> Most pages to visit (default: 20)
200
+ --cookie <name> Consent cookie name, if changed (default: keksi)
201
+ --check Write nothing; exit 1 if the file is missing entries
202
+ `;
203
+ var { values, positionals } = parseArgs({
204
+ allowPositionals: true,
205
+ options: {
206
+ out: { type: "string", default: "keksi.cookies.json" },
207
+ pages: { type: "string", default: "20" },
208
+ cookie: { type: "string", default: "keksi" },
209
+ check: { type: "boolean", default: false },
210
+ help: { type: "boolean", short: "h", default: false }
211
+ }
212
+ });
213
+ var [command, url] = positionals;
214
+ if (values.help || command !== "scan" || !url) {
215
+ process.stdout.write(HELP);
216
+ process.exit(values.help ? 0 : 1);
217
+ }
218
+ var existing = existsSync(values.out) ? JSON.parse(readFileSync(values.out, "utf8")) : [];
219
+ var found = await scan(url, { pages: Number(values.pages), cookieName: values.cookie });
220
+ var { list, added, missing } = merge(existing, found);
221
+ var label = (c) => `${c.name}${c.storage ? ` (${c.storage})` : ""}`;
222
+ var blank = (c) => !c.purpose || typeof c.purpose === "object" && !Object.values(c.purpose).some(Boolean);
223
+ for (const c of added) console.log(`+ ${label(c)}`);
224
+ for (const c of missing) console.log(`? ${label(c)}: not seen, remove it unless it's set after login or on unscanned pages`);
225
+ if (values.check) {
226
+ if (added.length) {
227
+ console.error(`${values.out} is missing ${added.length} entr${added.length === 1 ? "y" : "ies"}. Run keksi scan ${url}`);
228
+ process.exit(1);
229
+ }
230
+ console.log(`${values.out} is up to date.`);
231
+ } else {
232
+ writeFileSync(values.out, `${JSON.stringify(list, null, 2)}
233
+ `);
234
+ const todo = list.filter(blank);
235
+ console.log(`Wrote ${list.length} entr${list.length === 1 ? "y" : "ies"} to ${values.out}.`);
236
+ if (todo.length) console.log(`Add a purpose for: ${todo.map(label).join(", ")}`);
237
+ }
@@ -0,0 +1,9 @@
1
+ import { type Texts } from './i18n.js';
2
+ import type { Category, KeksiConfig, Locale, Provider, RuntimeData } from './types.js';
3
+ export declare function defineConfig(config: KeksiConfig): KeksiConfig;
4
+ export declare function activeProviders(config: KeksiConfig): Provider[];
5
+ export declare function categoriesOf(config: KeksiConfig): Category[];
6
+ export declare function textsFor(config: KeksiConfig, locale: Locale): Texts;
7
+ /** Changes when a provider is added or removed, which asks everyone again. Ids and texts can change freely. */
8
+ export declare function version(config: KeksiConfig): string;
9
+ export declare function runtimeData(config: KeksiConfig, locale: Locale): RuntimeData;
package/dist/i18n.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ import type { Locale } from './types.js';
2
+ declare const fi: {
3
+ title: string;
4
+ intro: string;
5
+ transfer: string;
6
+ more: string;
7
+ accept: string;
8
+ reject: string;
9
+ settings: string;
10
+ settingsTitle: string;
11
+ save: string;
12
+ close: string;
13
+ alwaysOn: string;
14
+ necessary: string;
15
+ necessaryInfo: string;
16
+ analytics: string;
17
+ analyticsInfo: string;
18
+ marketing: string;
19
+ marketingInfo: string;
20
+ providers: string;
21
+ thisSite: string;
22
+ consentPurpose: string;
23
+ privacyPolicy: string;
24
+ dpf: string;
25
+ name: string;
26
+ duration: string;
27
+ purpose: string;
28
+ setBy: string;
29
+ session: string;
30
+ persistent: string;
31
+ y: string;
32
+ mo: string;
33
+ d: string;
34
+ min: string;
35
+ ytText: string;
36
+ ytButton: string;
37
+ ytOpen: string;
38
+ };
39
+ export type Texts = typeof fi;
40
+ export declare const texts: Record<Locale, Texts>;
41
+ export {};
@@ -0,0 +1,6 @@
1
+ export { defineConfig, version } from './config.js';
2
+ export { cookieTable, renderKeksi, type CookieTableOptions, type RenderOptions } from './render.js';
3
+ export { providers } from './providers.js';
4
+ export type { KeksiApi } from './runtime.js';
5
+ export type { Texts } from './i18n.js';
6
+ export type * from './types.js';