@zerocost/sdk 0.18.0 → 0.19.0

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.
@@ -0,0 +1,63 @@
1
+ import { getBaseUrl } from './config';
2
+ export class ZerocostClient {
3
+ config;
4
+ baseUrl;
5
+ isInitialized = false;
6
+ constructor(config) {
7
+ if (!config.appId) {
8
+ throw new Error('ZerocostSDK: appId is required');
9
+ }
10
+ if (!config.apiKey) {
11
+ throw new Error('ZerocostSDK: apiKey is required');
12
+ }
13
+ this.config = {
14
+ environment: 'production',
15
+ debug: false,
16
+ ...config,
17
+ };
18
+ this.baseUrl = getBaseUrl(config.baseUrl);
19
+ }
20
+ init() {
21
+ if (this.isInitialized) {
22
+ this.log('ZerocostSDK is already initialized.');
23
+ return;
24
+ }
25
+ this.isInitialized = true;
26
+ this.log(`ZerocostSDK initialized for ${this.config.appId} in ${this.config.environment} mode.`);
27
+ }
28
+ getConfig() {
29
+ return this.config;
30
+ }
31
+ async request(path, body) {
32
+ const url = `${this.baseUrl}${path}`;
33
+ const payload = {
34
+ ...(body || {}),
35
+ app_id: this.config.appId,
36
+ };
37
+ this.log(`→ ${path}`, payload);
38
+ const res = await fetch(url, {
39
+ method: 'POST',
40
+ headers: {
41
+ 'Content-Type': 'application/json',
42
+ 'x-api-key': this.config.apiKey,
43
+ },
44
+ body: JSON.stringify(payload),
45
+ });
46
+ const data = await res.json();
47
+ if (!res.ok) {
48
+ this.log(`✗ ${res.status}`, data);
49
+ throw new Error(data.error || `Request failed with status ${res.status}`);
50
+ }
51
+ this.log(`✓ ${res.status}`, data);
52
+ return data;
53
+ }
54
+ log(message, data) {
55
+ if (this.config.debug) {
56
+ // Sanitize message to hide internal URLs/infra
57
+ const sanitizedMessage = typeof message === 'string'
58
+ ? message.replace(/https:\/\/[a-z0-9.-]+\.supabase\.co/gi, '[INFRA]')
59
+ : message;
60
+ console.log(`[Zerocost] ${sanitizedMessage}`, data ?? '');
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,5 @@
1
+ export const EDGE_FUNCTION_BASE = 'https://mwbgzpbuoojqsuxduieo.supabase.co/functions/v1';
2
+ export { ZEROCOST_BASE_URL } from './constants';
3
+ export function getBaseUrl(custom) {
4
+ return custom || EDGE_FUNCTION_BASE;
5
+ }
@@ -0,0 +1,490 @@
1
+ /**
2
+ * consent-ui.ts — Pure DOM consent popup for the Zerocost SDK.
3
+ *
4
+ * - Desktop: centered card modal (max 480px)
5
+ * - Mobile (≤640px): bottom-sheet modal
6
+ * - Themes: light / dark / auto
7
+ * - Non-dismissable (no Escape, no backdrop click)
8
+ * - Returns a Promise that resolves with the user's toggle selections
9
+ */
10
+ import { ZEROCOST_BASE_URL } from './config';
11
+ // ── CSS ──────────────────────────────────────────────────────────────
12
+ const STYLE_ID = 'zerocost-consent-styles';
13
+ export function injectStyles(theme) {
14
+ if (document.getElementById(STYLE_ID))
15
+ return;
16
+ const darkVars = `
17
+ --zc-bg: #111111;
18
+ --zc-surface: #1a1a1a;
19
+ --zc-border: #2a2a2a;
20
+ --zc-text: #ffffff;
21
+ --zc-text-secondary: #999999;
22
+ --zc-accent: #ffffff;
23
+ --zc-accent-bg: #ffffff;
24
+ --zc-accent-fg: #000000;
25
+ --zc-toggle-off-bg: #333333;
26
+ --zc-toggle-on-bg: #00e599;
27
+ --zc-toggle-knob: #ffffff;
28
+ --zc-backdrop: rgba(0,0,0,0.65);
29
+ --zc-link: #888888;
30
+ --zc-link-hover: #cccccc;
31
+ `;
32
+ const lightVars = `
33
+ --zc-bg: #ffffff;
34
+ --zc-surface: #f5f5f5;
35
+ --zc-border: #e0e0e0;
36
+ --zc-text: #111111;
37
+ --zc-text-secondary: #666666;
38
+ --zc-accent: #111111;
39
+ --zc-accent-bg: #111111;
40
+ --zc-accent-fg: #ffffff;
41
+ --zc-toggle-off-bg: #cccccc;
42
+ --zc-toggle-on-bg: #00c77d;
43
+ --zc-toggle-knob: #ffffff;
44
+ --zc-backdrop: rgba(0,0,0,0.45);
45
+ --zc-link: #666666;
46
+ --zc-link-hover: #111111;
47
+ `;
48
+ let themeRule;
49
+ if (theme === 'dark') {
50
+ themeRule = `.zc-consent-root, .zc-settings-btn { ${darkVars} }`;
51
+ }
52
+ else if (theme === 'light') {
53
+ themeRule = `.zc-consent-root, .zc-settings-btn { ${lightVars} }`;
54
+ }
55
+ else {
56
+ themeRule = `
57
+ .zc-consent-root, .zc-settings-btn { ${lightVars} }
58
+ @media (prefers-color-scheme: dark) {
59
+ .zc-consent-root, .zc-settings-btn { ${darkVars} }
60
+ }
61
+ `;
62
+ }
63
+ const css = `
64
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
65
+ ${themeRule}
66
+
67
+ .zc-consent-root * {
68
+ box-sizing: border-box;
69
+ margin: 0;
70
+ padding: 0;
71
+ font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, sans-serif;
72
+ }
73
+
74
+ .zc-consent-backdrop {
75
+ position: fixed;
76
+ inset: 0;
77
+ z-index: 999999;
78
+ background: var(--zc-backdrop);
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ animation: zc-fade-in 200ms ease;
83
+ }
84
+
85
+ @keyframes zc-fade-in {
86
+ from { opacity: 0; }
87
+ to { opacity: 1; }
88
+ }
89
+
90
+ @keyframes zc-slide-up {
91
+ from { transform: translateY(100%); }
92
+ to { transform: translateY(0); }
93
+ }
94
+
95
+ .zc-consent-card {
96
+ background: var(--zc-bg);
97
+ border: 1px solid var(--zc-border);
98
+ border-radius: 16px;
99
+ width: 100%;
100
+ max-width: 440px;
101
+ max-height: 90vh;
102
+ overflow-y: auto;
103
+ padding: 24px 20px 20px;
104
+ animation: zc-fade-in 200ms ease;
105
+ }
106
+
107
+ /* Mobile: bottom-sheet style */
108
+ @media (max-width: 640px) {
109
+ .zc-consent-backdrop {
110
+ align-items: flex-end;
111
+ }
112
+ .zc-consent-card {
113
+ border-radius: 20px 20px 0 0;
114
+ max-width: 100%;
115
+ animation: zc-slide-up 200ms ease;
116
+ }
117
+ }
118
+
119
+ /* Scrollbar */
120
+ .zc-consent-card::-webkit-scrollbar { width: 4px; }
121
+ .zc-consent-card::-webkit-scrollbar-thumb { background: var(--zc-border); border-radius: 4px; }
122
+
123
+ .zc-consent-header {
124
+ display: flex;
125
+ align-items: center;
126
+ gap: 10px;
127
+ margin-bottom: 4px;
128
+ }
129
+
130
+ .zc-consent-logo {
131
+ width: 28px;
132
+ height: 28px;
133
+ border-radius: 6px;
134
+ background: var(--zc-accent-bg);
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: center;
138
+ flex-shrink: 0;
139
+ }
140
+
141
+ .zc-consent-logo svg {
142
+ width: 16px;
143
+ height: 16px;
144
+ }
145
+
146
+ .zc-consent-title {
147
+ font-size: 16px;
148
+ font-weight: 700;
149
+ color: var(--zc-text);
150
+ letter-spacing: -0.02em;
151
+ }
152
+
153
+ .zc-consent-subtitle {
154
+ font-size: 13px;
155
+ color: var(--zc-text-secondary);
156
+ line-height: 1.5;
157
+ margin-bottom: 16px;
158
+ }
159
+
160
+ .zc-consent-toggles {
161
+ display: flex;
162
+ flex-direction: column;
163
+ gap: 10px;
164
+ margin-bottom: 16px;
165
+ }
166
+
167
+ .zc-consent-toggle-card {
168
+ background: var(--zc-surface);
169
+ border: 1px solid var(--zc-border);
170
+ border-radius: 12px;
171
+ padding: 12px 14px;
172
+ }
173
+
174
+ .zc-consent-toggle-row {
175
+ display: flex;
176
+ align-items: center;
177
+ justify-content: space-between;
178
+ margin-bottom: 6px;
179
+ }
180
+
181
+ .zc-consent-toggle-label {
182
+ font-size: 14px;
183
+ font-weight: 600;
184
+ color: var(--zc-text);
185
+ }
186
+
187
+ .zc-consent-toggle-desc {
188
+ font-size: 12px;
189
+ color: var(--zc-text-secondary);
190
+ line-height: 1.5;
191
+ margin-bottom: 4px;
192
+ }
193
+
194
+ .zc-consent-learn-more {
195
+ font-size: 11px;
196
+ color: var(--zc-link);
197
+ text-decoration: none;
198
+ cursor: pointer;
199
+ transition: color 150ms;
200
+ }
201
+
202
+ .zc-consent-learn-more:hover {
203
+ color: var(--zc-link-hover);
204
+ }
205
+
206
+ /* Toggle switch */
207
+ .zc-toggle {
208
+ position: relative;
209
+ width: 40px;
210
+ height: 22px;
211
+ flex-shrink: 0;
212
+ cursor: pointer;
213
+ }
214
+
215
+ .zc-toggle input {
216
+ opacity: 0;
217
+ width: 0;
218
+ height: 0;
219
+ position: absolute;
220
+ }
221
+
222
+ .zc-toggle-track {
223
+ position: absolute;
224
+ inset: 0;
225
+ background: var(--zc-toggle-off-bg);
226
+ border-radius: 11px;
227
+ transition: background 200ms ease;
228
+ }
229
+
230
+ .zc-toggle input:checked + .zc-toggle-track {
231
+ background: var(--zc-toggle-on-bg);
232
+ }
233
+
234
+ .zc-toggle-knob {
235
+ position: absolute;
236
+ top: 2px;
237
+ left: 2px;
238
+ width: 18px;
239
+ height: 18px;
240
+ background: var(--zc-toggle-knob);
241
+ border-radius: 50%;
242
+ transition: transform 200ms ease;
243
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
244
+ }
245
+
246
+ .zc-toggle input:checked ~ .zc-toggle-knob {
247
+ transform: translateX(18px);
248
+ }
249
+
250
+ /* Footer */
251
+ .zc-consent-footer {
252
+ display: flex;
253
+ flex-wrap: wrap;
254
+ gap: 4px 12px;
255
+ justify-content: center;
256
+ margin-bottom: 14px;
257
+ }
258
+
259
+ .zc-consent-footer a {
260
+ font-size: 11px;
261
+ color: var(--zc-link);
262
+ text-decoration: none;
263
+ transition: color 150ms;
264
+ }
265
+
266
+ .zc-consent-footer a:hover {
267
+ color: var(--zc-link-hover);
268
+ }
269
+
270
+ .zc-consent-footer-sep {
271
+ font-size: 11px;
272
+ color: var(--zc-link);
273
+ opacity: 0.5;
274
+ }
275
+
276
+ /* Confirm button */
277
+ .zc-consent-confirm {
278
+ display: block;
279
+ width: 100%;
280
+ padding: 12px;
281
+ font-size: 14px;
282
+ font-weight: 600;
283
+ border: none;
284
+ border-radius: 10px;
285
+ cursor: pointer;
286
+ background: var(--zc-accent-bg);
287
+ color: var(--zc-accent-fg);
288
+ letter-spacing: -0.01em;
289
+ transition: opacity 150ms;
290
+ }
291
+
292
+ .zc-consent-confirm:hover {
293
+ opacity: 0.88;
294
+ }
295
+
296
+ .zc-consent-confirm:active {
297
+ opacity: 0.75;
298
+ }
299
+
300
+ /* Floating settings button */
301
+ .zc-settings-btn {
302
+ position: fixed;
303
+ width: 44px;
304
+ height: 44px;
305
+ border-radius: 50%;
306
+ background: var(--zc-bg);
307
+ border: 1px solid var(--zc-border);
308
+ color: var(--zc-text);
309
+ display: flex;
310
+ align-items: center;
311
+ justify-content: center;
312
+ cursor: pointer;
313
+ z-index: 999998;
314
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
315
+ transition: transform 200ms, border-color 200ms;
316
+ }
317
+
318
+ .zc-settings-btn:hover {
319
+ transform: scale(1.05);
320
+ border-color: var(--zc-text-secondary);
321
+ }
322
+
323
+ .zc-settings-btn svg {
324
+ width: 20px;
325
+ height: 20px;
326
+ }
327
+
328
+ .zc-settings-bottom-left { bottom: 20px; left: 20px; }
329
+ .zc-settings-bottom-right { bottom: 20px; right: 20px; }
330
+ .zc-settings-top-left { top: 20px; left: 20px; }
331
+ .zc-settings-top-right { top: 20px; right: 20px; }
332
+
333
+ @media (max-width: 640px) {
334
+ .zc-settings-btn { width: 40px; height: 40px; }
335
+ .zc-settings-bottom-left, .zc-settings-bottom-right { bottom: 16px; }
336
+ }
337
+ `;
338
+ const style = document.createElement('style');
339
+ style.id = STYLE_ID;
340
+ style.textContent = css;
341
+ document.head.appendChild(style);
342
+ }
343
+ // ── Toggle helper ────────────────────────────────────────────────────
344
+ function createToggle(id, checked) {
345
+ const label = document.createElement('label');
346
+ label.className = 'zc-toggle';
347
+ const input = document.createElement('input');
348
+ input.type = 'checkbox';
349
+ input.checked = checked;
350
+ input.id = id;
351
+ const track = document.createElement('span');
352
+ track.className = 'zc-toggle-track';
353
+ const knob = document.createElement('span');
354
+ knob.className = 'zc-toggle-knob';
355
+ label.appendChild(input);
356
+ label.appendChild(track);
357
+ label.appendChild(knob);
358
+ return label;
359
+ }
360
+ // ── Toggle card helper ───────────────────────────────────────────────
361
+ function createToggleCard(toggleId, title, description, learnMoreUrl, defaultOn) {
362
+ const card = document.createElement('div');
363
+ card.className = 'zc-consent-toggle-card';
364
+ // Row: label + toggle
365
+ const row = document.createElement('div');
366
+ row.className = 'zc-consent-toggle-row';
367
+ const labelSpan = document.createElement('span');
368
+ labelSpan.className = 'zc-consent-toggle-label';
369
+ labelSpan.textContent = title;
370
+ const toggle = createToggle(toggleId, defaultOn);
371
+ row.appendChild(labelSpan);
372
+ row.appendChild(toggle);
373
+ card.appendChild(row);
374
+ // Description
375
+ const desc = document.createElement('div');
376
+ desc.className = 'zc-consent-toggle-desc';
377
+ desc.textContent = description;
378
+ card.appendChild(desc);
379
+ // Learn more
380
+ const link = document.createElement('a');
381
+ link.className = 'zc-consent-learn-more';
382
+ link.href = learnMoreUrl;
383
+ link.target = '_blank';
384
+ link.rel = 'noopener noreferrer';
385
+ link.textContent = 'Learn more ↗';
386
+ card.appendChild(link);
387
+ return card;
388
+ }
389
+ // ── Public: show consent popup ───────────────────────────────────────
390
+ export function showConsentUI(options) {
391
+ return new Promise((resolve) => {
392
+ const { appName, theme, privacyPolicyUrl } = options;
393
+ const defaults = options.defaults ?? { ads: true, usageData: false, aiInteractions: false };
394
+ injectStyles(theme);
395
+ // Root wrapper
396
+ const root = document.createElement('div');
397
+ root.className = 'zc-consent-root';
398
+ // Backdrop (non-dismissable)
399
+ const backdrop = document.createElement('div');
400
+ backdrop.className = 'zc-consent-backdrop';
401
+ // Block Escape key
402
+ const blockEscape = (e) => {
403
+ if (e.key === 'Escape') {
404
+ e.preventDefault();
405
+ e.stopPropagation();
406
+ }
407
+ };
408
+ document.addEventListener('keydown', blockEscape, true);
409
+ // Card
410
+ const card = document.createElement('div');
411
+ card.className = 'zc-consent-card';
412
+ // Header
413
+ const header = document.createElement('div');
414
+ header.className = 'zc-consent-header';
415
+ const logo = document.createElement('div');
416
+ logo.className = 'zc-consent-logo';
417
+ logo.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="color:var(--zc-accent-fg)"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>`;
418
+ const title = document.createElement('div');
419
+ title.className = 'zc-consent-title';
420
+ title.textContent = `${appName || 'This app'} uses Zerocost`;
421
+ header.appendChild(logo);
422
+ header.appendChild(title);
423
+ card.appendChild(header);
424
+ // Subtitle
425
+ const subtitle = document.createElement('div');
426
+ subtitle.className = 'zc-consent-subtitle';
427
+ subtitle.textContent = 'Manage your preferences below. You can update these anytime.';
428
+ card.appendChild(subtitle);
429
+ // Toggles
430
+ const toggles = document.createElement('div');
431
+ toggles.className = 'zc-consent-toggles';
432
+ // Base URL for Zerocost documentation/legal links
433
+ const zerocostBaseUrl = ZEROCOST_BASE_URL;
434
+ toggles.appendChild(createToggleCard('zc-toggle-ads', 'Ads', 'Contextual, non-intrusive ads. No cookies or browsing history used.', `${zerocostBaseUrl}/docs/ads`, defaults.ads));
435
+ toggles.appendChild(createToggleCard('zc-toggle-usage', 'Usage data', 'Anonymized usage patterns. No personal information is shared.', `${zerocostBaseUrl}/docs/usage-data`, defaults.usageData));
436
+ toggles.appendChild(createToggleCard('zc-toggle-ai', 'AI interactions', 'Anonymized conversation data used for AI research.', `${zerocostBaseUrl}/docs/ai-interactions`, defaults.aiInteractions));
437
+ card.appendChild(toggles);
438
+ // Footer links
439
+ const footer = document.createElement('div');
440
+ footer.className = 'zc-consent-footer';
441
+ const ppLink = document.createElement('a');
442
+ ppLink.href = privacyPolicyUrl || `${zerocostBaseUrl}/privacy`;
443
+ ppLink.target = '_blank';
444
+ ppLink.rel = 'noopener noreferrer';
445
+ ppLink.textContent = 'Privacy Policy';
446
+ footer.appendChild(ppLink);
447
+ const sep1 = document.createElement('span');
448
+ sep1.className = 'zc-consent-footer-sep';
449
+ sep1.textContent = '·';
450
+ footer.appendChild(sep1);
451
+ const termsLink = document.createElement('a');
452
+ termsLink.href = `${zerocostBaseUrl}/terms`;
453
+ termsLink.target = '_blank';
454
+ termsLink.rel = 'noopener noreferrer';
455
+ termsLink.textContent = 'Terms';
456
+ footer.appendChild(termsLink);
457
+ const sep2 = document.createElement('span');
458
+ sep2.className = 'zc-consent-footer-sep';
459
+ sep2.textContent = '·';
460
+ footer.appendChild(sep2);
461
+ const dnsLink = document.createElement('a');
462
+ dnsLink.href = `${zerocostBaseUrl}/docs/do-not-sell`;
463
+ dnsLink.target = '_blank';
464
+ dnsLink.rel = 'noopener noreferrer';
465
+ dnsLink.textContent = 'Do Not Sell My Data';
466
+ footer.appendChild(dnsLink);
467
+ card.appendChild(footer);
468
+ // Confirm button
469
+ const confirmBtn = document.createElement('button');
470
+ confirmBtn.className = 'zc-consent-confirm';
471
+ confirmBtn.textContent = 'Confirm';
472
+ confirmBtn.addEventListener('click', () => {
473
+ const ads = document.getElementById('zc-toggle-ads')?.checked ?? defaults.ads;
474
+ const usageData = document.getElementById('zc-toggle-usage')?.checked ?? defaults.usageData;
475
+ const aiInteractions = document.getElementById('zc-toggle-ai')?.checked ?? defaults.aiInteractions;
476
+ // Cleanup
477
+ document.removeEventListener('keydown', blockEscape, true);
478
+ root.remove();
479
+ resolve({ ads, usageData, aiInteractions });
480
+ });
481
+ card.appendChild(confirmBtn);
482
+ backdrop.appendChild(card);
483
+ root.appendChild(backdrop);
484
+ document.body.appendChild(root);
485
+ });
486
+ }
487
+ /** Remove any existing consent popup from the DOM */
488
+ export function removeConsentUI() {
489
+ document.querySelector('.zc-consent-root')?.remove();
490
+ }