@adxensor/publisher-sdk 1.0.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,487 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defProps = Object.defineProperties;
3
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __spreadValues = (a, b) => {
9
+ for (var prop in b || (b = {}))
10
+ if (__hasOwnProp.call(b, prop))
11
+ __defNormalProp(a, prop, b[prop]);
12
+ if (__getOwnPropSymbols)
13
+ for (var prop of __getOwnPropSymbols(b)) {
14
+ if (__propIsEnum.call(b, prop))
15
+ __defNormalProp(a, prop, b[prop]);
16
+ }
17
+ return a;
18
+ };
19
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
+
21
+ // src/core/api.ts
22
+ var AdXensorApi = class {
23
+ constructor(baseUrl, apiKey) {
24
+ this.baseUrl = baseUrl;
25
+ this.apiKey = apiKey;
26
+ }
27
+ headers() {
28
+ const h = { "Content-Type": "application/json" };
29
+ if (this.apiKey) h["X-Publisher-Key"] = this.apiKey;
30
+ return h;
31
+ }
32
+ // ─── Fetch the best ad for a slot ─────────────────────────────────────────
33
+ async serveAd(req) {
34
+ var _a, _b;
35
+ try {
36
+ const params = new URLSearchParams({ zone: req.size, site: req.siteId });
37
+ if (req.url) params.set("url", req.url);
38
+ if (req.referrer) params.set("ref", req.referrer);
39
+ const res = await fetch(`${this.baseUrl}/ad?${params.toString()}`, {
40
+ method: "GET",
41
+ headers: this.headers()
42
+ });
43
+ if (res.status === 204 || !res.ok) return null;
44
+ const data = await res.json();
45
+ const eventId = (_a = data.impression_url.split("/").pop()) != null ? _a : "";
46
+ const [w, h] = ((_b = data.size) != null ? _b : "300x250").split("x").map(Number);
47
+ return {
48
+ adId: eventId,
49
+ campaignId: data.campaign_id,
50
+ type: "image",
51
+ imageUrl: data.creative_url,
52
+ clickHref: data.click_url,
53
+ // full URL — use directly as <a> href
54
+ width: w != null ? w : 300,
55
+ height: h != null ? h : 250,
56
+ altText: "",
57
+ impressionToken: eventId,
58
+ clickToken: eventId
59
+ };
60
+ } catch (e) {
61
+ return null;
62
+ }
63
+ }
64
+ // ─── Fire-and-forget event (uses sendBeacon when available) ───────────────
65
+ sendEvent(path, payload) {
66
+ const url = `${this.baseUrl}/events/${path}`;
67
+ const body = JSON.stringify(payload);
68
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
69
+ navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
70
+ } else {
71
+ fetch(url, {
72
+ method: "POST",
73
+ headers: this.headers(),
74
+ body,
75
+ keepalive: true
76
+ }).catch(() => {
77
+ });
78
+ }
79
+ }
80
+ };
81
+
82
+ // src/core/session.ts
83
+ var SESSION_KEY = "_adx_sid";
84
+ var FIRED_KEY = "_adx_fired";
85
+ function getSessionId() {
86
+ try {
87
+ let id = sessionStorage.getItem(SESSION_KEY);
88
+ if (!id) {
89
+ id = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2) + Date.now().toString(36);
90
+ sessionStorage.setItem(SESSION_KEY, id);
91
+ }
92
+ return id;
93
+ } catch (e) {
94
+ return Math.random().toString(36).slice(2);
95
+ }
96
+ }
97
+ function hasFired(key) {
98
+ try {
99
+ const map = JSON.parse(sessionStorage.getItem(FIRED_KEY) || "{}");
100
+ return key in map;
101
+ } catch (e) {
102
+ return false;
103
+ }
104
+ }
105
+ function markFired(key) {
106
+ try {
107
+ const map = JSON.parse(sessionStorage.getItem(FIRED_KEY) || "{}");
108
+ map[key] = Date.now();
109
+ sessionStorage.setItem(FIRED_KEY, JSON.stringify(map));
110
+ } catch (e) {
111
+ }
112
+ }
113
+
114
+ // src/core/device.ts
115
+ function getDevice() {
116
+ if (typeof window === "undefined") return "desktop";
117
+ const w = window.innerWidth;
118
+ if (w < 768) return "mobile";
119
+ if (w < 1024) return "tablet";
120
+ return "desktop";
121
+ }
122
+ function resolveSize(requested, container) {
123
+ if (requested !== "auto") return requested;
124
+ const device = getDevice();
125
+ const w = container.offsetWidth || window.innerWidth;
126
+ if (device === "mobile") {
127
+ return w >= 320 ? "320x50" : "300x250";
128
+ }
129
+ if (device === "tablet") {
130
+ return w >= 468 ? "468x60" : "300x250";
131
+ }
132
+ if (w >= 728) return "728x90";
133
+ if (w >= 468) return "468x60";
134
+ return "300x250";
135
+ }
136
+
137
+ // src/core/tracker.ts
138
+ function getLang() {
139
+ var _a;
140
+ return ((_a = navigator.language) != null ? _a : "fr").split("-")[0].toLowerCase();
141
+ }
142
+ var Tracker = class {
143
+ constructor(api, siteId, sessionId) {
144
+ this.api = api;
145
+ this.siteId = siteId;
146
+ this.sessionId = sessionId;
147
+ }
148
+ // ─── Base context — attached to every event ────────────────────────────────
149
+ base() {
150
+ return {
151
+ siteId: this.siteId,
152
+ url: location.href,
153
+ device: getDevice(),
154
+ sessionId: this.sessionId,
155
+ language: getLang(),
156
+ // "fr" | "en" | "pt" | …
157
+ screenWidth: screen.width,
158
+ // for device analytics breakdowns
159
+ ts: Date.now()
160
+ };
161
+ }
162
+ // ─── Pageview: once per page load ─────────────────────────────────────────
163
+ pageview() {
164
+ this.api.sendEvent("pageview", __spreadProps(__spreadValues({}, this.base()), {
165
+ referrer: document.referrer
166
+ }));
167
+ }
168
+ // ─── Impression: once per adId × session ──────────────────────────────────
169
+ impression(adId, slotId, impressionToken) {
170
+ const key = `imp:${adId}:${this.sessionId}`;
171
+ if (hasFired(key)) return;
172
+ markFired(key);
173
+ this.api.sendEvent("impression", __spreadProps(__spreadValues({}, this.base()), {
174
+ adId,
175
+ slotId,
176
+ impressionToken
177
+ }));
178
+ }
179
+ // ─── Click: deduplicated within 500 ms window per ad ─────────────────────
180
+ click(adId, slotId, clickToken) {
181
+ const tsKey = `clk_ts:${adId}`;
182
+ try {
183
+ const last = parseInt(sessionStorage.getItem(tsKey) || "0", 10);
184
+ if (Date.now() - last < 500) return;
185
+ sessionStorage.setItem(tsKey, String(Date.now()));
186
+ } catch (e) {
187
+ }
188
+ this.api.sendEvent("click", __spreadProps(__spreadValues({}, this.base()), {
189
+ adId,
190
+ slotId,
191
+ clickToken
192
+ }));
193
+ }
194
+ // ─── View: once per adId × session (50% visible for ≥1 s) ────────────────
195
+ view(adId, slotId) {
196
+ const key = `view:${adId}:${this.sessionId}`;
197
+ if (hasFired(key)) return;
198
+ markFired(key);
199
+ this.api.sendEvent("view", __spreadProps(__spreadValues({}, this.base()), {
200
+ adId,
201
+ slotId
202
+ }));
203
+ }
204
+ };
205
+
206
+ // src/core/renderer.ts
207
+ function renderAd(container, ad, clickHref) {
208
+ var _a;
209
+ container.style.overflow = "hidden";
210
+ container.style.display = "block";
211
+ container.style.lineHeight = "0";
212
+ if (ad.type === "html" && ad.htmlContent) {
213
+ container.innerHTML = ad.htmlContent;
214
+ return;
215
+ }
216
+ const a = document.createElement("a");
217
+ a.href = clickHref;
218
+ a.target = "_blank";
219
+ a.rel = "noopener noreferrer";
220
+ a.setAttribute("data-adx-click", "1");
221
+ a.style.display = "block";
222
+ const img = document.createElement("img");
223
+ img.src = (_a = ad.imageUrl) != null ? _a : "";
224
+ img.width = ad.width;
225
+ img.height = ad.height;
226
+ img.alt = ad.altText || "";
227
+ img.style.display = "block";
228
+ img.style.maxWidth = "100%";
229
+ img.loading = "lazy";
230
+ a.appendChild(img);
231
+ container.innerHTML = "";
232
+ container.appendChild(a);
233
+ }
234
+
235
+ // src/core/AdSlot.ts
236
+ var VIEW_THRESHOLD = 0.5;
237
+ var VIEW_DURATION = 1e3;
238
+ var AdSlot = class {
239
+ constructor(el, api, tracker, siteId, sessionId, options = {}) {
240
+ this.el = el;
241
+ this.api = api;
242
+ this.tracker = tracker;
243
+ this.siteId = siteId;
244
+ this.sessionId = sessionId;
245
+ this.options = options;
246
+ this.observer = null;
247
+ this.viewTimer = null;
248
+ var _a, _b;
249
+ this.slotId = (_b = (_a = options.slotId) != null ? _a : el.dataset.adSlot) != null ? _b : (
250
+ // data-ad-slot="banner-top"
251
+ el.id || `adx-${Math.random().toString(36).slice(2, 9)}`
252
+ );
253
+ }
254
+ // ─── Load entry point ─────────────────────────────────────────────────────
255
+ load(lazy = true) {
256
+ if (this.el.dataset.adxState) return;
257
+ if (lazy && typeof IntersectionObserver !== "undefined") {
258
+ this.watchForEntry();
259
+ } else {
260
+ void this.fetch();
261
+ }
262
+ }
263
+ // ─── Lazy entry: wait until element is near the viewport ─────────────────
264
+ watchForEntry() {
265
+ const io = new IntersectionObserver(
266
+ (entries) => {
267
+ var _a;
268
+ if ((_a = entries[0]) == null ? void 0 : _a.isIntersecting) {
269
+ io.disconnect();
270
+ void this.fetch();
271
+ }
272
+ },
273
+ { rootMargin: "200px", threshold: 0 }
274
+ );
275
+ io.observe(this.el);
276
+ }
277
+ // ─── Fetch ad from API ────────────────────────────────────────────────────
278
+ async fetch() {
279
+ var _a, _b, _c;
280
+ if (this.el.dataset.adxState) return;
281
+ this.setState("loading");
282
+ try {
283
+ const size = resolveSize(
284
+ (_b = (_a = this.options.format) != null ? _a : this.el.dataset.adFormat) != null ? _b : "auto",
285
+ this.el
286
+ );
287
+ const [w, h] = size.split("x").map(Number);
288
+ if (w && h) {
289
+ this.el.style.width = `${w}px`;
290
+ this.el.style.maxWidth = "100%";
291
+ this.el.style.minHeight = `${h}px`;
292
+ }
293
+ const ad = await this.api.serveAd({
294
+ siteId: this.siteId,
295
+ slotId: this.slotId,
296
+ size,
297
+ device: getDevice(),
298
+ url: location.href,
299
+ referrer: document.referrer,
300
+ // Normalise "fr-FR" → "fr"; server resolves country from IP separately
301
+ language: ((_c = navigator.language) != null ? _c : "fr").split("-")[0].toLowerCase(),
302
+ screenWidth: screen.width,
303
+ sessionId: this.sessionId
304
+ });
305
+ if (!ad) {
306
+ this.setState("empty");
307
+ this.el.style.display = "none";
308
+ return;
309
+ }
310
+ renderAd(this.el, ad, ad.clickHref);
311
+ this.setState("filled");
312
+ this.el.addEventListener("click", (e) => {
313
+ if (e.target.closest("[data-adx-click]")) {
314
+ this.tracker.click(ad.adId, this.slotId, ad.clickToken);
315
+ }
316
+ }, { passive: true });
317
+ this.tracker.impression(ad.adId, this.slotId, ad.impressionToken);
318
+ this.watchVisibility(ad.adId, this.slotId);
319
+ } catch (e) {
320
+ this.setState("error");
321
+ this.el.style.display = "none";
322
+ }
323
+ }
324
+ // ─── Viewability (IAB MRC standard) ──────────────────────────────────────
325
+ watchVisibility(adId, slotId) {
326
+ if (typeof IntersectionObserver === "undefined") {
327
+ this.tracker.view(adId, slotId);
328
+ return;
329
+ }
330
+ this.observer = new IntersectionObserver(
331
+ ([entry]) => {
332
+ if (!entry) return;
333
+ if (entry.intersectionRatio >= VIEW_THRESHOLD) {
334
+ if (this.viewTimer === null) {
335
+ this.viewTimer = setTimeout(() => {
336
+ this.tracker.view(adId, slotId);
337
+ this.disconnect();
338
+ }, VIEW_DURATION);
339
+ }
340
+ } else {
341
+ if (this.viewTimer !== null) {
342
+ clearTimeout(this.viewTimer);
343
+ this.viewTimer = null;
344
+ }
345
+ }
346
+ },
347
+ { threshold: [VIEW_THRESHOLD] }
348
+ );
349
+ this.observer.observe(this.el);
350
+ }
351
+ // ─── State machine ────────────────────────────────────────────────────────
352
+ setState(state) {
353
+ this.el.dataset.adxState = state;
354
+ }
355
+ // ─── Cleanup ──────────────────────────────────────────────────────────────
356
+ disconnect() {
357
+ var _a;
358
+ (_a = this.observer) == null ? void 0 : _a.disconnect();
359
+ this.observer = null;
360
+ if (this.viewTimer !== null) {
361
+ clearTimeout(this.viewTimer);
362
+ this.viewTimer = null;
363
+ }
364
+ }
365
+ };
366
+
367
+ // src/core/AdXensor.ts
368
+ var DEFAULT_API_URL = "https://core.adxensor.com/v1";
369
+ var INS_SELECTOR = "ins.adxensor:not([data-adx-state])";
370
+ var _AdXensor = class _AdXensor {
371
+ constructor(config) {
372
+ this.initialized = false;
373
+ this.domObserver = null;
374
+ var _a;
375
+ this.config = config;
376
+ this.sessionId = getSessionId();
377
+ this.api = new AdXensorApi(
378
+ (_a = config.apiUrl) != null ? _a : DEFAULT_API_URL,
379
+ config.apiKey
380
+ );
381
+ this.tracker = new Tracker(this.api, config.siteId, this.sessionId);
382
+ }
383
+ // ─── Singleton helpers ────────────────────────────────────────────────────
384
+ static getInstance(config) {
385
+ if (!_AdXensor.instance) _AdXensor.instance = new _AdXensor(config);
386
+ return _AdXensor.instance;
387
+ }
388
+ static reset() {
389
+ var _a;
390
+ (_a = _AdXensor.instance) == null ? void 0 : _a.destroy();
391
+ _AdXensor.instance = null;
392
+ }
393
+ // ─── Public API ───────────────────────────────────────────────────────────
394
+ /**
395
+ * Idempotent init — safe to call multiple times.
396
+ * Fires pageview, fills existing slots, watches DOM for new ones.
397
+ */
398
+ init() {
399
+ if (this.initialized) return this;
400
+ this.initialized = true;
401
+ this.tracker.pageview();
402
+ this.fillAll();
403
+ this.watchDom();
404
+ return this;
405
+ }
406
+ /** Fill every unfilled <ins class="adxensor"> currently in the DOM. */
407
+ fillAll() {
408
+ document.querySelectorAll(INS_SELECTOR).forEach((el) => this.fill(el));
409
+ }
410
+ /**
411
+ * AdSense-style push — fills the next unfilled slot in DOM order.
412
+ * Use alongside <script>(window.adxensor = window.adxensor || []).push({})<\/script>
413
+ */
414
+ push(options = {}) {
415
+ const el = document.querySelector(INS_SELECTOR);
416
+ if (el) this.fill(el, options);
417
+ }
418
+ /**
419
+ * Fill a specific element.
420
+ * No-op if the element is already loading/filled/empty/error (idempotent).
421
+ */
422
+ fill(el, options = {}) {
423
+ var _a, _b;
424
+ if (el.dataset.adxState) return;
425
+ if (el.style.display === "none") el.style.display = "block";
426
+ const slot = new AdSlot(
427
+ el,
428
+ this.api,
429
+ this.tracker,
430
+ this.config.siteId,
431
+ this.sessionId,
432
+ options
433
+ );
434
+ const lazy = (_b = (_a = options.lazy) != null ? _a : this.config.lazyLoad) != null ? _b : true;
435
+ slot.load(lazy);
436
+ }
437
+ /**
438
+ * Programmatic slot fill by CSS selector.
439
+ * Logs a warning in debug mode if the element is not found.
440
+ */
441
+ defineSlot(selector, options = {}) {
442
+ const el = document.querySelector(selector);
443
+ if (!el) {
444
+ if (this.config.debug) console.warn(`[AdXensor] defineSlot: "${selector}" not found`);
445
+ return;
446
+ }
447
+ this.fill(el, options);
448
+ }
449
+ // ─── DOM watcher (SPA support) ────────────────────────────────────────────
450
+ watchDom() {
451
+ if (typeof MutationObserver === "undefined") return;
452
+ this.domObserver = new MutationObserver((mutations) => {
453
+ for (const { addedNodes } of mutations) {
454
+ addedNodes.forEach((node) => {
455
+ if (!(node instanceof HTMLElement)) return;
456
+ if (node.matches("ins.adxensor") && !node.dataset.adxState) {
457
+ this.fill(node);
458
+ }
459
+ node.querySelectorAll(INS_SELECTOR).forEach((el) => this.fill(el));
460
+ });
461
+ }
462
+ });
463
+ this.domObserver.observe(document.body, { childList: true, subtree: true });
464
+ }
465
+ destroy() {
466
+ var _a;
467
+ (_a = this.domObserver) == null ? void 0 : _a.disconnect();
468
+ this.domObserver = null;
469
+ this.initialized = false;
470
+ }
471
+ };
472
+ /** One global instance per page (like AdSense). */
473
+ _AdXensor.instance = null;
474
+ var AdXensor = _AdXensor;
475
+ export {
476
+ AdSlot,
477
+ AdXensor,
478
+ AdXensorApi,
479
+ Tracker,
480
+ getDevice,
481
+ getSessionId,
482
+ hasFired,
483
+ markFired,
484
+ renderAd,
485
+ resolveSize
486
+ };
487
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/core/api.ts", "../../../src/core/session.ts", "../../../src/core/device.ts", "../../../src/core/tracker.ts", "../../../src/core/renderer.ts", "../../../src/core/AdSlot.ts", "../../../src/core/AdXensor.ts"],
4
+ "sourcesContent": ["import type { ServeRequest, AdResponse, AdEventPayload, PageviewPayload } from './types.js';\n\n/** ads_core /v1/ad response shape */\ninterface AdsCoreAdResponse {\n creative_url: string;\n click_url: string; // full URL e.g. https://ads.adxensor.com/v1/click/TOKEN\n impression_url: string; // full URL e.g. https://ads.adxensor.com/v1/impression/TOKEN\n campaign_id: string;\n size: string; // \"300x250\"\n}\n\nexport class AdXensorApi {\n constructor(\n private readonly baseUrl: string, // e.g. \"https://ads.adxensor.com/v1\"\n private readonly apiKey?: string,\n ) {}\n\n private headers(): Record<string, string> {\n const h: Record<string, string> = { 'Content-Type': 'application/json' };\n if (this.apiKey) h['X-Publisher-Key'] = this.apiKey;\n return h;\n }\n\n // \u2500\u2500\u2500 Fetch the best ad for a slot \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n async serveAd(req: ServeRequest): Promise<AdResponse | null> {\n try {\n // Map SDK params \u2192 ads_core query schema\n const params = new URLSearchParams({ zone: req.size, site: req.siteId });\n if (req.url) params.set('url', req.url);\n if (req.referrer) params.set('ref', req.referrer);\n\n const res = await fetch(`${this.baseUrl}/ad?${params.toString()}`, {\n method: 'GET',\n headers: this.headers(),\n });\n\n // 204 = no ad available for this slot\n if (res.status === 204 || !res.ok) return null;\n\n const data = await res.json() as AdsCoreAdResponse;\n\n // Extract shared event token from either tracking URL\n const eventId = data.impression_url.split('/').pop() ?? '';\n\n const [w, h] = (data.size ?? '300x250').split('x').map(Number);\n\n return {\n adId: eventId,\n campaignId: data.campaign_id,\n type: 'image',\n imageUrl: data.creative_url,\n clickHref: data.click_url, // full URL \u2014 use directly as <a> href\n width: w ?? 300,\n height: h ?? 250,\n altText: '',\n impressionToken: eventId,\n clickToken: eventId,\n };\n } catch {\n return null;\n }\n }\n\n // \u2500\u2500\u2500 Fire-and-forget event (uses sendBeacon when available) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n sendEvent(path: string, payload: AdEventPayload | PageviewPayload): void {\n const url = `${this.baseUrl}/events/${path}`;\n const body = JSON.stringify(payload);\n\n if (typeof navigator !== 'undefined' && navigator.sendBeacon) {\n navigator.sendBeacon(url, new Blob([body], { type: 'application/json' }));\n } else {\n fetch(url, {\n method: 'POST',\n headers: this.headers(),\n body,\n keepalive: true,\n }).catch(() => { /* silent */ });\n }\n }\n}\n", "const SESSION_KEY = '_adx_sid';\nconst FIRED_KEY = '_adx_fired';\n\n/** Returns (or creates) a session ID stored in sessionStorage. */\nexport function getSessionId(): string {\n try {\n let id = sessionStorage.getItem(SESSION_KEY);\n if (!id) {\n id = typeof crypto !== 'undefined' && crypto.randomUUID\n ? crypto.randomUUID()\n : Math.random().toString(36).slice(2) + Date.now().toString(36);\n sessionStorage.setItem(SESSION_KEY, id);\n }\n return id;\n } catch {\n // sessionStorage blocked (iframe sandbox, private mode, etc.)\n return Math.random().toString(36).slice(2);\n }\n}\n\n/** Returns true if this event key was already fired this session. */\nexport function hasFired(key: string): boolean {\n try {\n const map = JSON.parse(sessionStorage.getItem(FIRED_KEY) || '{}') as Record<string, number>;\n return key in map;\n } catch {\n return false;\n }\n}\n\n/** Mark an event key as fired. */\nexport function markFired(key: string): void {\n try {\n const map = JSON.parse(sessionStorage.getItem(FIRED_KEY) || '{}') as Record<string, number>;\n map[key] = Date.now();\n sessionStorage.setItem(FIRED_KEY, JSON.stringify(map));\n } catch { /* ignore */ }\n}\n", "import type { DeviceType } from './types.js';\n\nexport function getDevice(): DeviceType {\n if (typeof window === 'undefined') return 'desktop';\n const w = window.innerWidth;\n if (w < 768) return 'mobile';\n if (w < 1024) return 'tablet';\n return 'desktop';\n}\n\n/**\n * Resolve 'auto' to a concrete size based on the container width + device.\n * Falls back to a safe default for each device tier.\n */\nexport function resolveSize(requested: string, container: HTMLElement): string {\n if (requested !== 'auto') return requested;\n\n const device = getDevice();\n const w = container.offsetWidth || window.innerWidth;\n\n if (device === 'mobile') {\n return w >= 320 ? '320x50' : '300x250';\n }\n if (device === 'tablet') {\n return w >= 468 ? '468x60' : '300x250';\n }\n // desktop\n if (w >= 728) return '728x90';\n if (w >= 468) return '468x60';\n return '300x250';\n}\n", "import { hasFired, markFired } from './session.js';\nimport { getDevice } from './device.js';\nimport type { AdXensorApi } from './api.js';\n\n/**\n * Normalise navigator.language (e.g. \"fr-FR\", \"en-US\") to a 2-char\n * ISO 639-1 code (\"fr\", \"en\"). Country is resolved server-side from IP.\n */\nfunction getLang(): string {\n return (navigator.language ?? 'fr').split('-')[0].toLowerCase();\n}\n\nexport class Tracker {\n constructor(\n private readonly api: AdXensorApi,\n private readonly siteId: string,\n private readonly sessionId: string,\n ) {}\n\n // \u2500\u2500\u2500 Base context \u2014 attached to every event \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n private base() {\n return {\n siteId: this.siteId,\n url: location.href,\n device: getDevice(),\n sessionId: this.sessionId,\n language: getLang(), // \"fr\" | \"en\" | \"pt\" | \u2026\n screenWidth: screen.width, // for device analytics breakdowns\n ts: Date.now(),\n };\n }\n\n // \u2500\u2500\u2500 Pageview: once per page load \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n pageview(): void {\n this.api.sendEvent('pageview', {\n ...this.base(),\n referrer: document.referrer,\n });\n }\n\n // \u2500\u2500\u2500 Impression: once per adId \u00D7 session \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n impression(adId: string, slotId: string, impressionToken: string): void {\n const key = `imp:${adId}:${this.sessionId}`;\n if (hasFired(key)) return;\n markFired(key);\n this.api.sendEvent('impression', {\n ...this.base(),\n adId,\n slotId,\n impressionToken,\n });\n }\n\n // \u2500\u2500\u2500 Click: deduplicated within 500 ms window per ad \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n click(adId: string, slotId: string, clickToken: string): void {\n const tsKey = `clk_ts:${adId}`;\n try {\n const last = parseInt(sessionStorage.getItem(tsKey) || '0', 10);\n if (Date.now() - last < 500) return; // double-click guard\n sessionStorage.setItem(tsKey, String(Date.now()));\n } catch { /* ignore */ }\n this.api.sendEvent('click', {\n ...this.base(),\n adId,\n slotId,\n clickToken,\n });\n }\n\n // \u2500\u2500\u2500 View: once per adId \u00D7 session (50% visible for \u22651 s) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n view(adId: string, slotId: string): void {\n const key = `view:${adId}:${this.sessionId}`;\n if (hasFired(key)) return;\n markFired(key);\n this.api.sendEvent('view', {\n ...this.base(),\n adId,\n slotId,\n });\n }\n}\n", "import type { AdResponse } from './types.js';\n\n/**\n * Inject the ad creative into `container`.\n * - type 'html': raw HTML creative (rich media, animated banners)\n * - type 'image': <a><img></a> wrapped in a click-tracked link\n *\n * The caller handles the click event via addEventListener; the rendered\n * anchor uses href=\"#\" so the tracker can intercept and open the real URL.\n */\nexport function renderAd(container: HTMLElement, ad: AdResponse, clickHref: string): void {\n container.style.overflow = 'hidden';\n container.style.display = 'block';\n container.style.lineHeight = '0'; // removes bottom gap under <img>\n\n if (ad.type === 'html' && ad.htmlContent) {\n container.innerHTML = ad.htmlContent;\n return;\n }\n\n const a = document.createElement('a');\n a.href = clickHref;\n a.target = '_blank';\n a.rel = 'noopener noreferrer';\n a.setAttribute('data-adx-click', '1');\n a.style.display = 'block';\n\n const img = document.createElement('img');\n img.src = ad.imageUrl ?? '';\n img.width = ad.width;\n img.height = ad.height;\n img.alt = ad.altText || '';\n img.style.display = 'block';\n img.style.maxWidth = '100%';\n img.loading = 'lazy';\n\n a.appendChild(img);\n container.innerHTML = '';\n container.appendChild(a);\n}\n", "import type { AdXensorApi } from './api.js';\nimport type { Tracker } from './tracker.js';\nimport type { SlotOptions } from './types.js';\nimport { getDevice, resolveSize } from './device.js';\nimport { renderAd } from './renderer.js';\n\nconst VIEW_THRESHOLD = 0.5; // 50% visible\nconst VIEW_DURATION = 1000; // 1 second held in view = fires view event\n\nexport class AdSlot {\n private readonly slotId: string;\n private observer: IntersectionObserver | null = null;\n private viewTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(\n private readonly el: HTMLElement,\n private readonly api: AdXensorApi,\n private readonly tracker: Tracker,\n private readonly siteId: string,\n private readonly sessionId: string,\n private readonly options: SlotOptions = {},\n ) {\n // AdSense-style: data-ad-slot. Falls back to element id or a random id.\n this.slotId =\n options.slotId ??\n el.dataset.adSlot ?? // data-ad-slot=\"banner-top\"\n (el.id || `adx-${Math.random().toString(36).slice(2, 9)}`);\n }\n\n // \u2500\u2500\u2500 Load entry point \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n load(lazy = true): void {\n if (this.el.dataset.adxState) return; // idempotent \u2014 already in-flight or done\n\n if (lazy && typeof IntersectionObserver !== 'undefined') {\n this.watchForEntry();\n } else {\n void this.fetch();\n }\n }\n\n // \u2500\u2500\u2500 Lazy entry: wait until element is near the viewport \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private watchForEntry(): void {\n const io = new IntersectionObserver(\n (entries) => {\n if (entries[0]?.isIntersecting) {\n io.disconnect();\n void this.fetch();\n }\n },\n { rootMargin: '200px', threshold: 0 },\n );\n io.observe(this.el);\n }\n\n // \u2500\u2500\u2500 Fetch ad from API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private async fetch(): Promise<void> {\n if (this.el.dataset.adxState) return; // double-check after async gap\n this.setState('loading');\n\n try {\n // AdSense-style: data-ad-format. Falls back to options.format then 'auto'.\n const size = resolveSize(\n this.options.format ?? this.el.dataset.adFormat ?? 'auto',\n this.el,\n );\n const [w, h] = size.split('x').map(Number);\n\n // Reserve exact dimensions to prevent layout shift (CLS)\n if (w && h) {\n this.el.style.width = `${w}px`;\n this.el.style.maxWidth = '100%';\n this.el.style.minHeight = `${h}px`;\n }\n\n const ad = await this.api.serveAd({\n siteId: this.siteId,\n slotId: this.slotId,\n size,\n device: getDevice(),\n url: location.href,\n referrer: document.referrer,\n // Normalise \"fr-FR\" \u2192 \"fr\"; server resolves country from IP separately\n language: (navigator.language ?? 'fr').split('-')[0].toLowerCase(),\n screenWidth: screen.width,\n sessionId: this.sessionId,\n });\n\n if (!ad) {\n this.setState('empty');\n this.el.style.display = 'none';\n return;\n }\n\n // ads_core provides the full click URL directly (handles redirect internally)\n renderAd(this.el, ad, ad.clickHref);\n this.setState('filled');\n\n // Click tracking \u2014 passive: true because we never call preventDefault\n this.el.addEventListener('click', (e) => {\n if ((e.target as HTMLElement).closest('[data-adx-click]')) {\n this.tracker.click(ad.adId, this.slotId, ad.clickToken);\n }\n }, { passive: true });\n\n // Impression fires immediately after render (once per adId \u00D7 session)\n this.tracker.impression(ad.adId, this.slotId, ad.impressionToken);\n\n // View fires after \u226550% visible for \u22651s (IAB standard)\n this.watchVisibility(ad.adId, this.slotId);\n\n } catch {\n this.setState('error');\n this.el.style.display = 'none';\n }\n }\n\n // \u2500\u2500\u2500 Viewability (IAB MRC standard) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private watchVisibility(adId: string, slotId: string): void {\n if (typeof IntersectionObserver === 'undefined') {\n this.tracker.view(adId, slotId); // fallback: fire immediately\n return;\n }\n\n this.observer = new IntersectionObserver(\n ([entry]) => {\n if (!entry) return;\n\n if (entry.intersectionRatio >= VIEW_THRESHOLD) {\n if (this.viewTimer === null) {\n this.viewTimer = setTimeout(() => {\n this.tracker.view(adId, slotId);\n this.disconnect(); // stop observing once view is counted\n }, VIEW_DURATION);\n }\n } else {\n // Left viewport before 1s \u2014 reset timer\n if (this.viewTimer !== null) {\n clearTimeout(this.viewTimer);\n this.viewTimer = null;\n }\n }\n },\n { threshold: [VIEW_THRESHOLD] },\n );\n\n this.observer.observe(this.el);\n }\n\n // \u2500\u2500\u2500 State machine \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private setState(state: 'loading' | 'filled' | 'empty' | 'error'): void {\n this.el.dataset.adxState = state;\n }\n\n // \u2500\u2500\u2500 Cleanup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n disconnect(): void {\n this.observer?.disconnect();\n this.observer = null;\n if (this.viewTimer !== null) {\n clearTimeout(this.viewTimer);\n this.viewTimer = null;\n }\n }\n}\n", "import { AdXensorApi } from './api.js';\nimport { Tracker } from './tracker.js';\nimport { AdSlot } from './AdSlot.js';\nimport { getSessionId } from './session.js';\nimport type { AdXensorConfig, SlotOptions } from './types.js';\n\n/** ads_core public base URL \u2014 must include /v1 suffix */\nconst DEFAULT_API_URL = 'https://core.adxensor.com/v1';\n\n/** Selector for unfilled <ins class=\"adxensor\"> slots */\nconst INS_SELECTOR = 'ins.adxensor:not([data-adx-state])';\n\nexport class AdXensor {\n /** One global instance per page (like AdSense). */\n private static instance: AdXensor | null = null;\n\n private readonly api: AdXensorApi;\n private readonly tracker: Tracker;\n private readonly sessionId: string;\n private readonly config: AdXensorConfig;\n private initialized = false;\n private domObserver: MutationObserver | null = null;\n\n constructor(config: AdXensorConfig) {\n this.config = config;\n this.sessionId = getSessionId();\n this.api = new AdXensorApi(\n config.apiUrl ?? DEFAULT_API_URL,\n config.apiKey,\n );\n this.tracker = new Tracker(this.api, config.siteId, this.sessionId);\n }\n\n // \u2500\u2500\u2500 Singleton helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n static getInstance(config: AdXensorConfig): AdXensor {\n if (!AdXensor.instance) AdXensor.instance = new AdXensor(config);\n return AdXensor.instance;\n }\n\n static reset(): void {\n AdXensor.instance?.destroy();\n AdXensor.instance = null;\n }\n\n // \u2500\u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Idempotent init \u2014 safe to call multiple times.\n * Fires pageview, fills existing slots, watches DOM for new ones.\n */\n init(): this {\n if (this.initialized) return this;\n this.initialized = true;\n this.tracker.pageview();\n this.fillAll();\n this.watchDom();\n return this;\n }\n\n /** Fill every unfilled <ins class=\"adxensor\"> currently in the DOM. */\n fillAll(): void {\n document.querySelectorAll<HTMLElement>(INS_SELECTOR).forEach((el) => this.fill(el));\n }\n\n /**\n * AdSense-style push \u2014 fills the next unfilled slot in DOM order.\n * Use alongside <script>(window.adxensor = window.adxensor || []).push({})</script>\n */\n push(options: SlotOptions = {}): void {\n const el = document.querySelector<HTMLElement>(INS_SELECTOR);\n if (el) this.fill(el, options);\n }\n\n /**\n * Fill a specific element.\n * No-op if the element is already loading/filled/empty/error (idempotent).\n */\n fill(el: HTMLElement, options: SlotOptions = {}): void {\n if (el.dataset.adxState) return; // state machine guard\n\n // Enforce display:block \u2014 required like AdSense\n if (el.style.display === 'none') el.style.display = 'block';\n\n const slot = new AdSlot(\n el,\n this.api,\n this.tracker,\n this.config.siteId,\n this.sessionId,\n options,\n );\n\n const lazy = options.lazy ?? this.config.lazyLoad ?? true;\n slot.load(lazy);\n }\n\n /**\n * Programmatic slot fill by CSS selector.\n * Logs a warning in debug mode if the element is not found.\n */\n defineSlot(selector: string, options: SlotOptions = {}): void {\n const el = document.querySelector<HTMLElement>(selector);\n if (!el) {\n if (this.config.debug) console.warn(`[AdXensor] defineSlot: \"${selector}\" not found`);\n return;\n }\n this.fill(el, options);\n }\n\n // \u2500\u2500\u2500 DOM watcher (SPA support) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private watchDom(): void {\n if (typeof MutationObserver === 'undefined') return;\n\n this.domObserver = new MutationObserver((mutations) => {\n for (const { addedNodes } of mutations) {\n addedNodes.forEach((node) => {\n if (!(node instanceof HTMLElement)) return;\n if (node.matches('ins.adxensor') && !node.dataset.adxState) {\n this.fill(node);\n }\n node.querySelectorAll<HTMLElement>(INS_SELECTOR).forEach((el) => this.fill(el));\n });\n }\n });\n\n this.domObserver.observe(document.body, { childList: true, subtree: true });\n }\n\n destroy(): void {\n this.domObserver?.disconnect();\n this.domObserver = null;\n this.initialized = false;\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,21 @@
1
+ import type { AdXensorApi } from './api.js';
2
+ import type { Tracker } from './tracker.js';
3
+ import type { SlotOptions } from './types.js';
4
+ export declare class AdSlot {
5
+ private readonly el;
6
+ private readonly api;
7
+ private readonly tracker;
8
+ private readonly siteId;
9
+ private readonly sessionId;
10
+ private readonly options;
11
+ private readonly slotId;
12
+ private observer;
13
+ private viewTimer;
14
+ constructor(el: HTMLElement, api: AdXensorApi, tracker: Tracker, siteId: string, sessionId: string, options?: SlotOptions);
15
+ load(lazy?: boolean): void;
16
+ private watchForEntry;
17
+ private fetch;
18
+ private watchVisibility;
19
+ private setState;
20
+ disconnect(): void;
21
+ }
@@ -0,0 +1,38 @@
1
+ import type { AdXensorConfig, SlotOptions } from './types.js';
2
+ export declare class AdXensor {
3
+ /** One global instance per page (like AdSense). */
4
+ private static instance;
5
+ private readonly api;
6
+ private readonly tracker;
7
+ private readonly sessionId;
8
+ private readonly config;
9
+ private initialized;
10
+ private domObserver;
11
+ constructor(config: AdXensorConfig);
12
+ static getInstance(config: AdXensorConfig): AdXensor;
13
+ static reset(): void;
14
+ /**
15
+ * Idempotent init — safe to call multiple times.
16
+ * Fires pageview, fills existing slots, watches DOM for new ones.
17
+ */
18
+ init(): this;
19
+ /** Fill every unfilled <ins class="adxensor"> currently in the DOM. */
20
+ fillAll(): void;
21
+ /**
22
+ * AdSense-style push — fills the next unfilled slot in DOM order.
23
+ * Use alongside <script>(window.adxensor = window.adxensor || []).push({})</script>
24
+ */
25
+ push(options?: SlotOptions): void;
26
+ /**
27
+ * Fill a specific element.
28
+ * No-op if the element is already loading/filled/empty/error (idempotent).
29
+ */
30
+ fill(el: HTMLElement, options?: SlotOptions): void;
31
+ /**
32
+ * Programmatic slot fill by CSS selector.
33
+ * Logs a warning in debug mode if the element is not found.
34
+ */
35
+ defineSlot(selector: string, options?: SlotOptions): void;
36
+ private watchDom;
37
+ destroy(): void;
38
+ }
@@ -0,0 +1,10 @@
1
+ import type { ServeRequest, AdResponse, AdEventPayload, PageviewPayload } from './types.js';
2
+ export declare class AdXensorApi {
3
+ private readonly baseUrl;
4
+ private readonly apiKey?;
5
+ constructor(baseUrl: string, // e.g. "https://ads.adxensor.com/v1"
6
+ apiKey?: string | undefined);
7
+ private headers;
8
+ serveAd(req: ServeRequest): Promise<AdResponse | null>;
9
+ sendEvent(path: string, payload: AdEventPayload | PageviewPayload): void;
10
+ }
@@ -0,0 +1,7 @@
1
+ import type { DeviceType } from './types.js';
2
+ export declare function getDevice(): DeviceType;
3
+ /**
4
+ * Resolve 'auto' to a concrete size based on the container width + device.
5
+ * Falls back to a safe default for each device tier.
6
+ */
7
+ export declare function resolveSize(requested: string, container: HTMLElement): string;
@@ -0,0 +1,10 @@
1
+ import type { AdResponse } from './types.js';
2
+ /**
3
+ * Inject the ad creative into `container`.
4
+ * - type 'html': raw HTML creative (rich media, animated banners)
5
+ * - type 'image': <a><img></a> wrapped in a click-tracked link
6
+ *
7
+ * The caller handles the click event via addEventListener; the rendered
8
+ * anchor uses href="#" so the tracker can intercept and open the real URL.
9
+ */
10
+ export declare function renderAd(container: HTMLElement, ad: AdResponse, clickHref: string): void;
@@ -0,0 +1,6 @@
1
+ /** Returns (or creates) a session ID stored in sessionStorage. */
2
+ export declare function getSessionId(): string;
3
+ /** Returns true if this event key was already fired this session. */
4
+ export declare function hasFired(key: string): boolean;
5
+ /** Mark an event key as fired. */
6
+ export declare function markFired(key: string): void;