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