@zerocost/sdk 0.12.0 → 0.13.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.
package/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ConsentManager: () => ConsentManager,
23
24
  LLMDataModule: () => LLMDataModule,
24
25
  RecordingModule: () => RecordingModule,
25
26
  ZerocostClient: () => ZerocostClient,
@@ -1303,6 +1304,607 @@ var RecordingModule = class {
1303
1304
  }
1304
1305
  };
1305
1306
 
1307
+ // src/core/consent-ui.ts
1308
+ var STYLE_ID = "zerocost-consent-styles";
1309
+ function injectStyles(theme) {
1310
+ if (document.getElementById(STYLE_ID)) return;
1311
+ const darkVars = `
1312
+ --zc-bg: #111111;
1313
+ --zc-surface: #1a1a1a;
1314
+ --zc-border: #2a2a2a;
1315
+ --zc-text: #ffffff;
1316
+ --zc-text-secondary: #999999;
1317
+ --zc-accent: #ffffff;
1318
+ --zc-accent-bg: #ffffff;
1319
+ --zc-accent-fg: #000000;
1320
+ --zc-toggle-off-bg: #333333;
1321
+ --zc-toggle-on-bg: #00e599;
1322
+ --zc-toggle-knob: #ffffff;
1323
+ --zc-backdrop: rgba(0,0,0,0.65);
1324
+ --zc-link: #888888;
1325
+ --zc-link-hover: #cccccc;
1326
+ `;
1327
+ const lightVars = `
1328
+ --zc-bg: #ffffff;
1329
+ --zc-surface: #f5f5f5;
1330
+ --zc-border: #e0e0e0;
1331
+ --zc-text: #111111;
1332
+ --zc-text-secondary: #666666;
1333
+ --zc-accent: #111111;
1334
+ --zc-accent-bg: #111111;
1335
+ --zc-accent-fg: #ffffff;
1336
+ --zc-toggle-off-bg: #cccccc;
1337
+ --zc-toggle-on-bg: #00c77d;
1338
+ --zc-toggle-knob: #ffffff;
1339
+ --zc-backdrop: rgba(0,0,0,0.45);
1340
+ --zc-link: #666666;
1341
+ --zc-link-hover: #111111;
1342
+ `;
1343
+ let themeRule;
1344
+ if (theme === "dark") {
1345
+ themeRule = `.zc-consent-root { ${darkVars} }`;
1346
+ } else if (theme === "light") {
1347
+ themeRule = `.zc-consent-root { ${lightVars} }`;
1348
+ } else {
1349
+ themeRule = `
1350
+ .zc-consent-root { ${lightVars} }
1351
+ @media (prefers-color-scheme: dark) {
1352
+ .zc-consent-root { ${darkVars} }
1353
+ }
1354
+ `;
1355
+ }
1356
+ const css = `
1357
+ ${themeRule}
1358
+
1359
+ .zc-consent-root * {
1360
+ box-sizing: border-box;
1361
+ margin: 0;
1362
+ padding: 0;
1363
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, sans-serif;
1364
+ }
1365
+
1366
+ .zc-consent-backdrop {
1367
+ position: fixed;
1368
+ inset: 0;
1369
+ z-index: 999999;
1370
+ background: var(--zc-backdrop);
1371
+ display: flex;
1372
+ align-items: center;
1373
+ justify-content: center;
1374
+ animation: zc-fade-in 200ms ease;
1375
+ }
1376
+
1377
+ @keyframes zc-fade-in {
1378
+ from { opacity: 0; }
1379
+ to { opacity: 1; }
1380
+ }
1381
+
1382
+ @keyframes zc-slide-up {
1383
+ from { transform: translateY(100%); }
1384
+ to { transform: translateY(0); }
1385
+ }
1386
+
1387
+ .zc-consent-card {
1388
+ background: var(--zc-bg);
1389
+ border: 1px solid var(--zc-border);
1390
+ border-radius: 16px;
1391
+ width: 100%;
1392
+ max-width: 460px;
1393
+ max-height: 90vh;
1394
+ overflow-y: auto;
1395
+ padding: 28px 24px 24px;
1396
+ animation: zc-fade-in 200ms ease;
1397
+ }
1398
+
1399
+ /* Mobile: bottom-sheet style */
1400
+ @media (max-width: 640px) {
1401
+ .zc-consent-backdrop {
1402
+ align-items: flex-end;
1403
+ }
1404
+ .zc-consent-card {
1405
+ border-radius: 20px 20px 0 0;
1406
+ max-width: 100%;
1407
+ animation: zc-slide-up 200ms ease;
1408
+ }
1409
+ }
1410
+
1411
+ /* Scrollbar */
1412
+ .zc-consent-card::-webkit-scrollbar { width: 4px; }
1413
+ .zc-consent-card::-webkit-scrollbar-thumb { background: var(--zc-border); border-radius: 4px; }
1414
+
1415
+ .zc-consent-header {
1416
+ display: flex;
1417
+ align-items: center;
1418
+ gap: 10px;
1419
+ margin-bottom: 6px;
1420
+ }
1421
+
1422
+ .zc-consent-logo {
1423
+ width: 28px;
1424
+ height: 28px;
1425
+ border-radius: 6px;
1426
+ background: var(--zc-accent-bg);
1427
+ display: flex;
1428
+ align-items: center;
1429
+ justify-content: center;
1430
+ flex-shrink: 0;
1431
+ }
1432
+
1433
+ .zc-consent-logo svg {
1434
+ width: 16px;
1435
+ height: 16px;
1436
+ }
1437
+
1438
+ .zc-consent-title {
1439
+ font-size: 16px;
1440
+ font-weight: 700;
1441
+ color: var(--zc-text);
1442
+ letter-spacing: -0.02em;
1443
+ }
1444
+
1445
+ .zc-consent-subtitle {
1446
+ font-size: 13px;
1447
+ color: var(--zc-text-secondary);
1448
+ line-height: 1.5;
1449
+ margin-bottom: 20px;
1450
+ }
1451
+
1452
+ .zc-consent-toggles {
1453
+ display: flex;
1454
+ flex-direction: column;
1455
+ gap: 10px;
1456
+ margin-bottom: 20px;
1457
+ }
1458
+
1459
+ .zc-consent-toggle-card {
1460
+ background: var(--zc-surface);
1461
+ border: 1px solid var(--zc-border);
1462
+ border-radius: 12px;
1463
+ padding: 14px 16px;
1464
+ }
1465
+
1466
+ .zc-consent-toggle-row {
1467
+ display: flex;
1468
+ align-items: center;
1469
+ justify-content: space-between;
1470
+ margin-bottom: 6px;
1471
+ }
1472
+
1473
+ .zc-consent-toggle-label {
1474
+ font-size: 14px;
1475
+ font-weight: 600;
1476
+ color: var(--zc-text);
1477
+ }
1478
+
1479
+ .zc-consent-toggle-desc {
1480
+ font-size: 12px;
1481
+ color: var(--zc-text-secondary);
1482
+ line-height: 1.5;
1483
+ margin-bottom: 4px;
1484
+ }
1485
+
1486
+ .zc-consent-learn-more {
1487
+ font-size: 11px;
1488
+ color: var(--zc-link);
1489
+ text-decoration: none;
1490
+ cursor: pointer;
1491
+ transition: color 150ms;
1492
+ }
1493
+
1494
+ .zc-consent-learn-more:hover {
1495
+ color: var(--zc-link-hover);
1496
+ }
1497
+
1498
+ /* Toggle switch */
1499
+ .zc-toggle {
1500
+ position: relative;
1501
+ width: 40px;
1502
+ height: 22px;
1503
+ flex-shrink: 0;
1504
+ cursor: pointer;
1505
+ }
1506
+
1507
+ .zc-toggle input {
1508
+ opacity: 0;
1509
+ width: 0;
1510
+ height: 0;
1511
+ position: absolute;
1512
+ }
1513
+
1514
+ .zc-toggle-track {
1515
+ position: absolute;
1516
+ inset: 0;
1517
+ background: var(--zc-toggle-off-bg);
1518
+ border-radius: 11px;
1519
+ transition: background 200ms ease;
1520
+ }
1521
+
1522
+ .zc-toggle input:checked + .zc-toggle-track {
1523
+ background: var(--zc-toggle-on-bg);
1524
+ }
1525
+
1526
+ .zc-toggle-knob {
1527
+ position: absolute;
1528
+ top: 2px;
1529
+ left: 2px;
1530
+ width: 18px;
1531
+ height: 18px;
1532
+ background: var(--zc-toggle-knob);
1533
+ border-radius: 50%;
1534
+ transition: transform 200ms ease;
1535
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
1536
+ }
1537
+
1538
+ .zc-toggle input:checked ~ .zc-toggle-knob {
1539
+ transform: translateX(18px);
1540
+ }
1541
+
1542
+ /* Footer */
1543
+ .zc-consent-footer {
1544
+ display: flex;
1545
+ flex-wrap: wrap;
1546
+ gap: 4px 12px;
1547
+ justify-content: center;
1548
+ margin-bottom: 16px;
1549
+ }
1550
+
1551
+ .zc-consent-footer a {
1552
+ font-size: 11px;
1553
+ color: var(--zc-link);
1554
+ text-decoration: none;
1555
+ transition: color 150ms;
1556
+ }
1557
+
1558
+ .zc-consent-footer a:hover {
1559
+ color: var(--zc-link-hover);
1560
+ }
1561
+
1562
+ .zc-consent-footer-sep {
1563
+ font-size: 11px;
1564
+ color: var(--zc-link);
1565
+ opacity: 0.5;
1566
+ }
1567
+
1568
+ /* Confirm button */
1569
+ .zc-consent-confirm {
1570
+ display: block;
1571
+ width: 100%;
1572
+ padding: 12px;
1573
+ font-size: 14px;
1574
+ font-weight: 600;
1575
+ border: none;
1576
+ border-radius: 10px;
1577
+ cursor: pointer;
1578
+ background: var(--zc-accent-bg);
1579
+ color: var(--zc-accent-fg);
1580
+ letter-spacing: -0.01em;
1581
+ transition: opacity 150ms;
1582
+ }
1583
+
1584
+ .zc-consent-confirm:hover {
1585
+ opacity: 0.88;
1586
+ }
1587
+
1588
+ .zc-consent-confirm:active {
1589
+ opacity: 0.75;
1590
+ }
1591
+ `;
1592
+ const style = document.createElement("style");
1593
+ style.id = STYLE_ID;
1594
+ style.textContent = css;
1595
+ document.head.appendChild(style);
1596
+ }
1597
+ function createToggle(id, checked) {
1598
+ const label = document.createElement("label");
1599
+ label.className = "zc-toggle";
1600
+ const input = document.createElement("input");
1601
+ input.type = "checkbox";
1602
+ input.checked = checked;
1603
+ input.id = id;
1604
+ const track = document.createElement("span");
1605
+ track.className = "zc-toggle-track";
1606
+ const knob = document.createElement("span");
1607
+ knob.className = "zc-toggle-knob";
1608
+ label.appendChild(input);
1609
+ label.appendChild(track);
1610
+ label.appendChild(knob);
1611
+ return label;
1612
+ }
1613
+ function createToggleCard(toggleId, title, description, learnMoreUrl, defaultOn) {
1614
+ const card = document.createElement("div");
1615
+ card.className = "zc-consent-toggle-card";
1616
+ const row = document.createElement("div");
1617
+ row.className = "zc-consent-toggle-row";
1618
+ const labelSpan = document.createElement("span");
1619
+ labelSpan.className = "zc-consent-toggle-label";
1620
+ labelSpan.textContent = title;
1621
+ const toggle = createToggle(toggleId, defaultOn);
1622
+ row.appendChild(labelSpan);
1623
+ row.appendChild(toggle);
1624
+ card.appendChild(row);
1625
+ const desc = document.createElement("div");
1626
+ desc.className = "zc-consent-toggle-desc";
1627
+ desc.textContent = description;
1628
+ card.appendChild(desc);
1629
+ const link = document.createElement("a");
1630
+ link.className = "zc-consent-learn-more";
1631
+ link.href = learnMoreUrl;
1632
+ link.target = "_blank";
1633
+ link.rel = "noopener noreferrer";
1634
+ link.textContent = "Learn more \u2197";
1635
+ card.appendChild(link);
1636
+ return card;
1637
+ }
1638
+ function showConsentUI(options) {
1639
+ return new Promise((resolve) => {
1640
+ const { appName, theme, privacyPolicyUrl } = options;
1641
+ const defaults = options.defaults ?? { ads: true, usageData: false, aiInteractions: false };
1642
+ injectStyles(theme);
1643
+ const root = document.createElement("div");
1644
+ root.className = "zc-consent-root";
1645
+ const backdrop = document.createElement("div");
1646
+ backdrop.className = "zc-consent-backdrop";
1647
+ const blockEscape = (e) => {
1648
+ if (e.key === "Escape") {
1649
+ e.preventDefault();
1650
+ e.stopPropagation();
1651
+ }
1652
+ };
1653
+ document.addEventListener("keydown", blockEscape, true);
1654
+ const card = document.createElement("div");
1655
+ card.className = "zc-consent-card";
1656
+ const header = document.createElement("div");
1657
+ header.className = "zc-consent-header";
1658
+ const logo = document.createElement("div");
1659
+ logo.className = "zc-consent-logo";
1660
+ 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>`;
1661
+ const title = document.createElement("div");
1662
+ title.className = "zc-consent-title";
1663
+ title.textContent = `${appName || "This app"} uses Zerocost`;
1664
+ header.appendChild(logo);
1665
+ header.appendChild(title);
1666
+ card.appendChild(header);
1667
+ const subtitle = document.createElement("div");
1668
+ subtitle.className = "zc-consent-subtitle";
1669
+ subtitle.textContent = "Manage your preferences below. You can update these anytime.";
1670
+ card.appendChild(subtitle);
1671
+ const toggles = document.createElement("div");
1672
+ toggles.className = "zc-consent-toggles";
1673
+ const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
1674
+ toggles.appendChild(createToggleCard(
1675
+ "zc-toggle-ads",
1676
+ "Ads",
1677
+ "Contextual, non-intrusive ads. No cookies or browsing history used.",
1678
+ `${baseUrl}/consent/ads`,
1679
+ defaults.ads
1680
+ ));
1681
+ toggles.appendChild(createToggleCard(
1682
+ "zc-toggle-usage",
1683
+ "Usage data",
1684
+ "Anonymized usage patterns. No personal information is shared.",
1685
+ `${baseUrl}/consent/usage-data`,
1686
+ defaults.usageData
1687
+ ));
1688
+ toggles.appendChild(createToggleCard(
1689
+ "zc-toggle-ai",
1690
+ "AI interactions",
1691
+ "Anonymized conversation data used for AI research.",
1692
+ `${baseUrl}/consent/ai-interactions`,
1693
+ defaults.aiInteractions
1694
+ ));
1695
+ card.appendChild(toggles);
1696
+ const footer = document.createElement("div");
1697
+ footer.className = "zc-consent-footer";
1698
+ const ppLink = document.createElement("a");
1699
+ ppLink.href = privacyPolicyUrl || `${baseUrl}/privacy`;
1700
+ ppLink.target = "_blank";
1701
+ ppLink.rel = "noopener noreferrer";
1702
+ ppLink.textContent = "Privacy Policy";
1703
+ footer.appendChild(ppLink);
1704
+ const sep1 = document.createElement("span");
1705
+ sep1.className = "zc-consent-footer-sep";
1706
+ sep1.textContent = "\xB7";
1707
+ footer.appendChild(sep1);
1708
+ const termsLink = document.createElement("a");
1709
+ termsLink.href = `${baseUrl}/terms`;
1710
+ termsLink.target = "_blank";
1711
+ termsLink.rel = "noopener noreferrer";
1712
+ termsLink.textContent = "Terms";
1713
+ footer.appendChild(termsLink);
1714
+ const sep2 = document.createElement("span");
1715
+ sep2.className = "zc-consent-footer-sep";
1716
+ sep2.textContent = "\xB7";
1717
+ footer.appendChild(sep2);
1718
+ const dnsLink = document.createElement("a");
1719
+ dnsLink.href = `${baseUrl}/do-not-sell`;
1720
+ dnsLink.target = "_blank";
1721
+ dnsLink.rel = "noopener noreferrer";
1722
+ dnsLink.textContent = "Do Not Sell My Data";
1723
+ footer.appendChild(dnsLink);
1724
+ card.appendChild(footer);
1725
+ const confirmBtn = document.createElement("button");
1726
+ confirmBtn.className = "zc-consent-confirm";
1727
+ confirmBtn.textContent = "Confirm";
1728
+ confirmBtn.addEventListener("click", () => {
1729
+ const ads = document.getElementById("zc-toggle-ads")?.checked ?? defaults.ads;
1730
+ const usageData = document.getElementById("zc-toggle-usage")?.checked ?? defaults.usageData;
1731
+ const aiInteractions = document.getElementById("zc-toggle-ai")?.checked ?? defaults.aiInteractions;
1732
+ document.removeEventListener("keydown", blockEscape, true);
1733
+ root.remove();
1734
+ resolve({ ads, usageData, aiInteractions });
1735
+ });
1736
+ card.appendChild(confirmBtn);
1737
+ backdrop.appendChild(card);
1738
+ root.appendChild(backdrop);
1739
+ document.body.appendChild(root);
1740
+ });
1741
+ }
1742
+ function removeConsentUI() {
1743
+ document.querySelector(".zc-consent-root")?.remove();
1744
+ }
1745
+
1746
+ // src/core/consent.ts
1747
+ var CONSENT_VERSION = "1.1";
1748
+ var CONSENT_STORAGE_PREFIX = "zerocost-consent:";
1749
+ var TWELVE_MONTHS_MS = 365 * 24 * 60 * 60 * 1e3;
1750
+ var ConsentManager = class {
1751
+ record = null;
1752
+ needsReset = false;
1753
+ client;
1754
+ consentConfig;
1755
+ appName;
1756
+ theme;
1757
+ constructor(client, opts) {
1758
+ this.client = client;
1759
+ this.consentConfig = opts.consent ?? {};
1760
+ this.appName = opts.appName ?? "";
1761
+ this.theme = opts.theme ?? "dark";
1762
+ this.hydrateFromStorage();
1763
+ }
1764
+ // ── Public API (per spec §6.3) ───────────────────────────────────
1765
+ /** Returns the current consent record, or null if none exists. */
1766
+ get() {
1767
+ return this.record;
1768
+ }
1769
+ /** Programmatically open the consent popup (e.g. from app settings). */
1770
+ async open() {
1771
+ removeConsentUI();
1772
+ await this.promptAndWait();
1773
+ }
1774
+ /** Clear consent — prompt will re-fire on next init(). */
1775
+ reset() {
1776
+ this.record = null;
1777
+ this.needsReset = true;
1778
+ this.clearStorage();
1779
+ this.client.log("Consent reset. Prompt will re-fire on next init().");
1780
+ }
1781
+ /** Restore a previously saved record (skip re-prompting if valid). */
1782
+ restore(record) {
1783
+ if (this.isValid(record)) {
1784
+ this.record = record;
1785
+ this.writeStorage(record);
1786
+ this.client.log("Consent restored from saved record.");
1787
+ } else {
1788
+ this.client.log("Restored record invalid (version/expiry). Will re-prompt.");
1789
+ }
1790
+ }
1791
+ /** Check whether a specific feature is consented. */
1792
+ has(feature) {
1793
+ if (!this.record) return false;
1794
+ return !!this.record[feature];
1795
+ }
1796
+ // ── Internal (used by ZerocostSDK.init) ──────────────────────────
1797
+ /** Should the consent prompt be shown? */
1798
+ shouldPrompt() {
1799
+ if (this.needsReset) return true;
1800
+ if (!this.record) return true;
1801
+ if (!this.isValid(this.record)) return true;
1802
+ return false;
1803
+ }
1804
+ /** Show the consent popup, wait for confirmation, store record. */
1805
+ async promptAndWait() {
1806
+ this.needsReset = false;
1807
+ const result = await showConsentUI({
1808
+ appName: this.appName,
1809
+ theme: this.theme,
1810
+ privacyPolicyUrl: this.consentConfig.privacyPolicyUrl,
1811
+ defaults: this.record ? { ads: this.record.ads, usageData: this.record.usageData, aiInteractions: this.record.aiInteractions } : void 0
1812
+ });
1813
+ const userId = this.getOrCreateUserId();
1814
+ const record = {
1815
+ userId,
1816
+ appId: this.client.getConfig().appId,
1817
+ ads: result.ads,
1818
+ usageData: result.usageData,
1819
+ aiInteractions: result.aiInteractions,
1820
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1821
+ version: CONSENT_VERSION,
1822
+ method: "confirmed",
1823
+ ipRegion: "OTHER"
1824
+ // server can enrich via IP
1825
+ };
1826
+ this.record = record;
1827
+ this.writeStorage(record);
1828
+ if (this.consentConfig.onConsentChange) {
1829
+ try {
1830
+ this.consentConfig.onConsentChange(record);
1831
+ } catch (err) {
1832
+ this.client.log(`onConsentChange callback error: ${err}`);
1833
+ }
1834
+ }
1835
+ this.submitToServer(record);
1836
+ this.client.log("Consent confirmed.", record);
1837
+ }
1838
+ // ── Helpers ──────────────────────────────────────────────────────
1839
+ isValid(record) {
1840
+ if (record.version !== CONSENT_VERSION) return false;
1841
+ const age = Date.now() - new Date(record.timestamp).getTime();
1842
+ if (age > TWELVE_MONTHS_MS) return false;
1843
+ return true;
1844
+ }
1845
+ storageKey() {
1846
+ return `${CONSENT_STORAGE_PREFIX}${this.client.getConfig().appId}`;
1847
+ }
1848
+ hydrateFromStorage() {
1849
+ if (typeof window === "undefined") return;
1850
+ try {
1851
+ const raw = localStorage.getItem(this.storageKey());
1852
+ if (!raw) return;
1853
+ const parsed = JSON.parse(raw);
1854
+ if (this.isValid(parsed)) {
1855
+ this.record = parsed;
1856
+ }
1857
+ } catch {
1858
+ }
1859
+ }
1860
+ writeStorage(record) {
1861
+ if (typeof window === "undefined") return;
1862
+ try {
1863
+ localStorage.setItem(this.storageKey(), JSON.stringify(record));
1864
+ } catch {
1865
+ this.client.log("Failed to write consent to localStorage.");
1866
+ }
1867
+ }
1868
+ clearStorage() {
1869
+ if (typeof window === "undefined") return;
1870
+ try {
1871
+ localStorage.removeItem(this.storageKey());
1872
+ } catch {
1873
+ }
1874
+ }
1875
+ getOrCreateUserId() {
1876
+ const key = "zerocost-user-id";
1877
+ if (typeof window === "undefined") return this.generateUUID();
1878
+ let id = localStorage.getItem(key);
1879
+ if (!id) {
1880
+ id = this.generateUUID();
1881
+ try {
1882
+ localStorage.setItem(key, id);
1883
+ } catch {
1884
+ }
1885
+ }
1886
+ return id;
1887
+ }
1888
+ generateUUID() {
1889
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
1890
+ return crypto.randomUUID();
1891
+ }
1892
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1893
+ const r = Math.random() * 16 | 0;
1894
+ const v = c === "x" ? r : r & 3 | 8;
1895
+ return v.toString(16);
1896
+ });
1897
+ }
1898
+ async submitToServer(record) {
1899
+ try {
1900
+ await this.client.request("/consent/submit", record);
1901
+ this.client.log("Consent record submitted to server.");
1902
+ } catch (err) {
1903
+ this.client.log(`Failed to submit consent to server: ${err}`);
1904
+ }
1905
+ }
1906
+ };
1907
+
1306
1908
  // src/index.ts
1307
1909
  var CONFIG_CACHE_PREFIX = "zerocost-sdk-config:";
1308
1910
  var CONFIG_SYNC_DEBOUNCE_MS = 750;
@@ -1314,6 +1916,7 @@ var ZerocostSDK = class {
1314
1916
  widget;
1315
1917
  data;
1316
1918
  recording;
1919
+ consent;
1317
1920
  lastConfigHash = "";
1318
1921
  lastDataCollectionHash = "";
1319
1922
  configSyncInFlight = null;
@@ -1326,6 +1929,11 @@ var ZerocostSDK = class {
1326
1929
  this.widget = new WidgetModule(this.core);
1327
1930
  this.data = new LLMDataModule(this.core);
1328
1931
  this.recording = new RecordingModule(this.core);
1932
+ this.consent = new ConsentManager(this.core, {
1933
+ appName: config.appName,
1934
+ theme: config.theme,
1935
+ consent: config.consent
1936
+ });
1329
1937
  }
1330
1938
  async init() {
1331
1939
  this.core.init();
@@ -1337,6 +1945,14 @@ var ZerocostSDK = class {
1337
1945
  this.core.log("Running inside an iframe. Ads render if permissions allow.");
1338
1946
  }
1339
1947
  this.core.log("Initializing Zerocost SDK.");
1948
+ if (this.consent.shouldPrompt()) {
1949
+ this.core.log("Consent required \u2014 showing prompt.");
1950
+ await this.consent.promptAndWait();
1951
+ }
1952
+ if (!this.consent.has("ads")) {
1953
+ this.core.log("Ads consent not granted \u2014 skipping ad injection.");
1954
+ return;
1955
+ }
1340
1956
  const cachedConfig = this.readCachedConfig();
1341
1957
  if (cachedConfig) {
1342
1958
  this.lastConfigHash = this.configToHash(cachedConfig);
@@ -1405,10 +2021,10 @@ var ZerocostSDK = class {
1405
2021
  if (nextHash === this.lastDataCollectionHash) return;
1406
2022
  this.data.stop();
1407
2023
  this.recording.stop();
1408
- if (dataCollection?.llm) {
2024
+ if (dataCollection?.llm && this.consent.has("usageData")) {
1409
2025
  this.data.start(dataCollection.llm);
1410
2026
  }
1411
- if (dataCollection?.recording) {
2027
+ if (dataCollection?.recording && this.consent.has("aiInteractions")) {
1412
2028
  this.recording.start(dataCollection.recording);
1413
2029
  }
1414
2030
  this.lastDataCollectionHash = nextHash;
@@ -1524,6 +2140,7 @@ var ZerocostSDK = class {
1524
2140
  };
1525
2141
  // Annotate the CommonJS export names for ESM import in node:
1526
2142
  0 && (module.exports = {
2143
+ ConsentManager,
1527
2144
  LLMDataModule,
1528
2145
  RecordingModule,
1529
2146
  ZerocostClient,
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ import { TrackModule } from './modules/trackers';
4
4
  import { WidgetModule } from './modules/widget';
5
5
  import { LLMDataModule } from './modules/llm-data';
6
6
  import { RecordingModule } from './modules/recording';
7
+ import { ConsentManager } from './core/consent';
7
8
  import { ZerocostConfig } from './types';
8
9
  export declare class ZerocostSDK {
9
10
  core: ZerocostClient;
@@ -12,6 +13,7 @@ export declare class ZerocostSDK {
12
13
  widget: WidgetModule;
13
14
  data: LLMDataModule;
14
15
  recording: RecordingModule;
16
+ consent: ConsentManager;
15
17
  private lastConfigHash;
16
18
  private lastDataCollectionHash;
17
19
  private configSyncInFlight;
@@ -40,5 +42,6 @@ export declare class ZerocostSDK {
40
42
  }
41
43
  export * from './types';
42
44
  export { ZerocostClient } from './core/client';
45
+ export { ConsentManager } from './core/consent';
43
46
  export { LLMDataModule } from './modules/llm-data';
44
47
  export { RecordingModule } from './modules/recording';