@zerocost/sdk 0.2.0 → 0.3.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.js CHANGED
@@ -1,16 +1,27 @@
1
+ // src/core/config.ts
2
+ var EDGE_FUNCTION_BASE = "https://mwbgzpbuoojqsuxduieo.supabase.co/functions/v1";
3
+ function getBaseUrl(custom) {
4
+ return custom || EDGE_FUNCTION_BASE;
5
+ }
6
+
1
7
  // src/core/client.ts
2
8
  var ZerocostClient = class {
3
9
  config;
10
+ baseUrl;
4
11
  isInitialized = false;
5
12
  constructor(config) {
6
13
  if (!config.appId) {
7
14
  throw new Error("ZerocostSDK: appId is required");
8
15
  }
16
+ if (!config.apiKey) {
17
+ throw new Error("ZerocostSDK: apiKey is required");
18
+ }
9
19
  this.config = {
10
20
  environment: "production",
11
21
  debug: false,
12
22
  ...config
13
23
  };
24
+ this.baseUrl = getBaseUrl(config.baseUrl);
14
25
  }
15
26
  init() {
16
27
  if (this.isInitialized) {
@@ -18,40 +29,753 @@ var ZerocostClient = class {
18
29
  return;
19
30
  }
20
31
  this.isInitialized = true;
21
- this.log(`ZerocostSDK Initialized for ${this.config.appId} in ${this.config.environment} mode.`);
32
+ this.log(`ZerocostSDK initialized for ${this.config.appId} in ${this.config.environment} mode.`);
22
33
  }
23
34
  getConfig() {
24
35
  return this.config;
25
36
  }
37
+ async request(path, body) {
38
+ const url = `${this.baseUrl}${path}`;
39
+ this.log(`\u2192 ${url}`, body);
40
+ const res = await fetch(url, {
41
+ method: "POST",
42
+ headers: {
43
+ "Content-Type": "application/json",
44
+ "x-api-key": this.config.apiKey
45
+ },
46
+ body: body ? JSON.stringify(body) : void 0
47
+ });
48
+ const data = await res.json();
49
+ if (!res.ok) {
50
+ this.log(`\u2717 ${res.status}`, data);
51
+ throw new Error(data.error || `Request failed with status ${res.status}`);
52
+ }
53
+ this.log(`\u2713 ${res.status}`, data);
54
+ return data;
55
+ }
26
56
  log(message, data) {
27
57
  if (this.config.debug) {
28
- console.log(`[Zerocost] ${message}`, data || "");
58
+ console.log(`[Zerocost] ${message}`, data ?? "");
59
+ }
60
+ }
61
+ };
62
+
63
+ // src/modules/ads.ts
64
+ var AdsModule = class {
65
+ constructor(client) {
66
+ this.client = client;
67
+ }
68
+ /**
69
+ * Request an ad for a given placement.
70
+ * Returns ad content including headline, body, CTA, and tracking pixel.
71
+ */
72
+ async requestAd(placementId) {
73
+ const data = await this.client.request("/serve-ad", {
74
+ placement_id: placementId
75
+ });
76
+ return data.ad;
77
+ }
78
+ };
79
+
80
+ // src/modules/trackers.ts
81
+ var TrackModule = class {
82
+ constructor(client) {
83
+ this.client = client;
84
+ }
85
+ /**
86
+ * Track a custom event with optional properties.
87
+ */
88
+ async event(eventName, properties) {
89
+ await this.client.request("/track-event", {
90
+ event_name: eventName,
91
+ properties: properties || {}
92
+ });
93
+ }
94
+ /**
95
+ * Track an ad impression.
96
+ */
97
+ async impression(adId, placementId) {
98
+ await this.event("ad_impression", { ad_id: adId, placement_id: placementId });
99
+ }
100
+ /**
101
+ * Track an ad click.
102
+ */
103
+ async click(adId, placementId) {
104
+ await this.event("ad_click", { ad_id: adId, placement_id: placementId });
105
+ }
106
+ };
107
+
108
+ // src/modules/widget.ts
109
+ var POSITION_STYLES = {
110
+ "bottom-right": "position:fixed;bottom:24px;right:24px;z-index:9999;",
111
+ "bottom-left": "position:fixed;bottom:24px;left:24px;z-index:9999;",
112
+ "top-right": "position:fixed;top:24px;right:24px;z-index:9999;",
113
+ "top-left": "position:fixed;top:24px;left:24px;z-index:9999;",
114
+ "bottom-center": "position:fixed;bottom:24px;left:50%;transform:translateX(-50%);z-index:9999;",
115
+ "top-center": "position:fixed;top:24px;left:50%;transform:translateX(-50%);z-index:9999;",
116
+ "center": "position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:9999;",
117
+ "sidebar-left": "position:fixed;top:50%;left:24px;transform:translateY(-50%);z-index:9999;",
118
+ "sidebar-right": "position:fixed;top:50%;right:24px;transform:translateY(-50%);z-index:9999;"
119
+ };
120
+ var WidgetModule = class {
121
+ constructor(client) {
122
+ this.client = client;
123
+ }
124
+ mounted = /* @__PURE__ */ new Map();
125
+ async autoInjectWithConfig(display) {
126
+ try {
127
+ const configs = this.normalizeConfigs(display);
128
+ const enabledCount = Object.values(configs).filter((c) => c.enabled).length;
129
+ this.client.log(`Auto-inject: ${enabledCount} ad format(s) enabled. No custom component needed \u2014 ads render automatically.`);
130
+ await this.mountFormats(configs);
131
+ this.client.log("\u2713 Ad slots injected successfully. Ads will appear once inventory is available.");
132
+ } catch (err) {
133
+ this.client.log(`Widget autoInject error: ${err}`);
134
+ }
135
+ }
136
+ async autoInject() {
137
+ try {
138
+ this.client.log("Fetching ad placements from server...");
139
+ const { display } = await this.client.request("/get-placements");
140
+ const configs = this.normalizeConfigs(display);
141
+ const enabledCount = Object.values(configs).filter((c) => c.enabled).length;
142
+ this.client.log(`Auto-inject: ${enabledCount} ad format(s) enabled. No custom component needed \u2014 ads render automatically.`);
143
+ await this.mountFormats(configs);
144
+ this.client.log("\u2713 Ad slots injected successfully. Ads will appear once inventory is available.");
145
+ } catch (err) {
146
+ this.client.log(`Widget autoInject error: ${err}`);
147
+ }
148
+ }
149
+ normalizeConfigs(display) {
150
+ if (display && "video-widget" in display) {
151
+ return display;
152
+ }
153
+ const pos = display?.position || "bottom-right";
154
+ const theme = display?.theme || "dark";
155
+ return {
156
+ "video-widget": { position: pos, theme, autoplay: false, enabled: true },
157
+ "tooltip-ad": { position: pos, theme, autoplay: false, enabled: true },
158
+ "sponsored-card": { position: pos, theme, autoplay: false, enabled: true },
159
+ "inline-text": { position: "after-paragraph-1", theme, autoplay: false, enabled: true }
160
+ };
161
+ }
162
+ async mountFormats(configs) {
163
+ for (const [format, cfg] of Object.entries(configs)) {
164
+ if (!cfg.enabled) continue;
165
+ const elId = `zerocost-${format}`;
166
+ if (document.getElementById(elId)) continue;
167
+ const el = document.createElement("div");
168
+ el.id = elId;
169
+ el.setAttribute("data-zerocost", "");
170
+ el.setAttribute("data-format", format);
171
+ if (format === "inline-text") {
172
+ continue;
173
+ }
174
+ const posStyle = POSITION_STYLES[cfg.position] || POSITION_STYLES["bottom-right"];
175
+ const maxW = format === "video-widget" ? "max-width:200px;" : format === "sponsored-card" || format === "sidebar-display" ? "max-width:176px;" : "max-width:320px;";
176
+ el.setAttribute("style", posStyle + maxW);
177
+ document.body.appendChild(el);
178
+ await this.mount(elId, { format, refreshInterval: 30, theme: cfg.theme, autoplay: cfg.autoplay });
179
+ }
180
+ }
181
+ async mount(targetElementId, options = {}) {
182
+ const el = document.getElementById(targetElementId);
183
+ if (!el) return;
184
+ if (this.mounted.has(targetElementId)) this.unmount(targetElementId);
185
+ const refreshMs = (options.refreshInterval ?? 30) * 1e3;
186
+ const theme = options.theme || "dark";
187
+ const format = options.format || "video-widget";
188
+ const autoplay = options.autoplay ?? false;
189
+ const render = async () => {
190
+ try {
191
+ const formatMap = {
192
+ "video-widget": "video",
193
+ "sponsored-card": "display",
194
+ "sidebar-display": "display",
195
+ "tooltip-ad": "native",
196
+ "inline-text": "native"
197
+ };
198
+ const body = {
199
+ format: formatMap[format] || format,
200
+ theme,
201
+ autoplay
202
+ };
203
+ const data = await this.client.request("/serve-widget", body);
204
+ const ad = data.ad;
205
+ if (!ad || !data.html) {
206
+ this.client.log(`No ad inventory available for format "${format}". Slot "${targetElementId}" is empty \u2014 ads will appear automatically when inventory is added by partners.`);
207
+ el.innerHTML = "";
208
+ return;
209
+ }
210
+ el.innerHTML = data.html;
211
+ el.setAttribute("data-zerocost-ad-id", ad.id);
212
+ const ctas = el.querySelectorAll("[data-zc-cta]");
213
+ ctas.forEach((cta) => {
214
+ cta.addEventListener("click", () => {
215
+ this.client.request("/track-event", {
216
+ event_name: "ad_click",
217
+ properties: { ad_id: ad.id, format }
218
+ }).catch(() => {
219
+ });
220
+ });
221
+ });
222
+ const closeBtn = el.querySelector("[data-zc-close]");
223
+ if (closeBtn) {
224
+ closeBtn.addEventListener("click", (e) => {
225
+ e.preventDefault();
226
+ e.stopPropagation();
227
+ el.innerHTML = "";
228
+ el.style.display = "none";
229
+ });
230
+ }
231
+ } catch (err) {
232
+ this.client.log(`Widget render error: ${err}`);
233
+ }
234
+ };
235
+ await render();
236
+ const interval = refreshMs > 0 ? setInterval(render, refreshMs) : null;
237
+ this.mounted.set(targetElementId, { elementId: targetElementId, interval });
238
+ }
239
+ unmount(targetElementId) {
240
+ const slot = this.mounted.get(targetElementId);
241
+ if (slot) {
242
+ if (slot.interval) clearInterval(slot.interval);
243
+ const el = document.getElementById(targetElementId);
244
+ if (el) el.remove();
245
+ this.mounted.delete(targetElementId);
246
+ }
247
+ }
248
+ unmountAll() {
249
+ for (const id of this.mounted.keys()) this.unmount(id);
250
+ }
251
+ // ─── Format-specific renderers (matching SDK Playground previews exactly) ───
252
+ buildFormatHtml(format, ad, theme, autoplay) {
253
+ switch (format) {
254
+ case "video-widget":
255
+ return this.buildVideoWidget(ad, theme, autoplay);
256
+ case "tooltip-ad":
257
+ return this.buildTooltipAd(ad, theme);
258
+ case "sponsored-card":
259
+ return this.buildSponsoredCard(ad, theme);
260
+ case "inline-text":
261
+ return this.buildInlineText(ad, theme);
262
+ default:
263
+ return this.buildSponsoredCard(ad, theme);
264
+ }
265
+ }
266
+ /**
267
+ * Floating Video Widget — matches playground VideoWidgetPreview
268
+ * 9:16 aspect ratio, video with gradient overlay, sponsor label, CTA button
269
+ */
270
+ buildVideoWidget(ad, theme, autoplay) {
271
+ const isDark = theme === "dark";
272
+ const border = isDark ? "#333" : "#e0e0e0";
273
+ const videoSrc = ad.video_url || ad.image_url || "";
274
+ const hasVideo = !!ad.video_url;
275
+ const sponsor = ad.title || "Sponsor";
276
+ const cta = ad.cta_text || "Learn More";
277
+ return `
278
+ <div style="width:200px;aspect-ratio:9/16;border-radius:12px;border:1px solid ${border};overflow:hidden;position:relative;font-family:system-ui,-apple-system,sans-serif;box-shadow:0 8px 32px rgba(0,0,0,${isDark ? "0.5" : "0.15"});">
279
+ <button data-zc-close style="position:absolute;top:8px;right:8px;z-index:20;width:24px;height:24px;border-radius:50%;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);border:none;color:rgba(255,255,255,0.7);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;line-height:1;">\u2715</button>
280
+ ${hasVideo ? `<video src="${this.esc(videoSrc)}" ${autoplay ? "autoplay" : ""} muted loop playsinline style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;"></video>` : `<img src="${this.esc(videoSrc)}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;" />`}
281
+ <div style="position:absolute;inset-inline:0;bottom:0;padding:12px;background:linear-gradient(to top,rgba(0,0,0,0.9),rgba(0,0,0,0.5),transparent);z-index:10;">
282
+ <div style="font-size:8px;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:rgba(255,255,255,0.5);margin-bottom:2px;">Sponsored by ${this.esc(sponsor)}</div>
283
+ <div style="color:#fff;font-weight:600;font-size:12px;line-height:1.3;">${this.esc(ad.description || "")}</div>
284
+ <a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="display:block;margin-top:8px;width:100%;padding:6px 0;background:#fff;color:#000;font-size:10px;font-weight:700;text-align:center;border-radius:6px;text-decoration:none;cursor:pointer;">${this.esc(cta)}</a>
285
+ </div>
286
+ </div>`;
287
+ }
288
+ /**
289
+ * Tooltip Ad — matches playground ContextualTextPreview
290
+ * Small floating card with sponsored label, inline text, and link
291
+ */
292
+ buildTooltipAd(ad, theme) {
293
+ const isDark = theme === "dark";
294
+ const bg = isDark ? "#111" : "#ffffff";
295
+ const fg = isDark ? "#fff" : "#111";
296
+ const fgFaint = isDark ? "#888" : "#999";
297
+ const border = isDark ? "#333" : "#e0e0e0";
298
+ return `
299
+ <div style="max-width:320px;border-radius:8px;padding:12px;box-shadow:0 8px 32px rgba(0,0,0,${isDark ? "0.4" : "0.12"});background:${bg};border:1px solid ${border};font-family:system-ui,-apple-system,sans-serif;">
300
+ <div style="font-size:10px;font-family:monospace;margin-bottom:6px;display:flex;align-items:center;color:${fgFaint};">
301
+ <span style="width:6px;height:6px;border-radius:50%;background:#22c55e;margin-right:6px;display:inline-block;"></span>
302
+ Sponsored
303
+ </div>
304
+ <p style="font-size:12px;line-height:1.5;color:${fg};margin:0;">
305
+ ${this.esc(ad.description || ad.title)}
306
+ <a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="color:#60a5fa;text-decoration:underline;text-underline-offset:2px;cursor:pointer;margin-left:4px;">${this.esc(ad.cta_text || "Learn More")}</a>
307
+ </p>
308
+ </div>`;
309
+ }
310
+ /**
311
+ * Sponsored Card — matches playground SponsoredCardPreview
312
+ * Card with gradient header, icon, headline, description, CTA button
313
+ */
314
+ buildSponsoredCard(ad, theme) {
315
+ const isDark = theme === "dark";
316
+ const bg = isDark ? "#111" : "#ffffff";
317
+ const fg = isDark ? "#fff" : "#111";
318
+ const fgFaint = isDark ? "#888" : "#999";
319
+ const border = isDark ? "#333" : "#e0e0e0";
320
+ const iconBg = isDark ? "#0a0a0a" : "#f5f5f5";
321
+ const initial = (ad.title || "A").charAt(0).toUpperCase();
322
+ return `
323
+ <div style="width:176px;border-radius:12px;overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,${isDark ? "0.4" : "0.12"});background:${bg};border:1px solid ${border};font-family:system-ui,-apple-system,sans-serif;">
324
+ ${ad.image_url ? `<img src="${this.esc(ad.image_url)}" style="width:100%;height:80px;object-fit:cover;display:block;" />` : `<div style="height:80px;background:linear-gradient(135deg,rgba(249,115,22,0.2),rgba(236,72,153,0.2));"></div>`}
325
+ <div style="padding:12px;">
326
+ <div style="width:24px;height:24px;border-radius:4px;background:${iconBg};border:1px solid ${border};display:flex;align-items:center;justify-content:center;font-weight:700;font-size:9px;color:${fg};margin-bottom:8px;">${initial}</div>
327
+ <div style="font-weight:700;font-size:12px;color:${fg};margin-bottom:2px;letter-spacing:-0.01em;">${this.esc(ad.title)}</div>
328
+ <div style="font-size:10px;color:${fgFaint};line-height:1.4;margin-bottom:12px;">${this.esc(ad.description || "")}</div>
329
+ <a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="display:block;width:100%;padding:6px 0;border:1px solid ${border};border-radius:4px;font-size:10px;font-weight:500;color:${fg};text-align:center;text-decoration:none;cursor:pointer;">${this.esc(ad.cta_text || "Learn More")}</a>
330
+ </div>
331
+ </div>`;
332
+ }
333
+ /**
334
+ * Inline Text Ad — matches playground InlineTextPreview
335
+ * Card with sponsored header, icon, headline, body, CTA button
336
+ * Designed to be injected inside content/chat streams
337
+ */
338
+ buildInlineText(ad, theme) {
339
+ const isDark = theme === "dark";
340
+ const bg = isDark ? "#141414" : "#fafafa";
341
+ const fg = isDark ? "#fff" : "#111";
342
+ const fgMuted = isDark ? "#aaa" : "#666";
343
+ const fgFaint = isDark ? "#888" : "#999";
344
+ const border = isDark ? "#2a2a2a" : "#d0d0d0";
345
+ const headerBorder = isDark ? "#222" : "#e8e8e8";
346
+ const btnBg = isDark ? "#fff" : "#111";
347
+ const btnFg = isDark ? "#111" : "#fff";
348
+ const initial = "\u25B2";
349
+ const sponsor = ad.title?.split("\u2014")[0]?.trim() || ad.title || "Sponsor";
350
+ return `
351
+ <div style="border-radius:8px;overflow:hidden;border:1px solid ${border};background:${bg};font-family:system-ui,-apple-system,sans-serif;max-width:100%;">
352
+ <div style="padding:8px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid ${headerBorder};">
353
+ <span style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:${fgFaint};">Sponsored</span>
354
+ <span style="font-size:9px;color:${fgFaint};">Ad \xB7 ${this.esc(sponsor)}</span>
355
+ </div>
356
+ <div style="padding:12px;">
357
+ <div style="display:flex;gap:10px;">
358
+ <div style="width:32px;height:32px;border-radius:6px;background:${btnBg};color:${btnFg};display:flex;align-items:center;justify-content:center;font-weight:700;font-size:11px;flex-shrink:0;">${initial}</div>
359
+ <div style="flex:1;min-width:0;">
360
+ <div style="font-weight:600;font-size:12px;color:${fg};margin-bottom:2px;">${this.esc(ad.title)}</div>
361
+ <div style="font-size:11px;line-height:1.4;color:${fgMuted};">${this.esc(ad.description || "")}</div>
362
+ <a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="display:inline-block;margin-top:8px;padding:4px 12px;border-radius:4px;background:${btnBg};color:${btnFg};font-size:10px;font-weight:600;text-decoration:none;cursor:pointer;">${this.esc(ad.cta_text || "Learn More")} \u2192</a>
363
+ </div>
364
+ </div>
365
+ </div>
366
+ </div>`;
367
+ }
368
+ esc(str) {
369
+ if (!str) return "";
370
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
371
+ }
372
+ };
373
+
374
+ // src/modules/llm-data.ts
375
+ var LLMDataModule = class {
376
+ constructor(client) {
377
+ this.client = client;
378
+ }
379
+ config = null;
380
+ buffer = [];
381
+ flushInterval = null;
382
+ clickHandler = null;
383
+ errorHandler = null;
384
+ fetchOriginal = null;
385
+ /**
386
+ * Start capturing LLM training data based on server config.
387
+ */
388
+ start(config) {
389
+ this.config = config;
390
+ this.client.log(`LLMData: started (sample=${config.sampleRate}%)`);
391
+ if (Math.random() * 100 > config.sampleRate) {
392
+ this.client.log("LLMData: session not sampled, skipping");
393
+ return;
394
+ }
395
+ if (config.uiInteractions) this.captureUIInteractions();
396
+ if (config.textPrompts) this.interceptPrompts();
397
+ if (config.apiErrors) this.captureAPIErrors();
398
+ this.flushInterval = setInterval(() => this.flush(), 1e4);
399
+ }
400
+ stop() {
401
+ if (this.flushInterval) clearInterval(this.flushInterval);
402
+ if (this.clickHandler) {
403
+ document.removeEventListener("click", this.clickHandler, true);
404
+ }
405
+ if (this.errorHandler) {
406
+ window.removeEventListener("error", this.errorHandler);
407
+ }
408
+ if (this.fetchOriginal) {
409
+ window.fetch = this.fetchOriginal;
410
+ }
411
+ this.flush();
412
+ this.config = null;
413
+ }
414
+ /**
415
+ * Manually track an LLM prompt/response pair (for startups that want to
416
+ * explicitly send their AI interactions).
417
+ */
418
+ trackPrompt(prompt, response, meta) {
419
+ if (!this.config?.textPrompts) return;
420
+ this.pushEvent("llm_prompt", {
421
+ prompt: this.scrub(prompt),
422
+ response: response ? this.scrub(response) : void 0,
423
+ ...meta
424
+ });
425
+ }
426
+ /**
427
+ * Manually track an API error.
428
+ */
429
+ trackError(endpoint, status, message) {
430
+ if (!this.config?.apiErrors) return;
431
+ this.pushEvent("api_error", {
432
+ endpoint,
433
+ status,
434
+ message: message ? this.scrub(message) : void 0
435
+ });
436
+ }
437
+ // ── Private ──
438
+ captureUIInteractions() {
439
+ this.clickHandler = (e) => {
440
+ const target = e.target;
441
+ if (!target) return;
442
+ const tag = target.tagName?.toLowerCase();
443
+ const text = target.textContent?.slice(0, 50)?.trim() || "";
444
+ const role = target.getAttribute("role");
445
+ const ariaLabel = target.getAttribute("aria-label");
446
+ const path = this.getPath(target);
447
+ this.pushEvent("ui_interaction", {
448
+ action: "click",
449
+ tag,
450
+ text: this.scrub(text),
451
+ role,
452
+ ariaLabel,
453
+ path,
454
+ url: location.pathname
455
+ });
456
+ };
457
+ document.addEventListener("click", this.clickHandler, true);
458
+ }
459
+ interceptPrompts() {
460
+ this.fetchOriginal = window.fetch;
461
+ const self = this;
462
+ const origFetch = window.fetch;
463
+ window.fetch = async function(input, init) {
464
+ const fetchInput = input instanceof URL ? input.toString() : input;
465
+ const url = typeof fetchInput === "string" ? fetchInput : fetchInput.url;
466
+ const isLLM = /\/(chat|completions|generate|predict|inference|ask)/i.test(url);
467
+ if (!isLLM) return origFetch.call(window, fetchInput, init);
468
+ try {
469
+ const res = await origFetch.call(window, fetchInput, init);
470
+ const clone = res.clone();
471
+ let reqBody;
472
+ if (init?.body && typeof init.body === "string") {
473
+ try {
474
+ const parsed = JSON.parse(init.body);
475
+ reqBody = JSON.stringify(parsed.messages || parsed.prompt || "").slice(0, 500);
476
+ } catch {
477
+ reqBody = void 0;
478
+ }
479
+ }
480
+ clone.text().then((text) => {
481
+ self.pushEvent("llm_prompt", {
482
+ endpoint: new URL(url).pathname,
483
+ request: reqBody ? self.scrub(reqBody) : void 0,
484
+ response: self.scrub(text.slice(0, 500)),
485
+ status: res.status
486
+ });
487
+ }).catch(() => {
488
+ });
489
+ return res;
490
+ } catch (err) {
491
+ self.pushEvent("api_error", {
492
+ endpoint: new URL(url).pathname,
493
+ message: self.scrub(err.message)
494
+ });
495
+ throw err;
496
+ }
497
+ };
498
+ }
499
+ captureAPIErrors() {
500
+ this.errorHandler = (e) => {
501
+ this.pushEvent("api_error", {
502
+ message: this.scrub(e.message || ""),
503
+ filename: e.filename,
504
+ line: e.lineno,
505
+ url: location.pathname
506
+ });
507
+ };
508
+ window.addEventListener("error", this.errorHandler);
509
+ }
510
+ pushEvent(type, data) {
511
+ this.buffer.push({ type, data, timestamp: Date.now() });
512
+ if (this.buffer.length >= 50) this.flush();
513
+ }
514
+ async flush() {
515
+ if (this.buffer.length === 0) return;
516
+ const events = [...this.buffer];
517
+ this.buffer = [];
518
+ try {
519
+ await this.client.request("/ingest-data", { type: "llm", events });
520
+ } catch (err) {
521
+ this.client.log(`LLMData flush error: ${err}`);
522
+ this.buffer.unshift(...events);
523
+ }
524
+ }
525
+ /** Basic PII scrubbing */
526
+ scrub(text) {
527
+ return text.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, "[EMAIL]").replace(/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, "[PHONE]").replace(/\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g, "[CARD]").replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[SSN]").replace(/\b(sk|pk|api|key|secret|token)[-_]?[a-zA-Z0-9]{16,}\b/gi, "[API_KEY]");
528
+ }
529
+ getPath(el) {
530
+ const parts = [];
531
+ let cur = el;
532
+ while (cur && parts.length < 5) {
533
+ let seg = cur.tagName?.toLowerCase() || "";
534
+ if (cur.id) seg += `#${cur.id}`;
535
+ else if (cur.className && typeof cur.className === "string") {
536
+ const cls = cur.className.split(" ")[0];
537
+ if (cls) seg += `.${cls}`;
538
+ }
539
+ parts.unshift(seg);
540
+ cur = cur.parentElement;
541
+ }
542
+ return parts.join(" > ");
543
+ }
544
+ };
545
+
546
+ // src/modules/recording.ts
547
+ var RecordingModule = class {
548
+ constructor(client) {
549
+ this.client = client;
550
+ }
551
+ config = null;
552
+ events = [];
553
+ observer = null;
554
+ flushInterval = null;
555
+ stopTimer = null;
556
+ handlers = [];
557
+ sessionId = "";
558
+ startTime = 0;
559
+ /**
560
+ * Start a lightweight UX session recording.
561
+ */
562
+ start(config) {
563
+ if (typeof document === "undefined") return;
564
+ this.config = config;
565
+ this.sessionId = this.generateId();
566
+ this.startTime = Date.now();
567
+ if (Math.random() * 100 > config.sampleRate) {
568
+ this.client.log("Recording: session not sampled, skipping");
569
+ return;
570
+ }
571
+ this.client.log(`Recording: started session ${this.sessionId} (max ${config.maxDuration}s)`);
572
+ this.pushEvent("resize", {
573
+ width: window.innerWidth,
574
+ height: window.innerHeight,
575
+ url: location.href,
576
+ title: document.title
577
+ });
578
+ this.observer = new MutationObserver((mutations) => {
579
+ for (const m of mutations) {
580
+ if (m.type === "childList") {
581
+ this.pushEvent("mutation", {
582
+ target: this.describeEl(m.target),
583
+ added: m.addedNodes.length,
584
+ removed: m.removedNodes.length
585
+ });
586
+ } else if (m.type === "attributes") {
587
+ this.pushEvent("mutation", {
588
+ target: this.describeEl(m.target),
589
+ attr: m.attributeName
590
+ });
591
+ }
592
+ }
593
+ });
594
+ this.observer.observe(document.body, {
595
+ childList: true,
596
+ attributes: true,
597
+ subtree: true,
598
+ characterData: false
599
+ });
600
+ this.on("click", (e) => {
601
+ const target = e.target;
602
+ this.pushEvent("click", {
603
+ x: e.clientX,
604
+ y: e.clientY,
605
+ target: this.describeEl(target)
606
+ });
607
+ }, true);
608
+ let scrollTimeout = null;
609
+ this.on("scroll", () => {
610
+ if (scrollTimeout) return;
611
+ scrollTimeout = setTimeout(() => {
612
+ scrollTimeout = null;
613
+ this.pushEvent("scroll", {
614
+ x: window.scrollX,
615
+ y: window.scrollY
616
+ });
617
+ }, 250);
618
+ });
619
+ if (!config.blurInputs) {
620
+ this.on("input", (e) => {
621
+ const target = e.target;
622
+ if (this.shouldMask(target)) return;
623
+ this.pushEvent("input", {
624
+ target: this.describeEl(target),
625
+ length: target.value?.length || 0,
626
+ // Never send actual values - just metadata
627
+ type: target.type || "text"
628
+ });
629
+ }, true);
630
+ }
631
+ this.on("popstate", () => {
632
+ this.pushEvent("navigation", { url: location.pathname });
633
+ });
634
+ this.flushInterval = setInterval(() => this.flush(), 15e3);
635
+ this.stopTimer = setTimeout(() => {
636
+ this.client.log("Recording: max duration reached, stopping");
637
+ this.stop();
638
+ }, config.maxDuration * 1e3);
639
+ }
640
+ stop() {
641
+ if (this.observer) {
642
+ this.observer.disconnect();
643
+ this.observer = null;
644
+ }
645
+ for (const { event, fn } of this.handlers) {
646
+ (event === "click" ? document : window).removeEventListener(event, fn, true);
647
+ }
648
+ this.handlers = [];
649
+ if (this.flushInterval) clearInterval(this.flushInterval);
650
+ if (this.stopTimer) clearTimeout(this.stopTimer);
651
+ this.flush();
652
+ this.config = null;
653
+ }
654
+ // ── Private ──
655
+ on(event, fn, capture = false) {
656
+ const target = event === "click" ? document : window;
657
+ target.addEventListener(event, fn, capture);
658
+ this.handlers.push({ event, fn });
659
+ }
660
+ pushEvent(type, data) {
661
+ this.events.push({
662
+ type,
663
+ timestamp: Date.now() - this.startTime,
664
+ data
665
+ });
666
+ if (this.events.length >= 100) this.flush();
667
+ }
668
+ async flush() {
669
+ if (this.events.length === 0) return;
670
+ const batch = [...this.events];
671
+ this.events = [];
672
+ try {
673
+ await this.client.request("/ingest-data", {
674
+ type: "recording",
675
+ sessionId: this.sessionId,
676
+ events: batch
677
+ });
678
+ } catch (err) {
679
+ this.client.log(`Recording flush error: ${err}`);
680
+ this.events.unshift(...batch);
29
681
  }
30
682
  }
683
+ shouldMask(el) {
684
+ if (!this.config) return true;
685
+ for (const sel of this.config.maskSelectors) {
686
+ try {
687
+ if (el.matches(sel) || el.closest(sel)) return true;
688
+ } catch {
689
+ }
690
+ }
691
+ const type = el.type;
692
+ return type === "password" || type === "hidden";
693
+ }
694
+ describeEl(el) {
695
+ if (!el || !el.tagName) return "";
696
+ let desc = el.tagName.toLowerCase();
697
+ if (el.id) desc += `#${el.id}`;
698
+ const role = el.getAttribute("role");
699
+ if (role) desc += `[role=${role}]`;
700
+ const label = el.getAttribute("aria-label");
701
+ if (label) desc += `[${label.slice(0, 20)}]`;
702
+ return desc;
703
+ }
704
+ generateId() {
705
+ return "rec_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
706
+ }
31
707
  };
32
708
 
33
709
  // src/index.ts
34
710
  var ZerocostSDK = class {
35
711
  core;
712
+ ads;
713
+ track;
714
+ widget;
715
+ data;
716
+ recording;
36
717
  constructor(config) {
37
718
  this.core = new ZerocostClient(config);
719
+ this.ads = new AdsModule(this.core);
720
+ this.track = new TrackModule(this.core);
721
+ this.widget = new WidgetModule(this.core);
722
+ this.data = new LLMDataModule(this.core);
723
+ this.recording = new RecordingModule(this.core);
38
724
  }
39
- init() {
725
+ /**
726
+ * Initialize the SDK. Automatically:
727
+ * 1. Fetches display preferences and injects ad slots into the DOM
728
+ * 2. Starts LLM data collection if enabled
729
+ * 3. Starts UX session recording if enabled
730
+ *
731
+ * No custom components needed — ads render automatically.
732
+ * Enable `debug: true` in config to see detailed logs.
733
+ */
734
+ async init() {
40
735
  this.core.init();
41
- }
42
- // Stubs for modules
43
- ads = {
44
- requestAd: (placementId) => {
45
- this.core.log(`Requested ad for placement: ${placementId}`);
46
- return Promise.resolve({ id: "ad_123", content: "test" });
736
+ if (typeof document === "undefined") {
737
+ this.core.log("Running in non-browser environment \u2014 skipping DOM injection.");
738
+ return;
47
739
  }
48
- };
49
- track = {
50
- event: (eventName, properties) => {
51
- this.core.log(`Tracked event: ${eventName}`, properties);
740
+ this.core.log("Initializing... Ads will be auto-injected into the DOM. No custom component required.");
741
+ try {
742
+ const { display, dataCollection } = await this.core.request("/get-placements");
743
+ this.widget.autoInjectWithConfig(display);
744
+ if (dataCollection?.llm) {
745
+ this.data.start(dataCollection.llm);
746
+ }
747
+ if (dataCollection?.recording) {
748
+ this.recording.start(dataCollection.recording);
749
+ }
750
+ this.core.log("\u2713 SDK fully initialized. Ads are rendering automatically.");
751
+ } catch (err) {
752
+ this.core.log(`Init error: ${err}. Attempting fallback ad injection...`);
753
+ this.widget.autoInject();
52
754
  }
53
- };
755
+ }
756
+ /**
757
+ * Tear down all modules.
758
+ */
759
+ destroy() {
760
+ this.widget.unmountAll();
761
+ this.data.stop();
762
+ this.recording.stop();
763
+ }
764
+ /**
765
+ * Validate the configured API key against the server.
766
+ */
767
+ async validateKey() {
768
+ try {
769
+ const result = await this.core.request("/validate-key");
770
+ return result;
771
+ } catch (err) {
772
+ return { valid: false, error: err.message };
773
+ }
774
+ }
54
775
  };
55
776
  export {
777
+ LLMDataModule,
778
+ RecordingModule,
779
+ ZerocostClient,
56
780
  ZerocostSDK
57
781
  };