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