@zeroclickai/offers-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,564 @@
1
+ //#region src/types.d.ts
2
+ /**
3
+ * User identity context provided during initialization
4
+ */
5
+ interface Identity {
6
+ /** Tenant client user ID */
7
+ userId?: string;
8
+ /** SHA-256 hash of user email address (lowercase, trimmed before hashing) */
9
+ userEmailSha256?: string;
10
+ /** SHA-256 hash of user phone number (E.164 format before hashing) */
11
+ userPhoneNumberSha256?: string;
12
+ /** User locale/language code (e.g., 'en-US') */
13
+ userLocale?: string;
14
+ /** Session identifier for session-level tracking */
15
+ userSessionId?: string;
16
+ /** Tenant-defined grouping ID for analytics segmentation */
17
+ groupingId?: string;
18
+ }
19
+ /**
20
+ * Configuration for initializing the ZeroClick SDK
21
+ */
22
+ interface ZeroClickConfig {
23
+ /** ZeroClick Tenant Client API key (required for getOffers) */
24
+ apiKey?: string;
25
+ /** User identity context */
26
+ identity?: Identity;
27
+ /** Base URL for the API (defaults to production) */
28
+ baseUrl?: string;
29
+ }
30
+ /**
31
+ * Options for fetching offers (server-side only)
32
+ */
33
+ interface GetOffersOptions {
34
+ /** Client IP address (required for server-to-server requests) */
35
+ ipAddress: string;
36
+ /** Search query for offers */
37
+ query?: string;
38
+ /** Maximum number of offers to return (default: 1) */
39
+ limit?: number;
40
+ /** Client user agent string */
41
+ userAgent?: string;
42
+ /** Client origin/referer URL */
43
+ origin?: string;
44
+ }
45
+ /**
46
+ * Product availability status
47
+ */
48
+ type ProductAvailability = 'in_stock' | 'limited' | 'out_of_stock';
49
+ /**
50
+ * Distance unit for location
51
+ */
52
+ type DistanceUnit = 'km' | 'mi';
53
+ /**
54
+ * Brand information for an offer
55
+ */
56
+ interface Brand {
57
+ /** Name of the brand */
58
+ name: string;
59
+ /** Description of the brand */
60
+ description: string | null;
61
+ /** URL of the brand's website */
62
+ url: string | null;
63
+ /** URL of the brand's icon or logo */
64
+ iconUrl: string | null;
65
+ }
66
+ /**
67
+ * Product information for an offer
68
+ */
69
+ interface Product {
70
+ /** Product ID */
71
+ productId: string | null;
72
+ /** Stock keeping unit */
73
+ sku: string | null;
74
+ /** Product title */
75
+ title: string;
76
+ /** Product description */
77
+ description: string | null;
78
+ /** Product category */
79
+ category: string | null;
80
+ /** Product subcategory */
81
+ subcategory: string | null;
82
+ /** Product image URL */
83
+ image: string | null;
84
+ /** Product availability status */
85
+ availability: ProductAvailability | null;
86
+ /** Additional product metadata */
87
+ metadata: Record<string, unknown> | null;
88
+ }
89
+ /**
90
+ * Pricing information for an offer
91
+ */
92
+ interface Price {
93
+ /** Current price amount */
94
+ amount: string | null;
95
+ /** Currency code (e.g., 'USD') */
96
+ currency: string | null;
97
+ /** Original price before discounts */
98
+ originalPrice: string | null;
99
+ /** Discount amount or percentage */
100
+ discount: string | null;
101
+ /** Billing interval for subscriptions (e.g., 'monthly', 'yearly') */
102
+ interval: string | null;
103
+ }
104
+ /**
105
+ * Geographical coordinates
106
+ */
107
+ interface Coordinates {
108
+ /** Latitude */
109
+ lat: number;
110
+ /** Longitude */
111
+ lng: number;
112
+ }
113
+ /**
114
+ * Location information for an offer
115
+ */
116
+ interface Location {
117
+ /** Full address as plain text */
118
+ text: string;
119
+ /** Street address */
120
+ address: string | null;
121
+ /** City */
122
+ city: string | null;
123
+ /** State or region */
124
+ state: string | null;
125
+ /** ZIP or postal code */
126
+ zip: number | null;
127
+ /** Distance from user */
128
+ distance: number | null;
129
+ /** Unit of distance measurement */
130
+ distanceUnit: DistanceUnit | null;
131
+ /** Geographical coordinates */
132
+ coordinates: Coordinates | null;
133
+ /** Operating hours */
134
+ hours: string | null;
135
+ }
136
+ /**
137
+ * Media content information for an offer
138
+ */
139
+ interface Media {
140
+ /** Content title */
141
+ title: string | null;
142
+ /** Content URL */
143
+ url: string | null;
144
+ /** Content description */
145
+ description: string | null;
146
+ /** Type of content (article, video, book, etc.) */
147
+ contentType: string;
148
+ }
149
+ /**
150
+ * Rating information for an offer
151
+ */
152
+ interface Rating {
153
+ /** Rating value */
154
+ value: number;
155
+ /** Rating scale (e.g., 5 for 5-star, 100 for percentage) */
156
+ scale: number;
157
+ /** Number of ratings */
158
+ count: number | null;
159
+ }
160
+ /**
161
+ * UI rendering configuration returned by the API
162
+ */
163
+ interface OfferUI {
164
+ /** Render type discriminator (e.g., 'iframe') */
165
+ type: string;
166
+ /** URL for the UI content (e.g., iframe src) */
167
+ url: string;
168
+ }
169
+ /**
170
+ * An offer returned from the ZeroClick API
171
+ */
172
+ interface Offer {
173
+ /** Unique offer ID */
174
+ id: string;
175
+ /** Offer title */
176
+ title: string | null;
177
+ /** Offer subtitle */
178
+ subtitle: string | null;
179
+ /** Detailed content/description */
180
+ content: string | null;
181
+ /** Call to action text */
182
+ cta: string | null;
183
+ /** Click-through URL */
184
+ clickUrl: string;
185
+ /** Raw URL encoded (not for LLM use) */
186
+ rawUrlEncoded: string | null;
187
+ /** Offer image URL */
188
+ imageUrl: string | null;
189
+ /** Additional offer metadata */
190
+ metadata: Record<string, unknown> | null;
191
+ /** Additional context about the offer */
192
+ context: string | null;
193
+ /** Brand information */
194
+ brand: Brand | null;
195
+ /** Product information */
196
+ product: Product | null;
197
+ /** Pricing information */
198
+ price: Price | null;
199
+ /** Location information */
200
+ location: Location | null;
201
+ /** Media content information */
202
+ media: Media | null;
203
+ /** Rating information */
204
+ rating: Rating | null;
205
+ /** UI configuration for iframe rendering (optional) */
206
+ ui?: OfferUI | null;
207
+ }
208
+ /**
209
+ * Context collected and sent with each API request
210
+ */
211
+ interface RequestContext {
212
+ /** Anonymous ID for user tracking (persisted across sessions) */
213
+ anonymousId: string;
214
+ /** Session ID for this specific session (not persisted) */
215
+ sessionId: string;
216
+ /** SDK version */
217
+ sdkVersion: string;
218
+ /** User agent string */
219
+ userAgent?: string;
220
+ /** Request timestamp (ISO 8601) */
221
+ timestamp: string;
222
+ }
223
+ /**
224
+ * API error response
225
+ */
226
+ interface ApiError {
227
+ /** Error message */
228
+ message: string;
229
+ /** HTTP status code */
230
+ status: number;
231
+ }
232
+ /**
233
+ * Iframe theme/mode
234
+ */
235
+ type IframeMode = 'light' | 'dark';
236
+ /**
237
+ * Style configuration for iframe ad rendering.
238
+ * Allows surfaces to match their host theme (e.g., VS Code color palette).
239
+ * If not provided, the iframe renders a default black/white theme based on mode.
240
+ */
241
+ interface IframeStyleConfig {
242
+ /** Background color (e.g., '#1a1a2e') */
243
+ background?: string;
244
+ /** Background hover color (e.g., '#252540') */
245
+ backgroundHover?: string;
246
+ /** Border color (e.g., '#333333') */
247
+ borderColor?: string;
248
+ /** Border radius (e.g., '12px') */
249
+ borderRadius?: string;
250
+ /** Button/CTA background color (e.g., '#4a9eff') */
251
+ buttonBackground?: string;
252
+ /** Button/CTA hover background color (e.g., '#3a8eef') */
253
+ buttonBackgroundHover?: string;
254
+ /** Button/CTA text color (e.g., '#ffffff') */
255
+ buttonTextColor?: string;
256
+ /** Primary text color (e.g., '#ffffff') */
257
+ textColor?: string;
258
+ }
259
+ /**
260
+ * Event passed to the onCtaClick callback when a user clicks the CTA
261
+ */
262
+ interface CtaClickEvent {
263
+ /** The click-through URL */
264
+ url: string;
265
+ /** The offer ID that was clicked */
266
+ offerId: string;
267
+ }
268
+ /**
269
+ * Options for rendering offers as iframes (browser only)
270
+ */
271
+ interface IframeRenderOptions {
272
+ /** Theme/mode (URL param: m) */
273
+ mode?: IframeMode;
274
+ /** Style configuration to match host surface theme */
275
+ style?: IframeStyleConfig;
276
+ /** CSS width value (e.g., '300px', '100%') */
277
+ width?: string;
278
+ /** CSS height value (e.g., '250px') */
279
+ height?: string;
280
+ /** Sandbox attributes for iframe */
281
+ sandbox?: string;
282
+ /** Base URL for iframe pages (default: 'https://ui.zero.click') */
283
+ baseUrl?: string;
284
+ /**
285
+ * Callback invoked when the user clicks the CTA in the iframe.
286
+ * If not provided, the click message is ignored (native navigation handles it).
287
+ *
288
+ * Use this in environments where popups are blocked (e.g., VS Code webviews):
289
+ * ```typescript
290
+ * onCtaClick: ({ url }) => vscode.env.openExternal(vscode.Uri.parse(url))
291
+ * ```
292
+ */
293
+ onCtaClick?: (event: CtaClickEvent) => void;
294
+ /** Enable auto-height: iframe reports its content height and the SDK adjusts the iframe element */
295
+ autoHeight?: boolean;
296
+ /** Minimum height in pixels when autoHeight is enabled (default: 50) */
297
+ minHeight?: number;
298
+ /** Maximum height in pixels when autoHeight is enabled */
299
+ maxHeight?: number;
300
+ }
301
+ /**
302
+ * Message type constant for offer data postMessage
303
+ */
304
+ declare const OFFER_DATA_MESSAGE_TYPE: "zeroclick:offer-data";
305
+ /**
306
+ * Message type constant for style update postMessage
307
+ */
308
+ declare const STYLE_UPDATE_MESSAGE_TYPE: "zeroclick:style-update";
309
+ /**
310
+ * Message type constant for CTA click postMessage (iframe → parent)
311
+ */
312
+ declare const CTA_CLICK_MESSAGE_TYPE: "zeroclick:cta-click";
313
+ /**
314
+ * Message type constant for resize postMessage (iframe → parent)
315
+ */
316
+ declare const RESIZE_MESSAGE_TYPE: "zeroclick:resize";
317
+ /**
318
+ * PostMessage payload sent from SDK to iframe on initial render
319
+ */
320
+ interface OfferDataMessage {
321
+ /** Message type identifier */
322
+ type: typeof OFFER_DATA_MESSAGE_TYPE;
323
+ /** Protocol version */
324
+ version: '1.0';
325
+ /** Full offer data */
326
+ offer: Offer;
327
+ /** Style configuration to match host surface theme */
328
+ style?: IframeStyleConfig;
329
+ /** Whether the iframe should report its content height for auto-sizing */
330
+ autoHeight?: boolean;
331
+ /** Message timestamp */
332
+ timestamp: number;
333
+ }
334
+ /**
335
+ * PostMessage payload sent from SDK to iframe on style update
336
+ */
337
+ interface StyleUpdateMessage {
338
+ /** Message type identifier */
339
+ type: typeof STYLE_UPDATE_MESSAGE_TYPE;
340
+ /** Protocol version */
341
+ version: '1.0';
342
+ /** Updated style configuration */
343
+ style: IframeStyleConfig;
344
+ /** Message timestamp */
345
+ timestamp: number;
346
+ }
347
+ /**
348
+ * PostMessage payload sent from iframe to parent when CTA is clicked
349
+ */
350
+ interface CtaClickMessage {
351
+ /** Message type identifier */
352
+ type: typeof CTA_CLICK_MESSAGE_TYPE;
353
+ /** Protocol version */
354
+ version: '1.0';
355
+ /** The click-through URL */
356
+ url: string;
357
+ /** The offer ID associated with this click */
358
+ offerId: string;
359
+ /** Message timestamp */
360
+ timestamp: number;
361
+ }
362
+ /**
363
+ * PostMessage payload sent from iframe to parent when content height changes
364
+ */
365
+ interface ResizeMessage {
366
+ /** Message type identifier */
367
+ type: typeof RESIZE_MESSAGE_TYPE;
368
+ /** Protocol version */
369
+ version: '1.0';
370
+ /** Content height in pixels */
371
+ height: number;
372
+ /** Message timestamp */
373
+ timestamp: number;
374
+ }
375
+ /**
376
+ * Handle returned by renderOffer for controlling a rendered iframe
377
+ */
378
+ interface RenderHandle {
379
+ /** Send updated style configuration to the iframe via postMessage */
380
+ updateStyle(style: IframeStyleConfig): void;
381
+ /** Remove the iframe from the DOM */
382
+ destroy(): void;
383
+ }
384
+ //#endregion
385
+ //#region src/client.d.ts
386
+ /**
387
+ * ZeroClick SDK client
388
+ *
389
+ * Provides methods for fetching offers (server-side) and tracking impressions (client-side).
390
+ * Must be initialized with `ZeroClick.initialize()` before use.
391
+ *
392
+ * @example
393
+ * ```typescript
394
+ * import { ZeroClick } from '@zeroclickai/offers-sdk';
395
+ *
396
+ * // Initialize once on server startup
397
+ * ZeroClick.initialize({
398
+ * apiKey: 'your-api-key',
399
+ * identity: {
400
+ * userId: 'user-123',
401
+ * userLocale: 'en-US',
402
+ * },
403
+ * });
404
+ *
405
+ * // In your API route handler (server-side only)
406
+ * app.post('/api/offers', async (req, res) => {
407
+ * const offers = await ZeroClick.getOffers({
408
+ * ipAddress: req.ip,
409
+ * userAgent: req.headers['user-agent'],
410
+ * query: 'running shoes',
411
+ * limit: 3
412
+ * });
413
+ * res.json(offers);
414
+ * });
415
+ *
416
+ * // Track impressions (can be called client-side)
417
+ * await ZeroClick.trackOfferImpressions(['offer-123']);
418
+ * ```
419
+ */
420
+ declare class ZeroClick {
421
+ private static config;
422
+ private static apiClient;
423
+ /**
424
+ * Initialize the ZeroClick SDK
425
+ *
426
+ * @param config - Configuration object containing optional API key and identity
427
+ */
428
+ static initialize(config?: ZeroClickConfig): void;
429
+ /**
430
+ * Check if the SDK has been initialized
431
+ */
432
+ static isInitialized(): boolean;
433
+ /**
434
+ * Reset the SDK state (useful for testing)
435
+ */
436
+ static reset(): void;
437
+ /**
438
+ * Get offers based on intent signals (server-side only)
439
+ *
440
+ * This method must be called from your backend server, not from the browser.
441
+ * Pass the client's IP address from the incoming request.
442
+ *
443
+ * @param options - Options for fetching offers (ipAddress is required)
444
+ * @returns Promise resolving to an array of Offer objects
445
+ * @throws Error if SDK is not initialized
446
+ * @throws Error if apiKey was not provided during initialization
447
+ *
448
+ * @example
449
+ * ```typescript
450
+ * // In your backend (Express, Next.js API route, etc.)
451
+ * app.post('/api/offers', async (req, res) => {
452
+ * const clientIp = req.ip || req.headers['x-forwarded-for'];
453
+ *
454
+ * const offers = await ZeroClick.getOffers({
455
+ * ipAddress: clientIp,
456
+ * userAgent: req.headers['user-agent'],
457
+ * query: req.body.query,
458
+ * limit: 3,
459
+ * });
460
+ *
461
+ * res.json(offers);
462
+ * });
463
+ * ```
464
+ */
465
+ static getOffers(options: GetOffersOptions): Promise<Offer[]>;
466
+ /**
467
+ * Track offer impressions
468
+ *
469
+ * @param ids - Array of offer IDs that were displayed to the user
470
+ * @throws Error if SDK is not initialized
471
+ * @throws Error if ids is empty
472
+ *
473
+ * @example
474
+ * ```typescript
475
+ * await ZeroClick.trackOfferImpressions(['offer-123', 'offer-456']);
476
+ * ```
477
+ */
478
+ static trackOfferImpressions(ids: string[]): Promise<void>;
479
+ /**
480
+ * Get iframe URL for an offer
481
+ *
482
+ * Returns the iframe source URL from the offer's UI configuration.
483
+ * Returns null if the offer doesn't have iframe rendering configured.
484
+ *
485
+ * @param offer - The offer to get the iframe URL for
486
+ * @returns The iframe URL string, or null if not available
487
+ *
488
+ * @example
489
+ * ```typescript
490
+ * const url = ZeroClick.getIframeUrl(offer);
491
+ * if (url) {
492
+ * // Use the iframe URL
493
+ * }
494
+ * ```
495
+ */
496
+ static getIframeUrl(offer: Offer): string | null;
497
+ /**
498
+ * Generate iframe HTML tag for an offer
499
+ *
500
+ * Returns null if the offer doesn't have iframe rendering configured.
501
+ *
502
+ * @param offer - The offer to generate an iframe tag for
503
+ * @param options - Optional rendering options (width, height, sandbox, etc.)
504
+ * @returns HTML string for the iframe element, or null if iframe URL not available
505
+ *
506
+ * @example
507
+ * ```typescript
508
+ * const html = ZeroClick.getIframeTag(offer, { width: '300px', height: '250px' });
509
+ * if (html) {
510
+ * container.innerHTML = html;
511
+ * }
512
+ * ```
513
+ */
514
+ static getIframeTag(offer: Offer, options?: IframeRenderOptions): string | null;
515
+ /**
516
+ * Render an offer into a container element (browser only)
517
+ *
518
+ * Creates an iframe, inserts it into the container, waits for it to load,
519
+ * then sends the offer data via postMessage. Returns a no-op cleanup function
520
+ * if the offer doesn't have iframe rendering configured.
521
+ *
522
+ * @param offer - The offer to render
523
+ * @param container - DOM element or CSS selector string for the container
524
+ * @param options - Optional rendering options (width, height, mode, tenant, etc.)
525
+ * @returns Promise resolving to a cleanup function that removes the iframe
526
+ * @throws Error if not in browser environment
527
+ * @throws Error if container element not found
528
+ *
529
+ * @example
530
+ * ```typescript
531
+ * // Render into a div with id="ad-container"
532
+ * const handle = await ZeroClick.renderOffer(offer, '#ad-container', {
533
+ * width: '300px',
534
+ * height: '250px',
535
+ * mode: 'dark',
536
+ * style: { background: '#1a1a2e', textColor: '#fff', buttonBackground: '#4a9eff' }
537
+ * });
538
+ *
539
+ * // Update style (e.g., on theme change)
540
+ * handle.updateStyle({ background: '#fff', textColor: '#000' });
541
+ *
542
+ * // Later, remove the iframe
543
+ * handle.destroy();
544
+ * ```
545
+ */
546
+ static renderOffer(offer: Offer, container: HTMLElement | string, options?: IframeRenderOptions): Promise<RenderHandle>;
547
+ /**
548
+ * Ensure the SDK has been initialized
549
+ * @throws Error if not initialized
550
+ */
551
+ private static ensureInitialized;
552
+ }
553
+ //#endregion
554
+ //#region src/api.d.ts
555
+ /**
556
+ * Custom error class for API errors
557
+ */
558
+ declare class ZeroClickApiError extends Error {
559
+ readonly status: number;
560
+ constructor(error: ApiError);
561
+ }
562
+ //#endregion
563
+ export { type ApiError, type Brand, CTA_CLICK_MESSAGE_TYPE, type Coordinates, type CtaClickEvent, type CtaClickMessage, type DistanceUnit, type GetOffersOptions, type Identity, type IframeMode, type IframeRenderOptions, type IframeStyleConfig, type Location, type Media, OFFER_DATA_MESSAGE_TYPE, type Offer, type OfferDataMessage, type OfferUI, type Price, type Product, type ProductAvailability, RESIZE_MESSAGE_TYPE, type Rating, type RenderHandle, type RequestContext, type ResizeMessage, STYLE_UPDATE_MESSAGE_TYPE, type StyleUpdateMessage, ZeroClick, ZeroClickApiError, type ZeroClickConfig };
564
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/client.ts","../src/api.ts"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ var ZeroClick=(function(e){function t(){if(typeof navigator<`u`&&navigator.userAgent)return navigator.userAgent}function n(e){let n={"Content-Type":`application/json`,"X-ZeroClick-SDK-Version":`0.1.0`};e&&(n[`x-zc-api-key`]=e);let r=t();return r&&(n[`X-ZeroClick-User-Agent`]=r),n}var r=class extends Error{constructor(e){super(e.message),this.name=`ZeroClickApiError`,this.status=e.status}};async function i(e){try{let t=await e.json();return{message:t.error?.message??t.message??`An unknown error occurred`,status:e.status}}catch{return{message:e.statusText||`An unknown error occurred`,status:e.status}}}function a(e){let{apiKey:t,baseUrl:a}=e;async function o(e,o,s){let c=`${a}${e}`,l=n(s?.authenticate??!0?t:void 0),u=await fetch(c,{method:`POST`,headers:l,body:JSON.stringify(o)});if(!u.ok)throw new r(await i(u));if(u.status!==204)return u.json()}return{post:o}}let o=`zeroclick:offer-data`,s=`zeroclick:style-update`,c=`zeroclick:cta-click`,l=`zeroclick:resize`;var u=class e{static initialize(t={}){e.config=t,e.apiClient=a({apiKey:t.apiKey,baseUrl:t.baseUrl??`https://zeroclick.dev`})}static isInitialized(){return e.config!==null&&e.apiClient!==null}static reset(){e.config=null,e.apiClient=null}static async getOffers(t){if(e.ensureInitialized(),!e.config.apiKey)throw Error(`ZeroClick: apiKey is required for getOffers`);let{ipAddress:n,limit:r=1,query:i,userAgent:a,origin:o}=t,{identity:s}=e.config,c={method:`server`,ipAddress:n,limit:r,query:i};return a&&(c.userAgent=a),o&&(c.origin=o),s&&(s.userId&&(c.userId=s.userId),s.userEmailSha256&&(c.userEmailSha256=s.userEmailSha256),s.userPhoneNumberSha256&&(c.userPhoneNumberSha256=s.userPhoneNumberSha256),s.userSessionId&&(c.userSessionId=s.userSessionId),s.userLocale&&(c.userLocale=s.userLocale),s.groupingId&&(c.groupingId=s.groupingId)),await e.apiClient.post(`/api/v2/offers`,c)??[]}static async trackOfferImpressions(t){if(e.ensureInitialized(),!t||t.length===0)throw Error(`ZeroClick: ids array cannot be empty`);await e.apiClient.post(`/api/v2/impressions`,{ids:t},{authenticate:!1})}static getIframeUrl(e){return e.ui?.type===`iframe`&&e.ui.url?e.ui.url:null}static getIframeTag(t,n){let r=e.getIframeUrl(t);if(!r)return null;let{width:i=`300px`,height:a=`250px`,sandbox:o=`allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox`,style:s}=n??{},c=s?.borderColor??`#e0e0e0`,l=s?.borderRadius??`12px`;return`<iframe
2
+ src="${r}"
3
+ width="${i}"
4
+ height="${a}"
5
+ sandbox="${o}"
6
+ frameborder="0"
7
+ scrolling="no"
8
+ loading="lazy"
9
+ data-zeroclick-offer="${t.id}"
10
+ style="background: transparent; border: 1px solid ${c}; border-radius: ${l}; display: block;"
11
+ ></iframe>`}static async renderOffer(t,n,r){if(e.ensureInitialized(),typeof window>`u`)throw Error(`ZeroClick: renderOffer() can only be called in browser environments`);let i=typeof n==`string`?document.querySelector(n):n;if(!i)throw Error(`ZeroClick: Container not found: ${n}`);let a={updateStyle(){},destroy(){}},u=e.getIframeUrl(t);if(!u)return a;let d=null;try{let n=e.getIframeTag(t,r);if(!n)return a;if(i.innerHTML=n,d=i.querySelector(`iframe`),!d)throw Error(`Failed to create iframe element`);await new Promise((e,t)=>{let n=setTimeout(()=>t(Error(`Iframe load timeout`)),5e3);d.addEventListener(`load`,()=>{clearTimeout(n),e()},{once:!0}),d.addEventListener(`error`,()=>{clearTimeout(n),t(Error(`Iframe load error`))},{once:!0})});let f=new URL(u).origin,p=new MessageChannel,m=r?.onCtaClick,h=r?.autoHeight??!1,g=r?.minHeight??50,_=r?.maxHeight,v=d;p.port1.onmessage=e=>{let t=e.data;if(t?.type===c&&t?.url){m&&m({url:t.url,offerId:t.offerId});return}if(h&&t?.type===l&&t?.height){let e=Math.max(g,Math.min(t.height,_??1/0));v.style.height=`${e}px`}};let y={type:o,version:`1.0`,offer:t,style:r?.style,autoHeight:h,timestamp:Date.now()};return d.contentWindow?.postMessage(y,f,[p.port2]),{updateStyle(e){let t={type:s,version:`1.0`,style:e,timestamp:Date.now()};p.port1.postMessage(t)},destroy(){p.port1.close(),v.remove()}}}catch(e){return console.error(`ZeroClick: Failed to load iframe:`,e),d&&d.remove(),i.innerHTML=``,a}}static ensureInitialized(){if(!e.isInitialized())throw Error(`ZeroClick: SDK not initialized. Call ZeroClick.initialize() first.`)}};return u.config=null,u.apiClient=null,e.CTA_CLICK_MESSAGE_TYPE=c,e.OFFER_DATA_MESSAGE_TYPE=o,e.RESIZE_MESSAGE_TYPE=l,e.STYLE_UPDATE_MESSAGE_TYPE=s,e.ZeroClick=u,e.ZeroClickApiError=r,e})({});
12
+ //# sourceMappingURL=index.iife.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.iife.js","names":[],"sources":["../src/utils.ts","../src/context.ts","../src/api.ts","../src/types.ts","../src/client.ts"],"sourcesContent":["// Ambient declarations for environment detection\ndeclare const browser: unknown;\ndeclare const chrome: { runtime?: { id?: string } };\ndeclare const process: { versions?: { node?: string } } | undefined;\n\n/**\n * Storage key for anonymous ID\n */\nconst ANONYMOUS_ID_KEY = 'zeroclick_anonymous_id';\n\n/**\n * In-memory session ID (not persisted across sessions)\n */\nlet sessionId: string | null = null;\n\n/**\n * In-memory storage fallback for non-browser environments\n */\nconst memoryStorage: Record<string, string> = {};\n\n/**\n * Check if localStorage is available\n */\nfunction isLocalStorageAvailable(): boolean {\n try {\n if (typeof window === 'undefined' || !window.localStorage) {\n return false;\n }\n const testKey = '__zeroclick_test__';\n window.localStorage.setItem(testKey, 'test');\n window.localStorage.removeItem(testKey);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Get a value from storage (localStorage or memory fallback)\n */\nexport function getStoredValue(key: string): string | null {\n if (isLocalStorageAvailable()) {\n return window.localStorage.getItem(key);\n }\n return memoryStorage[key] ?? null;\n}\n\n/**\n * Set a value in storage (localStorage or memory fallback)\n */\nexport function setStoredValue(key: string, value: string): void {\n if (isLocalStorageAvailable()) {\n window.localStorage.setItem(key, value);\n } else {\n memoryStorage[key] = value;\n }\n}\n\n/**\n * Generate a UUID v4\n */\nexport function generateUuid(): string {\n // Use crypto.randomUUID if available (modern browsers and Node 19+)\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n\n // Fallback to manual generation\n // eslint-disable-next-line no-bitwise\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0; // eslint-disable-line no-bitwise\n const v = c === 'x' ? r : (r & 0x3) | 0x8; // eslint-disable-line no-bitwise\n return v.toString(16);\n });\n}\n\n/**\n * Get or generate the anonymous ID for this client (persisted across sessions)\n */\nexport function getAnonymousId(): string {\n let id = getStoredValue(ANONYMOUS_ID_KEY);\n if (!id) {\n id = generateUuid();\n setStoredValue(ANONYMOUS_ID_KEY, id);\n }\n return id;\n}\n\n/**\n * Get or generate the session ID for this session (not persisted)\n */\nexport function getSessionId(): string {\n if (!sessionId) {\n sessionId = generateUuid();\n }\n return sessionId;\n}\n\n/**\n * Detect the current runtime environment\n */\nexport type RuntimeEnvironment = 'browser' | 'node' | 'extension' | 'unknown';\n\nexport function detectEnvironment(): RuntimeEnvironment {\n // Check for browser\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Check for extension environment (Chrome/Firefox)\n if ((typeof chrome !== 'undefined' && chrome.runtime?.id) || typeof browser !== 'undefined') {\n return 'extension';\n }\n return 'browser';\n }\n\n // Check for Node.js\n if (typeof process !== 'undefined' && process.versions?.node) {\n return 'node';\n }\n\n return 'unknown';\n}\n\n/**\n * Get the user agent string if available\n */\nexport function getUserAgent(): string | undefined {\n if (typeof navigator !== 'undefined' && navigator.userAgent) {\n return navigator.userAgent;\n }\n return undefined;\n}\n","import { getUserAgent } from './utils.js';\n\n/**\n * SDK version - will be replaced by build process or read from package\n */\nexport const SDK_VERSION = '0.1.0';\n\n/**\n * Build headers for API requests\n */\nexport function buildRequestHeaders(apiKey?: string): Record<string, string> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-ZeroClick-SDK-Version': SDK_VERSION,\n };\n\n if (apiKey) {\n headers['x-zc-api-key'] = apiKey;\n }\n\n const userAgent = getUserAgent();\n if (userAgent) {\n headers['X-ZeroClick-User-Agent'] = userAgent;\n }\n\n return headers;\n}\n","import { buildRequestHeaders } from './context.js';\nimport type { ApiError } from './types.js';\n\n/**\n * Default API base URL\n */\nexport const DEFAULT_BASE_URL = 'https://zeroclick.dev';\n\n/**\n * Custom error class for API errors\n */\nexport class ZeroClickApiError extends Error {\n public readonly status: number;\n\n constructor(error: ApiError) {\n super(error.message);\n this.name = 'ZeroClickApiError';\n this.status = error.status;\n }\n}\n\n/**\n * Parse error response from API\n *\n * Spec format: { error: { message: string } }\n */\nasync function parseErrorResponse(response: Response): Promise<ApiError> {\n try {\n const data = await response.json();\n return {\n message: data.error?.message ?? data.message ?? 'An unknown error occurred',\n status: response.status,\n };\n } catch {\n return {\n message: response.statusText || 'An unknown error occurred',\n status: response.status,\n };\n }\n}\n\n/**\n * API client configuration\n */\ninterface ApiClientConfig {\n apiKey?: string;\n baseUrl: string;\n}\n\n/**\n * Create an API client instance\n */\nexport function createApiClient(config: ApiClientConfig) {\n const { apiKey, baseUrl } = config;\n\n /**\n * Make a POST request to the API\n *\n * Returns parsed JSON for responses with content, or undefined for 204 No Content.\n */\n async function post<T = void>(endpoint: string, body: Record<string, unknown>, options?: { authenticate?: boolean }): Promise<T> {\n const url = `${baseUrl}${endpoint}`;\n const authenticate = options?.authenticate ?? true;\n const headers = buildRequestHeaders(authenticate ? apiKey : undefined);\n\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorData = await parseErrorResponse(response);\n throw new ZeroClickApiError(errorData);\n }\n\n // Handle 204 No Content (e.g., impression tracking)\n if (response.status === 204) {\n return undefined as T;\n }\n\n return response.json() as Promise<T>;\n }\n\n return { post };\n}\n\nexport type ApiClient = ReturnType<typeof createApiClient>;\n","/**\n * User identity context provided during initialization\n */\nexport interface Identity {\n /** Tenant client user ID */\n userId?: string;\n /** SHA-256 hash of user email address (lowercase, trimmed before hashing) */\n userEmailSha256?: string;\n /** SHA-256 hash of user phone number (E.164 format before hashing) */\n userPhoneNumberSha256?: string;\n /** User locale/language code (e.g., 'en-US') */\n userLocale?: string;\n /** Session identifier for session-level tracking */\n userSessionId?: string;\n /** Tenant-defined grouping ID for analytics segmentation */\n groupingId?: string;\n}\n\n/**\n * Configuration for initializing the ZeroClick SDK\n */\nexport interface ZeroClickConfig {\n /** ZeroClick Tenant Client API key (required for getOffers) */\n apiKey?: string;\n /** User identity context */\n identity?: Identity;\n /** Base URL for the API (defaults to production) */\n baseUrl?: string;\n}\n\n/**\n * Options for fetching offers (server-side only)\n */\nexport interface GetOffersOptions {\n /** Client IP address (required for server-to-server requests) */\n ipAddress: string;\n /** Search query for offers */\n query?: string;\n /** Maximum number of offers to return (default: 1) */\n limit?: number;\n /** Client user agent string */\n userAgent?: string;\n /** Client origin/referer URL */\n origin?: string;\n}\n\n/**\n * Product availability status\n */\nexport type ProductAvailability = 'in_stock' | 'limited' | 'out_of_stock';\n\n/**\n * Distance unit for location\n */\nexport type DistanceUnit = 'km' | 'mi';\n\n/**\n * Brand information for an offer\n */\nexport interface Brand {\n /** Name of the brand */\n name: string;\n /** Description of the brand */\n description: string | null;\n /** URL of the brand's website */\n url: string | null;\n /** URL of the brand's icon or logo */\n iconUrl: string | null;\n}\n\n/**\n * Product information for an offer\n */\nexport interface Product {\n /** Product ID */\n productId: string | null;\n /** Stock keeping unit */\n sku: string | null;\n /** Product title */\n title: string;\n /** Product description */\n description: string | null;\n /** Product category */\n category: string | null;\n /** Product subcategory */\n subcategory: string | null;\n /** Product image URL */\n image: string | null;\n /** Product availability status */\n availability: ProductAvailability | null;\n /** Additional product metadata */\n metadata: Record<string, unknown> | null;\n}\n\n/**\n * Pricing information for an offer\n */\nexport interface Price {\n /** Current price amount */\n amount: string | null;\n /** Currency code (e.g., 'USD') */\n currency: string | null;\n /** Original price before discounts */\n originalPrice: string | null;\n /** Discount amount or percentage */\n discount: string | null;\n /** Billing interval for subscriptions (e.g., 'monthly', 'yearly') */\n interval: string | null;\n}\n\n/**\n * Geographical coordinates\n */\nexport interface Coordinates {\n /** Latitude */\n lat: number;\n /** Longitude */\n lng: number;\n}\n\n/**\n * Location information for an offer\n */\nexport interface Location {\n /** Full address as plain text */\n text: string;\n /** Street address */\n address: string | null;\n /** City */\n city: string | null;\n /** State or region */\n state: string | null;\n /** ZIP or postal code */\n zip: number | null;\n /** Distance from user */\n distance: number | null;\n /** Unit of distance measurement */\n distanceUnit: DistanceUnit | null;\n /** Geographical coordinates */\n coordinates: Coordinates | null;\n /** Operating hours */\n hours: string | null;\n}\n\n/**\n * Media content information for an offer\n */\nexport interface Media {\n /** Content title */\n title: string | null;\n /** Content URL */\n url: string | null;\n /** Content description */\n description: string | null;\n /** Type of content (article, video, book, etc.) */\n contentType: string;\n}\n\n/**\n * Rating information for an offer\n */\nexport interface Rating {\n /** Rating value */\n value: number;\n /** Rating scale (e.g., 5 for 5-star, 100 for percentage) */\n scale: number;\n /** Number of ratings */\n count: number | null;\n}\n\n/**\n * UI rendering configuration returned by the API\n */\nexport interface OfferUI {\n /** Render type discriminator (e.g., 'iframe') */\n type: string;\n /** URL for the UI content (e.g., iframe src) */\n url: string;\n}\n\n/**\n * An offer returned from the ZeroClick API\n */\nexport interface Offer {\n /** Unique offer ID */\n id: string;\n /** Offer title */\n title: string | null;\n /** Offer subtitle */\n subtitle: string | null;\n /** Detailed content/description */\n content: string | null;\n /** Call to action text */\n cta: string | null;\n /** Click-through URL */\n clickUrl: string;\n /** Raw URL encoded (not for LLM use) */\n rawUrlEncoded: string | null;\n /** Offer image URL */\n imageUrl: string | null;\n /** Additional offer metadata */\n metadata: Record<string, unknown> | null;\n /** Additional context about the offer */\n context: string | null;\n /** Brand information */\n brand: Brand | null;\n /** Product information */\n product: Product | null;\n /** Pricing information */\n price: Price | null;\n /** Location information */\n location: Location | null;\n /** Media content information */\n media: Media | null;\n /** Rating information */\n rating: Rating | null;\n /** UI configuration for iframe rendering (optional) */\n ui?: OfferUI | null;\n}\n\n/**\n * Context collected and sent with each API request\n */\nexport interface RequestContext {\n /** Anonymous ID for user tracking (persisted across sessions) */\n anonymousId: string;\n /** Session ID for this specific session (not persisted) */\n sessionId: string;\n /** SDK version */\n sdkVersion: string;\n /** User agent string */\n userAgent?: string;\n /** Request timestamp (ISO 8601) */\n timestamp: string;\n}\n\n/**\n * API error response\n */\nexport interface ApiError {\n /** Error message */\n message: string;\n /** HTTP status code */\n status: number;\n}\n\n/**\n * SDK initialization state\n */\nexport interface SdkState {\n /** Whether the SDK has been initialized */\n initialized: boolean;\n /** Current configuration */\n config: ZeroClickConfig | null;\n /** Anonymous ID for this client */\n anonymousId: string | null;\n}\n\n/**\n * Iframe theme/mode\n */\nexport type IframeMode = 'light' | 'dark';\n\n/**\n * Style configuration for iframe ad rendering.\n * Allows surfaces to match their host theme (e.g., VS Code color palette).\n * If not provided, the iframe renders a default black/white theme based on mode.\n */\nexport interface IframeStyleConfig {\n /** Background color (e.g., '#1a1a2e') */\n background?: string;\n /** Background hover color (e.g., '#252540') */\n backgroundHover?: string;\n /** Border color (e.g., '#333333') */\n borderColor?: string;\n /** Border radius (e.g., '12px') */\n borderRadius?: string;\n /** Button/CTA background color (e.g., '#4a9eff') */\n buttonBackground?: string;\n /** Button/CTA hover background color (e.g., '#3a8eef') */\n buttonBackgroundHover?: string;\n /** Button/CTA text color (e.g., '#ffffff') */\n buttonTextColor?: string;\n /** Primary text color (e.g., '#ffffff') */\n textColor?: string;\n}\n\n/**\n * Event passed to the onCtaClick callback when a user clicks the CTA\n */\nexport interface CtaClickEvent {\n /** The click-through URL */\n url: string;\n /** The offer ID that was clicked */\n offerId: string;\n}\n\n/**\n * Options for rendering offers as iframes (browser only)\n */\nexport interface IframeRenderOptions {\n /** Theme/mode (URL param: m) */\n mode?: IframeMode;\n /** Style configuration to match host surface theme */\n style?: IframeStyleConfig;\n /** CSS width value (e.g., '300px', '100%') */\n width?: string;\n /** CSS height value (e.g., '250px') */\n height?: string;\n /** Sandbox attributes for iframe */\n sandbox?: string;\n /** Base URL for iframe pages (default: 'https://ui.zero.click') */\n baseUrl?: string;\n /**\n * Callback invoked when the user clicks the CTA in the iframe.\n * If not provided, the click message is ignored (native navigation handles it).\n *\n * Use this in environments where popups are blocked (e.g., VS Code webviews):\n * ```typescript\n * onCtaClick: ({ url }) => vscode.env.openExternal(vscode.Uri.parse(url))\n * ```\n */\n onCtaClick?: (event: CtaClickEvent) => void;\n /** Enable auto-height: iframe reports its content height and the SDK adjusts the iframe element */\n autoHeight?: boolean;\n /** Minimum height in pixels when autoHeight is enabled (default: 50) */\n minHeight?: number;\n /** Maximum height in pixels when autoHeight is enabled */\n maxHeight?: number;\n}\n\n/**\n * Message type constant for offer data postMessage\n */\nexport const OFFER_DATA_MESSAGE_TYPE = 'zeroclick:offer-data' as const;\n\n/**\n * Message type constant for style update postMessage\n */\nexport const STYLE_UPDATE_MESSAGE_TYPE = 'zeroclick:style-update' as const;\n\n/**\n * Message type constant for CTA click postMessage (iframe → parent)\n */\nexport const CTA_CLICK_MESSAGE_TYPE = 'zeroclick:cta-click' as const;\n\n/**\n * Message type constant for resize postMessage (iframe → parent)\n */\nexport const RESIZE_MESSAGE_TYPE = 'zeroclick:resize' as const;\n\n/**\n * PostMessage payload sent from SDK to iframe on initial render\n */\nexport interface OfferDataMessage {\n /** Message type identifier */\n type: typeof OFFER_DATA_MESSAGE_TYPE;\n /** Protocol version */\n version: '1.0';\n /** Full offer data */\n offer: Offer;\n /** Style configuration to match host surface theme */\n style?: IframeStyleConfig;\n /** Whether the iframe should report its content height for auto-sizing */\n autoHeight?: boolean;\n /** Message timestamp */\n timestamp: number;\n}\n\n/**\n * PostMessage payload sent from SDK to iframe on style update\n */\nexport interface StyleUpdateMessage {\n /** Message type identifier */\n type: typeof STYLE_UPDATE_MESSAGE_TYPE;\n /** Protocol version */\n version: '1.0';\n /** Updated style configuration */\n style: IframeStyleConfig;\n /** Message timestamp */\n timestamp: number;\n}\n\n/**\n * PostMessage payload sent from iframe to parent when CTA is clicked\n */\nexport interface CtaClickMessage {\n /** Message type identifier */\n type: typeof CTA_CLICK_MESSAGE_TYPE;\n /** Protocol version */\n version: '1.0';\n /** The click-through URL */\n url: string;\n /** The offer ID associated with this click */\n offerId: string;\n /** Message timestamp */\n timestamp: number;\n}\n\n/**\n * PostMessage payload sent from iframe to parent when content height changes\n */\nexport interface ResizeMessage {\n /** Message type identifier */\n type: typeof RESIZE_MESSAGE_TYPE;\n /** Protocol version */\n version: '1.0';\n /** Content height in pixels */\n height: number;\n /** Message timestamp */\n timestamp: number;\n}\n\n/**\n * Handle returned by renderOffer for controlling a rendered iframe\n */\nexport interface RenderHandle {\n /** Send updated style configuration to the iframe via postMessage */\n updateStyle(style: IframeStyleConfig): void;\n /** Remove the iframe from the DOM */\n destroy(): void;\n}\n","import { type ApiClient, createApiClient, DEFAULT_BASE_URL } from './api.js';\nimport type {\n GetOffersOptions,\n IframeRenderOptions,\n IframeStyleConfig,\n Offer,\n OfferDataMessage,\n RenderHandle,\n StyleUpdateMessage,\n ZeroClickConfig,\n} from './types.js';\nimport { CTA_CLICK_MESSAGE_TYPE, OFFER_DATA_MESSAGE_TYPE, RESIZE_MESSAGE_TYPE, STYLE_UPDATE_MESSAGE_TYPE } from './types.js';\n\n/**\n * ZeroClick SDK client\n *\n * Provides methods for fetching offers (server-side) and tracking impressions (client-side).\n * Must be initialized with `ZeroClick.initialize()` before use.\n *\n * @example\n * ```typescript\n * import { ZeroClick } from '@zeroclickai/offers-sdk';\n *\n * // Initialize once on server startup\n * ZeroClick.initialize({\n * apiKey: 'your-api-key',\n * identity: {\n * userId: 'user-123',\n * userLocale: 'en-US',\n * },\n * });\n *\n * // In your API route handler (server-side only)\n * app.post('/api/offers', async (req, res) => {\n * const offers = await ZeroClick.getOffers({\n * ipAddress: req.ip,\n * userAgent: req.headers['user-agent'],\n * query: 'running shoes',\n * limit: 3\n * });\n * res.json(offers);\n * });\n *\n * // Track impressions (can be called client-side)\n * await ZeroClick.trackOfferImpressions(['offer-123']);\n * ```\n */\nexport class ZeroClick {\n private static config: ZeroClickConfig | null = null;\n\n private static apiClient: ApiClient | null = null;\n\n /**\n * Initialize the ZeroClick SDK\n *\n * @param config - Configuration object containing optional API key and identity\n */\n static initialize(config: ZeroClickConfig = {}): void {\n ZeroClick.config = config;\n ZeroClick.apiClient = createApiClient({\n apiKey: config.apiKey,\n baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,\n });\n }\n\n /**\n * Check if the SDK has been initialized\n */\n static isInitialized(): boolean {\n return ZeroClick.config !== null && ZeroClick.apiClient !== null;\n }\n\n /**\n * Reset the SDK state (useful for testing)\n */\n static reset(): void {\n ZeroClick.config = null;\n ZeroClick.apiClient = null;\n }\n\n /**\n * Get offers based on intent signals (server-side only)\n *\n * This method must be called from your backend server, not from the browser.\n * Pass the client's IP address from the incoming request.\n *\n * @param options - Options for fetching offers (ipAddress is required)\n * @returns Promise resolving to an array of Offer objects\n * @throws Error if SDK is not initialized\n * @throws Error if apiKey was not provided during initialization\n *\n * @example\n * ```typescript\n * // In your backend (Express, Next.js API route, etc.)\n * app.post('/api/offers', async (req, res) => {\n * const clientIp = req.ip || req.headers['x-forwarded-for'];\n *\n * const offers = await ZeroClick.getOffers({\n * ipAddress: clientIp,\n * userAgent: req.headers['user-agent'],\n * query: req.body.query,\n * limit: 3,\n * });\n *\n * res.json(offers);\n * });\n * ```\n */\n static async getOffers(options: GetOffersOptions): Promise<Offer[]> {\n ZeroClick.ensureInitialized();\n\n if (!ZeroClick.config!.apiKey) {\n throw new Error('ZeroClick: apiKey is required for getOffers');\n }\n\n const { ipAddress, limit = 1, query, userAgent, origin } = options;\n const { identity } = ZeroClick.config!;\n\n const body: Record<string, unknown> = {\n method: 'server',\n ipAddress,\n limit,\n query,\n };\n\n // Add optional client context\n if (userAgent) body.userAgent = userAgent;\n if (origin) body.origin = origin;\n\n // Flatten identity fields into the request body\n if (identity) {\n if (identity.userId) body.userId = identity.userId;\n if (identity.userEmailSha256) body.userEmailSha256 = identity.userEmailSha256;\n if (identity.userPhoneNumberSha256) body.userPhoneNumberSha256 = identity.userPhoneNumberSha256;\n if (identity.userSessionId) body.userSessionId = identity.userSessionId;\n if (identity.userLocale) body.userLocale = identity.userLocale;\n if (identity.groupingId) body.groupingId = identity.groupingId;\n }\n\n const response = await ZeroClick.apiClient!.post<Offer[]>('/api/v2/offers', body);\n\n return response ?? [];\n }\n\n /**\n * Track offer impressions\n *\n * @param ids - Array of offer IDs that were displayed to the user\n * @throws Error if SDK is not initialized\n * @throws Error if ids is empty\n *\n * @example\n * ```typescript\n * await ZeroClick.trackOfferImpressions(['offer-123', 'offer-456']);\n * ```\n */\n static async trackOfferImpressions(ids: string[]): Promise<void> {\n ZeroClick.ensureInitialized();\n\n if (!ids || ids.length === 0) {\n throw new Error('ZeroClick: ids array cannot be empty');\n }\n\n await ZeroClick.apiClient!.post('/api/v2/impressions', { ids }, { authenticate: false });\n }\n\n /**\n * Get iframe URL for an offer\n *\n * Returns the iframe source URL from the offer's UI configuration.\n * Returns null if the offer doesn't have iframe rendering configured.\n *\n * @param offer - The offer to get the iframe URL for\n * @returns The iframe URL string, or null if not available\n *\n * @example\n * ```typescript\n * const url = ZeroClick.getIframeUrl(offer);\n * if (url) {\n * // Use the iframe URL\n * }\n * ```\n */\n static getIframeUrl(offer: Offer): string | null {\n if (offer.ui?.type === 'iframe' && offer.ui.url) {\n return offer.ui.url;\n }\n return null;\n }\n\n /**\n * Generate iframe HTML tag for an offer\n *\n * Returns null if the offer doesn't have iframe rendering configured.\n *\n * @param offer - The offer to generate an iframe tag for\n * @param options - Optional rendering options (width, height, sandbox, etc.)\n * @returns HTML string for the iframe element, or null if iframe URL not available\n *\n * @example\n * ```typescript\n * const html = ZeroClick.getIframeTag(offer, { width: '300px', height: '250px' });\n * if (html) {\n * container.innerHTML = html;\n * }\n * ```\n */\n static getIframeTag(offer: Offer, options?: IframeRenderOptions): string | null {\n const url = ZeroClick.getIframeUrl(offer);\n if (!url) {\n return null;\n }\n\n const {\n width = '300px',\n height = '250px',\n sandbox = 'allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox',\n style,\n } = options ?? {};\n\n const borderColor = style?.borderColor ?? '#e0e0e0';\n const borderRadius = style?.borderRadius ?? '12px';\n\n return `<iframe\n src=\"${url}\"\n width=\"${width}\"\n height=\"${height}\"\n sandbox=\"${sandbox}\"\n frameborder=\"0\"\n scrolling=\"no\"\n loading=\"lazy\"\n data-zeroclick-offer=\"${offer.id}\"\n style=\"background: transparent; border: 1px solid ${borderColor}; border-radius: ${borderRadius}; display: block;\"\n ></iframe>`;\n }\n\n /**\n * Render an offer into a container element (browser only)\n *\n * Creates an iframe, inserts it into the container, waits for it to load,\n * then sends the offer data via postMessage. Returns a no-op cleanup function\n * if the offer doesn't have iframe rendering configured.\n *\n * @param offer - The offer to render\n * @param container - DOM element or CSS selector string for the container\n * @param options - Optional rendering options (width, height, mode, tenant, etc.)\n * @returns Promise resolving to a cleanup function that removes the iframe\n * @throws Error if not in browser environment\n * @throws Error if container element not found\n *\n * @example\n * ```typescript\n * // Render into a div with id=\"ad-container\"\n * const handle = await ZeroClick.renderOffer(offer, '#ad-container', {\n * width: '300px',\n * height: '250px',\n * mode: 'dark',\n * style: { background: '#1a1a2e', textColor: '#fff', buttonBackground: '#4a9eff' }\n * });\n *\n * // Update style (e.g., on theme change)\n * handle.updateStyle({ background: '#fff', textColor: '#000' });\n *\n * // Later, remove the iframe\n * handle.destroy();\n * ```\n */\n static async renderOffer(offer: Offer, container: HTMLElement | string, options?: IframeRenderOptions): Promise<RenderHandle> {\n ZeroClick.ensureInitialized();\n\n if (typeof window === 'undefined') {\n throw new Error('ZeroClick: renderOffer() can only be called in browser environments');\n }\n\n // Resolve container\n const containerEl = typeof container === 'string' ? document.querySelector<HTMLElement>(container) : container;\n\n if (!containerEl) {\n throw new Error(`ZeroClick: Container not found: ${container}`);\n }\n\n const noopHandle: RenderHandle = {\n updateStyle() {},\n destroy() {},\n };\n\n // Check if offer has iframe URL configured\n const url = ZeroClick.getIframeUrl(offer);\n if (!url) {\n return noopHandle;\n }\n\n let iframe: HTMLIFrameElement | null = null;\n\n try {\n // Create iframe\n const iframeHtml = ZeroClick.getIframeTag(offer, options);\n if (!iframeHtml) {\n return noopHandle;\n }\n\n containerEl.innerHTML = iframeHtml;\n iframe = containerEl.querySelector('iframe');\n\n if (!iframe) {\n throw new Error('Failed to create iframe element');\n }\n\n // Wait for iframe load with timeout\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => reject(new Error('Iframe load timeout')), 5000);\n\n const handleLoad = () => {\n clearTimeout(timeout);\n resolve();\n };\n\n const handleError = () => {\n clearTimeout(timeout);\n reject(new Error('Iframe load error'));\n };\n\n iframe!.addEventListener('load', handleLoad, { once: true });\n iframe!.addEventListener('error', handleError, { once: true });\n });\n\n // Create a dedicated MessageChannel for this render instance\n const iframeOrigin = new URL(url).origin;\n const channel = new MessageChannel();\n\n // Listen for messages from the iframe on our end of the channel\n const onCtaClick = options?.onCtaClick;\n const autoHeight = options?.autoHeight ?? false;\n const minHeight = options?.minHeight ?? 50;\n const maxHeight = options?.maxHeight;\n const iframeRef = iframe;\n\n channel.port1.onmessage = (event: MessageEvent) => {\n const msg = event.data;\n\n // Handle CTA clicks\n if (msg?.type === CTA_CLICK_MESSAGE_TYPE && msg?.url) {\n if (onCtaClick) {\n onCtaClick({ url: msg.url, offerId: msg.offerId });\n }\n return;\n }\n\n // Handle resize (auto-height)\n if (autoHeight && msg?.type === RESIZE_MESSAGE_TYPE && msg?.height) {\n const h = Math.max(minHeight, Math.min(msg.height, maxHeight ?? Infinity));\n iframeRef.style.height = `${h}px`;\n }\n };\n\n // Send offer data and transfer port2 to the iframe\n const message: OfferDataMessage = {\n type: OFFER_DATA_MESSAGE_TYPE,\n version: '1.0',\n offer,\n style: options?.style,\n autoHeight,\n timestamp: Date.now(),\n };\n\n iframe.contentWindow?.postMessage(message, iframeOrigin, [channel.port2]);\n\n // Return handle for ongoing control\n return {\n updateStyle(style: IframeStyleConfig) {\n const msg: StyleUpdateMessage = {\n type: STYLE_UPDATE_MESSAGE_TYPE,\n version: '1.0',\n style,\n timestamp: Date.now(),\n };\n channel.port1.postMessage(msg);\n },\n destroy() {\n channel.port1.close();\n iframeRef.remove();\n },\n };\n } catch (error) {\n // eslint-disable-next-line no-console\n console.error('ZeroClick: Failed to load iframe:', error);\n\n // TODO: Track render failure (potential ad blocking or network issues)\n // when /api/v2/events endpoint is implemented\n\n // Clean up failed iframe\n if (iframe) {\n iframe.remove();\n }\n containerEl.innerHTML = ''; // Clear container (show nothing)\n\n return noopHandle;\n }\n }\n\n /**\n * Ensure the SDK has been initialized\n * @throws Error if not initialized\n */\n private static ensureInitialized(): void {\n if (!ZeroClick.isInitialized()) {\n throw new Error('ZeroClick: SDK not initialized. Call ZeroClick.initialize() first.');\n }\n }\n}\n"],"mappings":"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"}