@trillboards/connect 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 @@
1
+ {"version":3,"sources":["../src/device.js","../src/auction.js","../src/offline.js","../src/branding.js","../src/analytics.js","../src/index.js"],"sourcesContent":["/**\n * Trillboards Connect SDK - Device Module\n *\n * Handles device registration, fingerprint generation, heartbeat loop,\n * and device status tracking against the Partner API.\n */\n\nconst API_BASE = 'https://api.trillboards.com/v1/partner';\n\n// Device status enum\nconst DeviceStatus = {\n UNREGISTERED: 'unregistered',\n REGISTERING: 'registering',\n REGISTERED: 'registered',\n ONLINE: 'online',\n OFFLINE: 'offline',\n ERROR: 'error'\n};\n\n/**\n * Generate a deterministic fingerprint from a device ID.\n * Uses a simple hash that works in both browser and Node environments.\n *\n * @param {string} deviceId - External device identifier\n * @returns {string} Deterministic fingerprint string\n */\nfunction generateFingerprint(deviceId) {\n if (!deviceId || typeof deviceId !== 'string') {\n throw new Error('deviceId must be a non-empty string');\n }\n\n // Simple FNV-1a 32-bit hash for deterministic browser-compatible fingerprinting\n let hash = 0x811c9dc5;\n for (let i = 0; i < deviceId.length; i++) {\n hash ^= deviceId.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n\n // Convert to unsigned 32-bit, then to hex\n const hashHex = (hash >>> 0).toString(16).padStart(8, '0');\n\n // Generate additional entropy from device ID length and char codes\n let extHash = 0x01000193;\n for (let i = 0; i < deviceId.length; i++) {\n extHash ^= deviceId.charCodeAt(i) * (i + 1);\n extHash = Math.imul(extHash, 0x811c9dc5);\n }\n const extHex = (extHash >>> 0).toString(16).padStart(8, '0');\n\n return `SDK_${hashHex}${extHex}`;\n}\n\n/**\n * Collect device metadata from the browser environment.\n * Falls back gracefully when APIs are unavailable.\n *\n * @returns {Object} Device metadata\n */\nfunction collectDeviceMetadata() {\n const metadata = {\n sdk_version: '1.0.0',\n timestamp: new Date().toISOString()\n };\n\n if (typeof navigator !== 'undefined') {\n metadata.user_agent = navigator.userAgent;\n metadata.platform = navigator.platform;\n metadata.language = navigator.language;\n metadata.online = navigator.onLine;\n metadata.hardware_concurrency = navigator.hardwareConcurrency;\n }\n\n if (typeof screen !== 'undefined') {\n metadata.screen_width = screen.width;\n metadata.screen_height = screen.height;\n metadata.pixel_ratio = typeof devicePixelRatio !== 'undefined' ? devicePixelRatio : 1;\n metadata.color_depth = screen.colorDepth;\n }\n\n return metadata;\n}\n\n/**\n * Register a device with the Partner API.\n *\n * @param {string} apiKey - Partner API key (trb_partner_xxx)\n * @param {string} deviceId - External device identifier\n * @param {Object} metadata - Additional device metadata\n * @returns {Promise<Object>} Registration response with fingerprint, screen_id, embed_url\n */\nasync function registerDevice(apiKey, deviceId, metadata = {}) {\n if (!apiKey) {\n throw new Error('apiKey is required for device registration');\n }\n if (!deviceId) {\n throw new Error('deviceId is required for device registration');\n }\n\n const browserMetadata = collectDeviceMetadata();\n const displayInfo = {};\n\n if (browserMetadata.screen_width) {\n displayInfo.width = browserMetadata.screen_width;\n displayInfo.height = browserMetadata.screen_height;\n }\n\n const body = {\n device_id: deviceId,\n device_type: metadata.device_type || 'digital_signage',\n name: metadata.name || `SDK Device: ${deviceId}`,\n display: {\n ...displayInfo,\n ...metadata.display\n },\n location: metadata.location || {},\n custom_metadata: {\n ...browserMetadata,\n ...metadata.custom\n }\n };\n\n const response = await fetch(`${API_BASE}/device`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${apiKey}`,\n 'X-SDK-Version': '1.0.0'\n },\n body: JSON.stringify(body)\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const err = new Error(errorData.message || `Device registration failed: ${response.status}`);\n err.statusCode = response.status;\n err.response = errorData;\n throw err;\n }\n\n return response.json();\n}\n\n/**\n * Create and manage a heartbeat loop for a device.\n *\n * @param {string} fingerprint - Device fingerprint (from registration)\n * @param {number} intervalMs - Heartbeat interval in milliseconds (default 60000)\n * @param {Object} options - Additional options\n * @param {Function} options.onSuccess - Callback on successful heartbeat\n * @param {Function} options.onError - Callback on heartbeat error\n * @param {number} options.maxRetries - Max consecutive failures before stopping (default 10)\n * @param {number} options.retryBackoff - Backoff multiplier for retries (default 1.5)\n * @returns {Object} Heartbeat controller with start/stop/getStatus methods\n */\nfunction startHeartbeat(fingerprint, intervalMs = 60000, options = {}) {\n if (!fingerprint) {\n throw new Error('fingerprint is required for heartbeat');\n }\n\n const maxRetries = options.maxRetries || 10;\n const retryBackoff = options.retryBackoff || 1.5;\n const onSuccess = options.onSuccess || (() => {});\n const onError = options.onError || (() => {});\n\n let timerId = null;\n let consecutiveFailures = 0;\n let totalBeats = 0;\n let lastBeatAt = null;\n let running = false;\n let currentInterval = intervalMs;\n\n async function sendBeat() {\n try {\n const response = await fetch(`${API_BASE}/device/${encodeURIComponent(fingerprint)}/heartbeat`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-SDK-Version': '1.0.0'\n },\n body: JSON.stringify({\n timestamp: new Date().toISOString(),\n total_beats: totalBeats\n })\n });\n\n if (!response.ok) {\n throw new Error(`Heartbeat failed: HTTP ${response.status}`);\n }\n\n const data = await response.json();\n consecutiveFailures = 0;\n currentInterval = intervalMs;\n totalBeats++;\n lastBeatAt = new Date();\n\n onSuccess({\n beat: totalBeats,\n status: data.status || 'online',\n last_seen_at: data.last_seen_at,\n timestamp: lastBeatAt.toISOString()\n });\n\n return data;\n } catch (err) {\n consecutiveFailures++;\n\n onError({\n error: err.message,\n consecutiveFailures,\n willRetry: consecutiveFailures < maxRetries\n });\n\n if (consecutiveFailures >= maxRetries) {\n console.error(`[Trillboards SDK] Heartbeat stopped after ${maxRetries} consecutive failures`);\n stop();\n return null;\n }\n\n // Apply exponential backoff\n currentInterval = Math.min(\n intervalMs * Math.pow(retryBackoff, consecutiveFailures),\n intervalMs * 5 // Cap at 5x the original interval\n );\n\n return null;\n }\n }\n\n function scheduleNext() {\n if (!running) return;\n timerId = setTimeout(async () => {\n await sendBeat();\n scheduleNext();\n }, currentInterval);\n }\n\n function start() {\n if (running) return;\n running = true;\n consecutiveFailures = 0;\n currentInterval = intervalMs;\n\n // Send first heartbeat immediately\n sendBeat().then(() => {\n scheduleNext();\n });\n }\n\n function stop() {\n running = false;\n if (timerId) {\n clearTimeout(timerId);\n timerId = null;\n }\n }\n\n function getStatus() {\n return {\n running,\n totalBeats,\n lastBeatAt: lastBeatAt ? lastBeatAt.toISOString() : null,\n consecutiveFailures,\n currentIntervalMs: currentInterval\n };\n }\n\n return {\n start,\n stop,\n getStatus,\n sendBeat\n };\n}\n\n/**\n * Device state tracker that maintains the current state of a device.\n * Emits state change events and provides a consistent status view.\n */\nclass DeviceTracker {\n constructor(deviceId) {\n this.deviceId = deviceId;\n this.status = DeviceStatus.UNREGISTERED;\n this.fingerprint = null;\n this.screenId = null;\n this.embedUrl = null;\n this.registrationData = null;\n this.heartbeatController = null;\n this.listeners = {};\n this.lastError = null;\n }\n\n /**\n * Listen for status change events\n * @param {string} event - Event name: 'status_change', 'registered', 'error', 'heartbeat'\n * @param {Function} callback - Event handler\n */\n on(event, callback) {\n if (!this.listeners[event]) {\n this.listeners[event] = [];\n }\n this.listeners[event].push(callback);\n }\n\n /**\n * Remove an event listener\n * @param {string} event - Event name\n * @param {Function} callback - Handler to remove\n */\n off(event, callback) {\n if (this.listeners[event]) {\n this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);\n }\n }\n\n /**\n * Emit an event to all registered listeners\n * @param {string} event - Event name\n * @param {Object} data - Event data\n */\n emit(event, data) {\n if (this.listeners[event]) {\n this.listeners[event].forEach(cb => {\n try {\n cb(data);\n } catch (err) {\n console.error(`[Trillboards SDK] Event listener error for '${event}':`, err);\n }\n });\n }\n }\n\n /**\n * Update device status and emit change event\n * @param {string} newStatus - New status from DeviceStatus enum\n */\n setStatus(newStatus) {\n const oldStatus = this.status;\n this.status = newStatus;\n if (oldStatus !== newStatus) {\n this.emit('status_change', {\n from: oldStatus,\n to: newStatus,\n deviceId: this.deviceId,\n timestamp: new Date().toISOString()\n });\n }\n }\n\n /**\n * Register the device and start heartbeat\n * @param {string} apiKey - Partner API key\n * @param {Object} metadata - Device metadata\n * @param {Object} heartbeatOptions - Heartbeat configuration\n * @returns {Promise<Object>} Registration data\n */\n async register(apiKey, metadata = {}, heartbeatOptions = {}) {\n this.setStatus(DeviceStatus.REGISTERING);\n\n try {\n const result = await registerDevice(apiKey, this.deviceId, metadata);\n\n this.fingerprint = result.fingerprint;\n this.screenId = result.screen_id;\n this.embedUrl = result.embed_url;\n this.registrationData = result;\n this.lastError = null;\n\n this.setStatus(DeviceStatus.REGISTERED);\n this.emit('registered', result);\n\n // Start heartbeat\n const interval = heartbeatOptions.interval || 60000;\n this.heartbeatController = startHeartbeat(this.fingerprint, interval, {\n onSuccess: (data) => {\n this.setStatus(DeviceStatus.ONLINE);\n this.emit('heartbeat', data);\n },\n onError: (data) => {\n if (!data.willRetry) {\n this.setStatus(DeviceStatus.OFFLINE);\n }\n this.emit('heartbeat_error', data);\n },\n maxRetries: heartbeatOptions.maxRetries || 10,\n retryBackoff: heartbeatOptions.retryBackoff || 1.5\n });\n this.heartbeatController.start();\n\n return result;\n } catch (err) {\n this.lastError = err;\n this.setStatus(DeviceStatus.ERROR);\n this.emit('error', {\n phase: 'registration',\n error: err.message,\n statusCode: err.statusCode\n });\n throw err;\n }\n }\n\n /**\n * Stop heartbeat and clean up\n */\n destroy() {\n if (this.heartbeatController) {\n this.heartbeatController.stop();\n this.heartbeatController = null;\n }\n this.setStatus(DeviceStatus.OFFLINE);\n this.listeners = {};\n }\n\n /**\n * Get the complete device state\n * @returns {Object} Current device state\n */\n getState() {\n return {\n deviceId: this.deviceId,\n status: this.status,\n fingerprint: this.fingerprint,\n screenId: this.screenId,\n embedUrl: this.embedUrl,\n heartbeat: this.heartbeatController ? this.heartbeatController.getStatus() : null,\n lastError: this.lastError ? this.lastError.message : null\n };\n }\n}\n\n// Export for both module and direct use\nmodule.exports = {\n registerDevice,\n generateFingerprint,\n startHeartbeat,\n DeviceTracker,\n DeviceStatus,\n collectDeviceMetadata\n};\n","/**\n * Trillboards Connect SDK - Auction Module\n *\n * Handles ad fetching, rendering (video/image), impression tracking,\n * ad rotation with configurable intervals, and VAST tag parsing.\n */\n\nconst API_BASE = 'https://api.trillboards.com/v1/partner';\n\n/**\n * Parse a VAST XML response and extract media files + tracking URLs.\n *\n * @param {string} vastXml - Raw VAST XML string\n * @returns {Object} Parsed VAST data with media files and tracking events\n */\nfunction parseVast(vastXml) {\n if (!vastXml || typeof vastXml !== 'string') {\n return null;\n }\n\n let doc;\n if (typeof DOMParser !== 'undefined') {\n const parser = new DOMParser();\n doc = parser.parseFromString(vastXml, 'text/xml');\n } else {\n // Node.js environment - return a minimal structure\n return {\n mediaFiles: [],\n tracking: {},\n impressionUrls: [],\n errorUrls: [],\n raw: vastXml\n };\n }\n\n const result = {\n mediaFiles: [],\n tracking: {},\n impressionUrls: [],\n errorUrls: [],\n clickThrough: null,\n clickTracking: [],\n duration: 0\n };\n\n // Extract impression pixels\n const impressions = doc.querySelectorAll('Impression');\n impressions.forEach(imp => {\n const url = (imp.textContent || '').trim();\n if (url) result.impressionUrls.push(url);\n });\n\n // Extract error tracking URLs\n const errors = doc.querySelectorAll('Error');\n errors.forEach(err => {\n const url = (err.textContent || '').trim();\n if (url) result.errorUrls.push(url);\n });\n\n // Extract media files\n const mediaFiles = doc.querySelectorAll('MediaFile');\n mediaFiles.forEach(mf => {\n result.mediaFiles.push({\n url: (mf.textContent || '').trim(),\n type: mf.getAttribute('type') || 'video/mp4',\n width: parseInt(mf.getAttribute('width')) || 0,\n height: parseInt(mf.getAttribute('height')) || 0,\n bitrate: parseInt(mf.getAttribute('bitrate')) || 0,\n delivery: mf.getAttribute('delivery') || 'progressive'\n });\n });\n\n // Extract tracking events\n const trackingEvents = doc.querySelectorAll('Tracking');\n trackingEvents.forEach(te => {\n const event = te.getAttribute('event');\n const url = (te.textContent || '').trim();\n if (event && url) {\n if (!result.tracking[event]) {\n result.tracking[event] = [];\n }\n result.tracking[event].push(url);\n }\n });\n\n // Extract click-through\n const clickThrough = doc.querySelector('ClickThrough');\n if (clickThrough) {\n result.clickThrough = (clickThrough.textContent || '').trim();\n }\n\n // Extract click tracking\n const clickTrackings = doc.querySelectorAll('ClickTracking');\n clickTrackings.forEach(ct => {\n const url = (ct.textContent || '').trim();\n if (url) result.clickTracking.push(url);\n });\n\n // Extract duration\n const duration = doc.querySelector('Duration');\n if (duration) {\n const timeStr = (duration.textContent || '').trim();\n const parts = timeStr.split(':');\n if (parts.length === 3) {\n result.duration = parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseFloat(parts[2]);\n }\n }\n\n return result;\n}\n\n/**\n * Fire a tracking pixel URL (impression, event, etc.)\n * Uses an Image beacon for reliability and no CORS issues.\n *\n * @param {string} url - Tracking pixel URL\n * @returns {Promise<boolean>} Whether the pixel was fired successfully\n */\nfunction firePixel(url) {\n return new Promise((resolve) => {\n if (!url) {\n resolve(false);\n return;\n }\n\n // Append cache-buster\n const separator = url.includes('?') ? '&' : '?';\n const pixelUrl = `${url}${separator}cb=${Date.now()}`;\n\n if (typeof Image !== 'undefined') {\n const img = new Image();\n img.onload = () => resolve(true);\n img.onerror = () => resolve(false);\n img.src = pixelUrl;\n\n // Timeout after 5 seconds\n setTimeout(() => resolve(false), 5000);\n } else {\n // Node.js fallback - use fetch\n fetch(pixelUrl, { method: 'GET', mode: 'no-cors' })\n .then(() => resolve(true))\n .catch(() => resolve(false));\n }\n });\n}\n\n/**\n * Fetch available ads for a device from the Partner API.\n *\n * @param {string} fingerprint - Device fingerprint\n * @param {Object} options - Fetch options\n * @param {string} options.etag - Previous ETag for conditional requests\n * @returns {Promise<Object>} Ad response with ads array and settings\n */\nasync function fetchAds(fingerprint, options = {}) {\n if (!fingerprint) {\n throw new Error('fingerprint is required to fetch ads');\n }\n\n const headers = {\n 'Accept': 'application/json',\n 'X-SDK-Version': '1.0.0'\n };\n\n if (options.etag) {\n headers['If-None-Match'] = options.etag;\n }\n\n const response = await fetch(`${API_BASE}/device/${encodeURIComponent(fingerprint)}/ads`, {\n method: 'GET',\n headers\n });\n\n // 304 Not Modified - ads haven't changed\n if (response.status === 304) {\n return { ads: [], unchanged: true, etag: options.etag };\n }\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const err = new Error(errorData.message || `Failed to fetch ads: ${response.status}`);\n err.statusCode = response.status;\n throw err;\n }\n\n const data = await response.json();\n return {\n ads: data.ads || [],\n settings: data.settings || {},\n cache_until: data.cache_until,\n etag: data.etag || null,\n device: data.device || {},\n unchanged: false\n };\n}\n\n/**\n * Render an ad (video or image) into a DOM container.\n *\n * @param {Object} ad - Ad object from fetchAds response\n * @param {HTMLElement|string} container - DOM element or selector\n * @param {Object} options - Rendering options\n * @param {boolean} options.muted - Start video muted (default true)\n * @param {boolean} options.autoplay - Autoplay video (default true)\n * @param {string} options.objectFit - CSS object-fit value (default 'cover')\n * @param {Function} options.onStart - Called when ad starts playing\n * @param {Function} options.onComplete - Called when ad finishes\n * @param {Function} options.onError - Called on rendering error\n * @returns {Object} Ad controller with pause/resume/destroy methods\n */\nfunction renderAd(ad, container, options = {}) {\n if (!ad || !ad.url) {\n throw new Error('Valid ad object with url is required');\n }\n\n const containerEl = typeof container === 'string'\n ? document.querySelector(container)\n : container;\n\n if (!containerEl) {\n throw new Error('Container element not found');\n }\n\n const muted = options.muted !== false;\n const autoplay = options.autoplay !== false;\n const objectFit = options.objectFit || 'cover';\n const onStart = options.onStart || (() => {});\n const onComplete = options.onComplete || (() => {});\n const onError = options.onError || (() => {});\n\n let element = null;\n let completionTimer = null;\n let startTime = null;\n let destroyed = false;\n\n // Clear container\n containerEl.innerHTML = '';\n\n // Style container\n containerEl.style.position = containerEl.style.position || 'relative';\n containerEl.style.overflow = 'hidden';\n\n if (ad.type === 'video') {\n element = document.createElement('video');\n element.src = ad.url;\n element.muted = muted;\n element.autoplay = autoplay;\n element.playsInline = true;\n element.setAttribute('playsinline', '');\n element.setAttribute('webkit-playsinline', '');\n element.style.width = '100%';\n element.style.height = '100%';\n element.style.objectFit = objectFit;\n element.style.display = 'block';\n\n element.addEventListener('play', () => {\n if (!destroyed) {\n startTime = Date.now();\n onStart({ ad, type: 'video', timestamp: new Date().toISOString() });\n }\n });\n\n element.addEventListener('ended', () => {\n if (!destroyed) {\n onComplete({\n ad,\n type: 'video',\n duration: startTime ? (Date.now() - startTime) / 1000 : 0,\n completed: true,\n timestamp: new Date().toISOString()\n });\n }\n });\n\n element.addEventListener('error', (e) => {\n if (!destroyed) {\n onError({\n ad,\n type: 'video',\n error: e.message || 'Video playback error',\n timestamp: new Date().toISOString()\n });\n }\n });\n\n containerEl.appendChild(element);\n\n } else {\n // Image ad\n element = document.createElement('img');\n element.src = ad.url;\n element.alt = ad.title || 'Advertisement';\n element.style.width = '100%';\n element.style.height = '100%';\n element.style.objectFit = objectFit;\n element.style.display = 'block';\n\n element.addEventListener('load', () => {\n if (!destroyed) {\n startTime = Date.now();\n onStart({ ad, type: 'image', timestamp: new Date().toISOString() });\n\n // Set completion timer for image ads\n const duration = (ad.duration || options.imageDuration || 8) * 1000;\n completionTimer = setTimeout(() => {\n if (!destroyed) {\n onComplete({\n ad,\n type: 'image',\n duration: ad.duration || options.imageDuration || 8,\n completed: true,\n timestamp: new Date().toISOString()\n });\n }\n }, duration);\n }\n });\n\n element.addEventListener('error', () => {\n if (!destroyed) {\n onError({\n ad,\n type: 'image',\n error: 'Image failed to load',\n timestamp: new Date().toISOString()\n });\n }\n });\n\n containerEl.appendChild(element);\n }\n\n // Return controller\n return {\n pause() {\n if (element && element.pause) {\n element.pause();\n }\n },\n resume() {\n if (element && element.play) {\n element.play().catch(() => {});\n }\n },\n destroy() {\n destroyed = true;\n if (completionTimer) {\n clearTimeout(completionTimer);\n completionTimer = null;\n }\n if (element) {\n if (element.pause) element.pause();\n if (element.parentNode) element.parentNode.removeChild(element);\n element = null;\n }\n },\n getPlaybackState() {\n if (!element) return { state: 'destroyed' };\n if (ad.type === 'video') {\n return {\n state: element.paused ? 'paused' : 'playing',\n currentTime: element.currentTime,\n duration: element.duration,\n muted: element.muted\n };\n }\n return {\n state: startTime ? 'showing' : 'loading',\n elapsed: startTime ? (Date.now() - startTime) / 1000 : 0\n };\n }\n };\n}\n\n/**\n * Track an ad impression by firing the impression pixel.\n *\n * @param {Object} ad - Ad object from fetchAds response\n * @param {string} fingerprint - Device fingerprint\n * @param {Object} options - Tracking options\n * @param {number} options.duration - View duration in seconds\n * @param {boolean} options.completed - Whether the ad completed playback\n * @returns {Promise<Object>} Tracking result\n */\nasync function trackImpression(ad, fingerprint, options = {}) {\n if (!ad || !ad.id) {\n throw new Error('Valid ad object with id is required');\n }\n\n const results = {\n impression_url: false,\n pixel_url: false,\n vast_impressions: []\n };\n\n // Fire the impression URL (POST endpoint)\n if (ad.impression_url) {\n try {\n const url = new URL(ad.impression_url);\n if (options.duration) url.searchParams.set('duration', String(options.duration));\n if (options.completed !== undefined) url.searchParams.set('completed', String(options.completed));\n\n const response = await fetch(url.toString(), {\n method: 'GET',\n mode: 'no-cors'\n });\n results.impression_url = true;\n } catch (err) {\n results.impression_url = false;\n }\n }\n\n // Fire the pixel URL (image beacon)\n if (ad.pixel_url) {\n results.pixel_url = await firePixel(ad.pixel_url);\n }\n\n // Fire VAST impression pixels if present\n if (ad.vast_data && ad.vast_data.impressionUrls) {\n for (const vastUrl of ad.vast_data.impressionUrls) {\n const fired = await firePixel(vastUrl);\n results.vast_impressions.push({ url: vastUrl, fired });\n }\n }\n\n return results;\n}\n\n/**\n * Ad rotation manager that cycles through ads with configurable timing.\n *\n * @param {Object} config - Rotation configuration\n * @param {string} config.fingerprint - Device fingerprint\n * @param {HTMLElement|string} config.container - DOM container for ad rendering\n * @param {number} config.intervalSeconds - Seconds between ad rotations (default 60)\n * @param {number} config.refreshMinutes - Minutes between ad list refreshes (default 5)\n * @param {Function} config.onAdStart - Called when an ad starts\n * @param {Function} config.onAdComplete - Called when an ad completes\n * @param {Function} config.onError - Called on error\n * @param {Function} config.onImpression - Called when impression is tracked\n * @param {Object} config.analytics - Analytics module reference for event buffering\n * @returns {Object} Rotation controller with start/stop/next/getStatus methods\n */\nfunction createAdRotation(config) {\n if (!config.fingerprint) {\n throw new Error('fingerprint is required for ad rotation');\n }\n\n const intervalSeconds = config.intervalSeconds || 60;\n const refreshMinutes = config.refreshMinutes || 5;\n const onAdStart = config.onAdStart || (() => {});\n const onAdComplete = config.onAdComplete || (() => {});\n const onError = config.onError || (() => {});\n const onImpression = config.onImpression || (() => {});\n\n let ads = [];\n let settings = {};\n let currentIndex = 0;\n let currentController = null;\n let rotationTimer = null;\n let refreshTimer = null;\n let running = false;\n let etag = null;\n let totalAdsShown = 0;\n let totalImpressions = 0;\n\n async function loadAds() {\n try {\n const result = await fetchAds(config.fingerprint, { etag });\n\n if (!result.unchanged) {\n ads = result.ads;\n settings = result.settings || {};\n etag = result.etag;\n currentIndex = 0;\n }\n\n return ads;\n } catch (err) {\n onError({ phase: 'fetch', error: err.message });\n return ads; // Return cached ads on error\n }\n }\n\n function getNextAd() {\n if (ads.length === 0) return null;\n const ad = ads[currentIndex % ads.length];\n currentIndex = (currentIndex + 1) % ads.length;\n return ad;\n }\n\n async function showNextAd() {\n if (!running) return;\n\n // Destroy current ad\n if (currentController) {\n currentController.destroy();\n currentController = null;\n }\n\n const ad = getNextAd();\n if (!ad) {\n // No ads available, retry after interval\n rotationTimer = setTimeout(showNextAd, intervalSeconds * 1000);\n return;\n }\n\n try {\n currentController = renderAd(ad, config.container, {\n muted: config.muted !== false,\n autoplay: true,\n objectFit: config.objectFit || 'cover',\n imageDuration: settings.image_duration || ad.duration || 8,\n\n onStart: (data) => {\n totalAdsShown++;\n onAdStart({ ...data, totalAdsShown });\n\n // Track impression\n trackImpression(ad, config.fingerprint, {\n duration: ad.duration,\n completed: false\n }).then(result => {\n totalImpressions++;\n onImpression({ ad, result, totalImpressions });\n }).catch(err => {\n onError({ phase: 'impression', error: err.message });\n });\n\n if (config.analytics) {\n config.analytics.track('ad_start', {\n ad_id: ad.id,\n ad_type: ad.type,\n fingerprint: config.fingerprint\n });\n }\n },\n\n onComplete: (data) => {\n onAdComplete({ ...data, totalAdsShown });\n\n // Impression already tracked on ad start (IAB standard).\n // Do NOT track again here — was causing 2x impression count.\n\n if (config.analytics) {\n config.analytics.track('ad_complete', {\n ad_id: ad.id,\n ad_type: ad.type,\n duration: data.duration,\n fingerprint: config.fingerprint\n });\n }\n\n // Show next ad after the configured interval\n const waitTime = ad.type === 'video' ? 0 : 0; // Videos auto-advance on end\n rotationTimer = setTimeout(showNextAd, waitTime);\n },\n\n onError: (data) => {\n onError({ ...data, phase: 'render' });\n\n if (config.analytics) {\n config.analytics.track('error', {\n ad_id: ad.id,\n error: data.error,\n fingerprint: config.fingerprint\n });\n }\n\n // Skip to next ad on error\n rotationTimer = setTimeout(showNextAd, 2000);\n }\n });\n\n // For image ads, schedule rotation after interval\n if (ad.type !== 'video') {\n const displayTime = (ad.duration || settings.image_duration || 8) * 1000;\n rotationTimer = setTimeout(showNextAd, displayTime);\n }\n\n } catch (err) {\n onError({ phase: 'render', error: err.message, ad });\n rotationTimer = setTimeout(showNextAd, 5000);\n }\n }\n\n function start() {\n if (running) return;\n running = true;\n\n loadAds().then(() => {\n showNextAd();\n });\n\n // Set up periodic ad list refresh\n refreshTimer = setInterval(async () => {\n await loadAds();\n }, refreshMinutes * 60 * 1000);\n }\n\n function stop() {\n running = false;\n\n if (rotationTimer) {\n clearTimeout(rotationTimer);\n rotationTimer = null;\n }\n if (refreshTimer) {\n clearInterval(refreshTimer);\n refreshTimer = null;\n }\n if (currentController) {\n currentController.destroy();\n currentController = null;\n }\n }\n\n function next() {\n if (!running) return;\n if (rotationTimer) {\n clearTimeout(rotationTimer);\n rotationTimer = null;\n }\n showNextAd();\n }\n\n function getStatus() {\n return {\n running,\n totalAds: ads.length,\n currentIndex,\n totalAdsShown,\n totalImpressions,\n intervalSeconds,\n settings,\n etag\n };\n }\n\n return {\n start,\n stop,\n next,\n getStatus,\n loadAds\n };\n}\n\nmodule.exports = {\n fetchAds,\n renderAd,\n trackImpression,\n parseVast,\n firePixel,\n createAdRotation\n};\n","/**\n * Trillboards Connect SDK - Offline Module\n *\n * IndexedDB-based offline cache for ads and impression queue.\n * Automatically syncs queued impressions when connectivity is restored.\n */\n\nconst DB_NAME = 'trillboards_connect';\nconst DB_VERSION = 1;\nconst AD_STORE = 'cached_ads';\nconst IMPRESSION_STORE = 'queued_impressions';\nconst META_STORE = 'metadata';\n\nconst API_BASE = 'https://api.trillboards.com/v1/partner';\n\n/**\n * Open or create the IndexedDB database.\n *\n * @returns {Promise<IDBDatabase>} Database instance\n */\nfunction openDatabase() {\n return new Promise((resolve, reject) => {\n if (typeof indexedDB === 'undefined') {\n reject(new Error('IndexedDB is not available in this environment'));\n return;\n }\n\n const request = indexedDB.open(DB_NAME, DB_VERSION);\n\n request.onupgradeneeded = (event) => {\n const db = event.target.result;\n\n // Cached ads store\n if (!db.objectStoreNames.contains(AD_STORE)) {\n const adStore = db.createObjectStore(AD_STORE, { keyPath: 'id' });\n adStore.createIndex('fingerprint', 'fingerprint', { unique: false });\n adStore.createIndex('cached_at', 'cached_at', { unique: false });\n adStore.createIndex('expires_at', 'expires_at', { unique: false });\n }\n\n // Queued impressions store\n if (!db.objectStoreNames.contains(IMPRESSION_STORE)) {\n const impStore = db.createObjectStore(IMPRESSION_STORE, { keyPath: 'queue_id', autoIncrement: true });\n impStore.createIndex('timestamp', 'timestamp', { unique: false });\n impStore.createIndex('synced', 'synced', { unique: false });\n }\n\n // Metadata store (for tracking sync state)\n if (!db.objectStoreNames.contains(META_STORE)) {\n db.createObjectStore(META_STORE, { keyPath: 'key' });\n }\n };\n\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n}\n\n/**\n * Execute a transaction on the database.\n *\n * @param {string|string[]} storeNames - Object store name(s)\n * @param {string} mode - 'readonly' or 'readwrite'\n * @param {Function} callback - Function receiving the store(s)\n * @returns {Promise<*>} Transaction result\n */\nasync function withTransaction(storeNames, mode, callback) {\n const db = await openDatabase();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(storeNames, mode);\n const stores = Array.isArray(storeNames)\n ? storeNames.map(name => tx.objectStore(name))\n : tx.objectStore(storeNames);\n\n let result;\n try {\n result = callback(stores, tx);\n } catch (err) {\n reject(err);\n return;\n }\n\n tx.oncomplete = () => {\n db.close();\n resolve(result);\n };\n tx.onerror = () => {\n db.close();\n reject(tx.error);\n };\n });\n}\n\n/**\n * Wrap an IDBRequest in a Promise.\n *\n * @param {IDBRequest} request - IndexedDB request\n * @returns {Promise<*>} Request result\n */\nfunction promisifyRequest(request) {\n return new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n}\n\n/**\n * Cache ads in IndexedDB for offline playback.\n *\n * @param {string} fingerprint - Device fingerprint\n * @param {Array} ads - Array of ad objects from fetchAds\n * @param {number} maxCacheSize - Maximum number of ads to cache (default 10)\n * @returns {Promise<number>} Number of ads cached\n */\nasync function cacheAds(fingerprint, ads, maxCacheSize = 10) {\n if (!ads || !Array.isArray(ads) || ads.length === 0) return 0;\n\n const now = new Date();\n const expiresAt = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 24 hours\n\n const adsToCache = ads.slice(0, maxCacheSize).map(ad => ({\n ...ad,\n fingerprint,\n cached_at: now.toISOString(),\n expires_at: expiresAt.toISOString()\n }));\n\n const db = await openDatabase();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(AD_STORE, 'readwrite');\n const store = tx.objectStore(AD_STORE);\n\n let count = 0;\n for (const ad of adsToCache) {\n const req = store.put(ad);\n req.onsuccess = () => count++;\n }\n\n tx.oncomplete = () => {\n db.close();\n resolve(count);\n };\n tx.onerror = () => {\n db.close();\n reject(tx.error);\n };\n });\n}\n\n/**\n * Get cached ads for a device.\n *\n * @param {string} fingerprint - Device fingerprint\n * @returns {Promise<Array>} Cached ad objects (not expired)\n */\nasync function getCachedAds(fingerprint) {\n const db = await openDatabase();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(AD_STORE, 'readonly');\n const store = tx.objectStore(AD_STORE);\n const index = store.index('fingerprint');\n const request = index.getAll(fingerprint);\n\n request.onsuccess = () => {\n const now = new Date().toISOString();\n const valid = (request.result || []).filter(ad => ad.expires_at > now);\n db.close();\n resolve(valid);\n };\n\n request.onerror = () => {\n db.close();\n reject(request.error);\n };\n });\n}\n\n/**\n * Clear expired ads from the cache.\n *\n * @returns {Promise<number>} Number of expired ads removed\n */\nasync function clearExpiredAds() {\n const db = await openDatabase();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(AD_STORE, 'readwrite');\n const store = tx.objectStore(AD_STORE);\n const index = store.index('expires_at');\n const now = new Date().toISOString();\n const range = IDBKeyRange.upperBound(now);\n const request = index.openCursor(range);\n\n let removed = 0;\n\n request.onsuccess = (event) => {\n const cursor = event.target.result;\n if (cursor) {\n cursor.delete();\n removed++;\n cursor.continue();\n }\n };\n\n tx.oncomplete = () => {\n db.close();\n resolve(removed);\n };\n tx.onerror = () => {\n db.close();\n reject(tx.error);\n };\n });\n}\n\n/**\n * Queue an impression for later sync.\n *\n * @param {Object} impressionData - Impression data to queue\n * @param {string} impressionData.adid - Ad ID\n * @param {string} impressionData.impid - Impression ID\n * @param {string} impressionData.did - Device ID\n * @param {number} impressionData.duration - View duration in seconds\n * @param {boolean} impressionData.completed - Whether the ad completed playback\n * @returns {Promise<number>} Queue ID of the stored impression\n */\nasync function queueImpression(impressionData) {\n if (!impressionData || !impressionData.adid) {\n throw new Error('impressionData with adid is required');\n }\n\n const entry = {\n ...impressionData,\n timestamp: new Date().toISOString(),\n synced: false,\n retry_count: 0\n };\n\n const db = await openDatabase();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(IMPRESSION_STORE, 'readwrite');\n const store = tx.objectStore(IMPRESSION_STORE);\n const request = store.add(entry);\n\n let queueId;\n request.onsuccess = () => { queueId = request.result; };\n\n tx.oncomplete = () => {\n db.close();\n resolve(queueId);\n };\n tx.onerror = () => {\n db.close();\n reject(tx.error);\n };\n });\n}\n\n/**\n * Get all unsynced impressions from the queue.\n *\n * @param {number} limit - Maximum number of impressions to retrieve (default 100)\n * @returns {Promise<Array>} Array of unsynced impression entries\n */\nasync function getUnsyncedImpressions(limit = 100) {\n const db = await openDatabase();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(IMPRESSION_STORE, 'readonly');\n const store = tx.objectStore(IMPRESSION_STORE);\n const index = store.index('synced');\n const request = index.getAll(false, limit);\n\n request.onsuccess = () => {\n db.close();\n resolve(request.result || []);\n };\n\n request.onerror = () => {\n db.close();\n reject(request.error);\n };\n });\n}\n\n/**\n * Mark impressions as synced after successful upload.\n *\n * @param {number[]} queueIds - Array of queue IDs to mark as synced\n * @returns {Promise<number>} Number of impressions marked\n */\nasync function markSynced(queueIds) {\n if (!queueIds || queueIds.length === 0) return 0;\n\n const db = await openDatabase();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(IMPRESSION_STORE, 'readwrite');\n const store = tx.objectStore(IMPRESSION_STORE);\n\n let count = 0;\n for (const id of queueIds) {\n const getReq = store.get(id);\n getReq.onsuccess = () => {\n const entry = getReq.result;\n if (entry) {\n entry.synced = true;\n entry.synced_at = new Date().toISOString();\n store.put(entry);\n count++;\n }\n };\n }\n\n tx.oncomplete = () => {\n db.close();\n resolve(count);\n };\n tx.onerror = () => {\n db.close();\n reject(tx.error);\n };\n });\n}\n\n/**\n * Remove synced impressions older than the specified age.\n *\n * @param {number} maxAgeHours - Maximum age in hours (default 72)\n * @returns {Promise<number>} Number of entries purged\n */\nasync function purgeSyncedImpressions(maxAgeHours = 72) {\n const cutoff = new Date(Date.now() - maxAgeHours * 60 * 60 * 1000).toISOString();\n\n const db = await openDatabase();\n return new Promise((resolve, reject) => {\n const tx = db.transaction(IMPRESSION_STORE, 'readwrite');\n const store = tx.objectStore(IMPRESSION_STORE);\n const request = store.openCursor();\n let removed = 0;\n\n request.onsuccess = (event) => {\n const cursor = event.target.result;\n if (cursor) {\n const entry = cursor.value;\n if (entry.synced && entry.timestamp < cutoff) {\n cursor.delete();\n removed++;\n }\n cursor.continue();\n }\n };\n\n tx.oncomplete = () => {\n db.close();\n resolve(removed);\n };\n tx.onerror = () => {\n db.close();\n reject(tx.error);\n };\n });\n}\n\n/**\n * Sync queued impressions to the server.\n * Sends impressions in batches via the /impressions/batch endpoint.\n *\n * @param {Object} options - Sync options\n * @param {number} options.batchSize - Number of impressions per batch (default 50)\n * @param {Function} options.onProgress - Progress callback\n * @param {Function} options.onError - Error callback\n * @returns {Promise<Object>} Sync results\n */\nasync function syncImpressions(options = {}) {\n const batchSize = options.batchSize || 50;\n const onProgress = options.onProgress || (() => {});\n const onError = options.onError || (() => {});\n\n const results = {\n total: 0,\n synced: 0,\n failed: 0,\n batches: 0\n };\n\n const unsynced = await getUnsyncedImpressions(100);\n results.total = unsynced.length;\n\n if (unsynced.length === 0) {\n return results;\n }\n\n // Process in batches\n for (let i = 0; i < unsynced.length; i += batchSize) {\n const batch = unsynced.slice(i, i + batchSize);\n\n const impressions = batch.map(entry => ({\n adid: entry.adid,\n impid: entry.impid,\n did: entry.did,\n device_fingerprint: entry.device_fingerprint,\n aid: entry.aid,\n sid: entry.sid,\n duration: entry.duration,\n completed: entry.completed,\n timestamp: entry.timestamp\n }));\n\n try {\n const response = await fetch(`${API_BASE}/impressions/batch`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-SDK-Version': '1.0.0'\n },\n body: JSON.stringify({ impressions })\n });\n\n if (response.ok) {\n const data = await response.json();\n const syncedIds = batch.map(entry => entry.queue_id);\n await markSynced(syncedIds);\n\n results.synced += data.recorded || batch.length;\n results.failed += data.failed || 0;\n } else {\n results.failed += batch.length;\n onError({\n batch: results.batches,\n status: response.status,\n count: batch.length\n });\n }\n } catch (err) {\n results.failed += batch.length;\n onError({\n batch: results.batches,\n error: err.message,\n count: batch.length\n });\n }\n\n results.batches++;\n onProgress({\n processed: Math.min(i + batchSize, unsynced.length),\n total: unsynced.length,\n synced: results.synced,\n failed: results.failed\n });\n }\n\n // Purge old synced entries\n await purgeSyncedImpressions(72).catch(() => {});\n\n return results;\n}\n\n/**\n * Create an auto-sync manager that watches for online/offline transitions\n * and syncs impressions when connectivity is restored.\n *\n * @param {Object} options - Auto-sync configuration\n * @param {number} options.syncIntervalMs - Periodic sync interval (default 300000 = 5 min)\n * @param {Function} options.onSyncComplete - Called after each sync\n * @param {Function} options.onStatusChange - Called on online/offline change\n * @returns {Object} Auto-sync controller with start/stop methods\n */\nfunction createAutoSync(options = {}) {\n const syncIntervalMs = options.syncIntervalMs || 5 * 60 * 1000;\n const onSyncComplete = options.onSyncComplete || (() => {});\n const onStatusChange = options.onStatusChange || (() => {});\n\n let intervalId = null;\n let running = false;\n let syncing = false;\n\n async function doSync() {\n if (syncing) return;\n syncing = true;\n\n try {\n const isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;\n if (!isOnline) {\n syncing = false;\n return;\n }\n\n const result = await syncImpressions({\n batchSize: 50,\n onError: (err) => {\n console.warn('[Trillboards SDK] Sync batch error:', err);\n }\n });\n\n onSyncComplete(result);\n } catch (err) {\n console.error('[Trillboards SDK] Auto-sync error:', err);\n } finally {\n syncing = false;\n }\n }\n\n function handleOnline() {\n onStatusChange({ online: true, timestamp: new Date().toISOString() });\n // Sync immediately when coming back online\n doSync();\n }\n\n function handleOffline() {\n onStatusChange({ online: false, timestamp: new Date().toISOString() });\n }\n\n function start() {\n if (running) return;\n running = true;\n\n // Listen for online/offline events\n if (typeof window !== 'undefined') {\n window.addEventListener('online', handleOnline);\n window.addEventListener('offline', handleOffline);\n }\n\n // Periodic sync\n intervalId = setInterval(doSync, syncIntervalMs);\n\n // Initial sync\n doSync();\n }\n\n function stop() {\n running = false;\n\n if (typeof window !== 'undefined') {\n window.removeEventListener('online', handleOnline);\n window.removeEventListener('offline', handleOffline);\n }\n\n if (intervalId) {\n clearInterval(intervalId);\n intervalId = null;\n }\n }\n\n function getStatus() {\n return {\n running,\n syncing,\n online: typeof navigator !== 'undefined' ? navigator.onLine : true\n };\n }\n\n return {\n start,\n stop,\n sync: doSync,\n getStatus\n };\n}\n\nmodule.exports = {\n // Database operations\n openDatabase,\n // Ad cache\n cacheAds,\n getCachedAds,\n clearExpiredAds,\n // Impression queue\n queueImpression,\n getUnsyncedImpressions,\n markSynced,\n purgeSyncedImpressions,\n syncImpressions,\n // Auto-sync\n createAutoSync\n};\n","/**\n * Trillboards Connect SDK - Branding Module\n *\n * White-label CSS injection from sdk_config.custom_css_url.\n * Default Trillboards branding that partners can override.\n */\n\nconst DEFAULT_BRANDING_ID = 'trillboards-connect-branding';\nconst CUSTOM_CSS_ID = 'trillboards-connect-custom-css';\n\n/**\n * Default Trillboards branding CSS.\n * Applied when no custom_css_url is provided by the partner.\n */\nconst DEFAULT_CSS = `\n/* Trillboards Connect SDK - Default Branding */\n.trillboards-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n background-color: #000000;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;\n}\n\n.trillboards-container video,\n.trillboards-container img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n\n.trillboards-overlay {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n padding: 8px 16px;\n background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));\n color: #ffffff;\n font-size: 12px;\n opacity: 0;\n transition: opacity 0.3s ease;\n pointer-events: none;\n}\n\n.trillboards-container:hover .trillboards-overlay {\n opacity: 1;\n}\n\n.trillboards-badge {\n position: absolute;\n top: 8px;\n right: 8px;\n background: rgba(0, 0, 0, 0.5);\n color: #ffffff;\n font-size: 10px;\n padding: 2px 6px;\n border-radius: 4px;\n opacity: 0.6;\n transition: opacity 0.3s ease;\n pointer-events: none;\n z-index: 10;\n}\n\n.trillboards-badge:hover {\n opacity: 1;\n}\n\n.trillboards-loading {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 40px;\n height: 40px;\n border: 3px solid rgba(255, 255, 255, 0.2);\n border-top: 3px solid #ffffff;\n border-radius: 50%;\n animation: trillboards-spin 1s linear infinite;\n}\n\n@keyframes trillboards-spin {\n 0% { transform: translate(-50%, -50%) rotate(0deg); }\n 100% { transform: translate(-50%, -50%) rotate(360deg); }\n}\n\n.trillboards-error {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n color: #999999;\n font-size: 14px;\n text-align: center;\n}\n\n.trillboards-skip-btn {\n position: absolute;\n bottom: 16px;\n right: 16px;\n background: rgba(255, 255, 255, 0.15);\n color: #ffffff;\n border: 1px solid rgba(255, 255, 255, 0.3);\n padding: 8px 16px;\n font-size: 12px;\n border-radius: 4px;\n cursor: pointer;\n transition: background 0.2s ease;\n z-index: 10;\n}\n\n.trillboards-skip-btn:hover {\n background: rgba(255, 255, 255, 0.25);\n}\n`;\n\n/**\n * Inject default Trillboards branding CSS into the document.\n * Idempotent - won't inject twice.\n *\n * @returns {HTMLStyleElement|null} The injected style element, or null if not in a browser\n */\nfunction injectDefaultBranding() {\n if (typeof document === 'undefined') return null;\n\n // Don't inject twice\n if (document.getElementById(DEFAULT_BRANDING_ID)) {\n return document.getElementById(DEFAULT_BRANDING_ID);\n }\n\n const style = document.createElement('style');\n style.id = DEFAULT_BRANDING_ID;\n style.type = 'text/css';\n style.textContent = DEFAULT_CSS;\n document.head.appendChild(style);\n\n return style;\n}\n\n/**\n * Load and inject custom CSS from a partner's custom_css_url.\n * This CSS is loaded after the default branding, so it can override any default styles.\n *\n * @param {string} cssUrl - URL to the custom CSS file\n * @param {Object} options - Loading options\n * @param {number} options.timeout - Timeout in milliseconds (default 5000)\n * @param {Function} options.onLoad - Called when CSS is loaded\n * @param {Function} options.onError - Called on load error\n * @returns {Promise<HTMLLinkElement|null>} The injected link element\n */\nasync function loadCustomCss(cssUrl, options = {}) {\n if (typeof document === 'undefined') return null;\n\n if (!cssUrl || typeof cssUrl !== 'string') {\n return null;\n }\n\n const timeout = options.timeout || 5000;\n const onLoad = options.onLoad || (() => {});\n const onError = options.onError || (() => {});\n\n // Remove existing custom CSS\n const existing = document.getElementById(CUSTOM_CSS_ID);\n if (existing) {\n existing.parentNode.removeChild(existing);\n }\n\n return new Promise((resolve) => {\n const link = document.createElement('link');\n link.id = CUSTOM_CSS_ID;\n link.rel = 'stylesheet';\n link.type = 'text/css';\n link.href = cssUrl;\n\n const timer = setTimeout(() => {\n onError({ error: 'CSS load timeout', url: cssUrl });\n resolve(null);\n }, timeout);\n\n link.onload = () => {\n clearTimeout(timer);\n onLoad({ url: cssUrl });\n resolve(link);\n };\n\n link.onerror = () => {\n clearTimeout(timer);\n onError({ error: 'CSS load failed', url: cssUrl });\n resolve(null);\n };\n\n document.head.appendChild(link);\n });\n}\n\n/**\n * Remove all Trillboards branding (default + custom) from the document.\n */\nfunction removeBranding() {\n if (typeof document === 'undefined') return;\n\n const defaultStyle = document.getElementById(DEFAULT_BRANDING_ID);\n if (defaultStyle) defaultStyle.parentNode.removeChild(defaultStyle);\n\n const customLink = document.getElementById(CUSTOM_CSS_ID);\n if (customLink) customLink.parentNode.removeChild(customLink);\n}\n\n/**\n * Apply branding based on SDK configuration.\n * Injects default branding, then loads custom CSS if provided.\n *\n * @param {Object} sdkConfig - Partner's sdk_config from the API\n * @param {string} sdkConfig.custom_css_url - URL to partner's custom CSS\n * @returns {Promise<Object>} Branding state\n */\nasync function applyBranding(sdkConfig = {}) {\n const state = {\n defaultApplied: false,\n customApplied: false,\n customUrl: null\n };\n\n // Apply default branding\n const defaultEl = injectDefaultBranding();\n state.defaultApplied = !!defaultEl;\n\n // Load custom CSS if configured\n if (sdkConfig.custom_css_url) {\n state.customUrl = sdkConfig.custom_css_url;\n const customEl = await loadCustomCss(sdkConfig.custom_css_url);\n state.customApplied = !!customEl;\n }\n\n return state;\n}\n\nmodule.exports = {\n injectDefaultBranding,\n loadCustomCss,\n removeBranding,\n applyBranding,\n DEFAULT_CSS\n};\n","/**\n * Trillboards Connect SDK - Analytics Module\n *\n * Client-side event buffering and batch sending.\n * Buffers events and sends them in batches (every 30s or when buffer hits 50 events).\n */\n\nconst API_BASE = 'https://api.trillboards.com/v1/partner';\n\n// Valid event types\nconst EventTypes = {\n AD_START: 'ad_start',\n AD_COMPLETE: 'ad_complete',\n AD_SKIP: 'ad_skip',\n AD_ERROR: 'ad_error',\n ERROR: 'error',\n HEARTBEAT: 'heartbeat',\n SDK_INIT: 'sdk_init',\n SDK_ERROR: 'sdk_error',\n DEVICE_REGISTERED: 'device_registered',\n OFFLINE_SYNC: 'offline_sync',\n AD_IMPRESSION: 'ad_impression',\n VISIBILITY_CHANGE: 'visibility_change'\n};\n\n/**\n * Create an analytics tracker instance.\n *\n * @param {Object} config - Analytics configuration\n * @param {string} config.fingerprint - Device fingerprint\n * @param {string} config.apiKey - Partner API key (optional, for authenticated endpoints)\n * @param {number} config.flushIntervalMs - Flush interval in milliseconds (default 30000)\n * @param {number} config.maxBufferSize - Max events before forced flush (default 50)\n * @param {string} config.endpoint - Custom analytics endpoint (default: /impressions/batch)\n * @param {Function} config.onFlush - Called after each flush\n * @param {Function} config.onError - Called on flush error\n * @param {boolean} config.enabled - Enable/disable analytics (default true)\n * @returns {Object} Analytics tracker with track/flush/destroy methods\n */\nfunction createAnalytics(config = {}) {\n const fingerprint = config.fingerprint || 'unknown';\n const apiKey = config.apiKey || null;\n const flushIntervalMs = config.flushIntervalMs || 30000;\n const maxBufferSize = config.maxBufferSize || 50;\n const onFlush = config.onFlush || (() => {});\n const onError = config.onError || (() => {});\n const enabled = config.enabled !== false;\n\n let buffer = [];\n let flushTimer = null;\n let flushing = false;\n let running = false;\n let totalTracked = 0;\n let totalFlushed = 0;\n let totalErrors = 0;\n let sessionId = generateSessionId();\n\n /**\n * Generate a unique session ID\n */\n function generateSessionId() {\n const timestamp = Date.now().toString(36);\n const random = Math.random().toString(36).substring(2, 8);\n return `sess_${timestamp}_${random}`;\n }\n\n /**\n * Track an event. The event is buffered and will be sent in the next batch.\n *\n * @param {string} eventType - Event type (from EventTypes)\n * @param {Object} data - Event data\n * @returns {Object} The tracked event object\n */\n function track(eventType, data = {}) {\n if (!enabled || !running) return null;\n\n const event = {\n event: eventType,\n fingerprint,\n session_id: sessionId,\n timestamp: new Date().toISOString(),\n data: {\n ...data,\n sdk_version: '1.0.0'\n }\n };\n\n buffer.push(event);\n totalTracked++;\n\n // Auto-flush if buffer is full\n if (buffer.length >= maxBufferSize) {\n flush();\n }\n\n return event;\n }\n\n /**\n * Flush the event buffer to the server.\n *\n * @returns {Promise<Object>} Flush result\n */\n async function flush() {\n if (flushing || buffer.length === 0) {\n return { flushed: 0, errors: 0 };\n }\n\n flushing = true;\n const eventsToSend = buffer.splice(0);\n\n try {\n // Group events by type for efficient sending\n const adEvents = eventsToSend.filter(e =>\n [EventTypes.AD_START, EventTypes.AD_COMPLETE, EventTypes.AD_SKIP, EventTypes.AD_IMPRESSION].includes(e.event)\n );\n const otherEvents = eventsToSend.filter(e =>\n ![EventTypes.AD_START, EventTypes.AD_COMPLETE, EventTypes.AD_SKIP, EventTypes.AD_IMPRESSION].includes(e.event)\n );\n\n let flushed = 0;\n\n // Send ad events as impression batch (fits the existing API)\n if (adEvents.length > 0) {\n const impressions = adEvents\n .filter(e => e.data && e.data.ad_id)\n .map(e => ({\n adid: e.data.ad_id,\n impid: e.data.impression_id || `sdk_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,\n did: e.fingerprint,\n device_fingerprint: e.fingerprint,\n duration: e.data.duration || 0,\n completed: e.event === EventTypes.AD_COMPLETE,\n timestamp: e.timestamp\n }));\n\n if (impressions.length > 0) {\n const headers = {\n 'Content-Type': 'application/json',\n 'X-SDK-Version': '1.0.0'\n };\n if (apiKey) {\n headers['Authorization'] = `Bearer ${apiKey}`;\n }\n\n const response = await fetch(`${API_BASE}/impressions/batch`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ impressions })\n });\n\n if (response.ok) {\n flushed += impressions.length;\n } else {\n throw new Error(`Batch send failed: ${response.status}`);\n }\n }\n\n // Count non-impression ad events as flushed (they were tracked locally)\n flushed += adEvents.length - adEvents.filter(e => e.data && e.data.ad_id).length;\n }\n\n // Other events are tracked locally (no server endpoint for generic events yet)\n flushed += otherEvents.length;\n\n totalFlushed += flushed;\n\n const result = { flushed, errors: 0, timestamp: new Date().toISOString() };\n onFlush(result);\n return result;\n\n } catch (err) {\n totalErrors++;\n\n // Put events back in buffer for retry\n buffer.unshift(...eventsToSend);\n\n // Cap buffer to prevent unbounded growth\n if (buffer.length > maxBufferSize * 3) {\n buffer = buffer.slice(-maxBufferSize * 2);\n }\n\n const errorResult = { flushed: 0, errors: eventsToSend.length, error: err.message };\n onError(errorResult);\n return errorResult;\n\n } finally {\n flushing = false;\n }\n }\n\n /**\n * Start the analytics tracker (begins periodic flushing).\n */\n function start() {\n if (running) return;\n running = true;\n sessionId = generateSessionId();\n\n flushTimer = setInterval(() => {\n flush();\n }, flushIntervalMs);\n\n // Flush on page unload (if in browser)\n if (typeof window !== 'undefined') {\n window.addEventListener('beforeunload', handleBeforeUnload);\n document.addEventListener('visibilitychange', handleVisibilityChange);\n }\n\n // Track SDK init event\n track(EventTypes.SDK_INIT, {\n fingerprint,\n flush_interval_ms: flushIntervalMs,\n max_buffer_size: maxBufferSize\n });\n }\n\n /**\n * Stop the analytics tracker and flush remaining events.\n */\n async function stop() {\n running = false;\n\n if (flushTimer) {\n clearInterval(flushTimer);\n flushTimer = null;\n }\n\n if (typeof window !== 'undefined') {\n window.removeEventListener('beforeunload', handleBeforeUnload);\n document.removeEventListener('visibilitychange', handleVisibilityChange);\n }\n\n // Final flush\n await flush();\n }\n\n function handleBeforeUnload() {\n // Use sendBeacon for reliability on page unload\n if (buffer.length > 0 && typeof navigator !== 'undefined' && navigator.sendBeacon) {\n const adEvents = buffer.filter(e => e.data && e.data.ad_id);\n if (adEvents.length > 0) {\n const impressions = adEvents.map(e => ({\n adid: e.data.ad_id,\n impid: e.data.impression_id || `sdk_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,\n did: e.fingerprint,\n duration: e.data.duration || 0,\n completed: e.event === EventTypes.AD_COMPLETE,\n timestamp: e.timestamp\n }));\n\n const blob = new Blob(\n [JSON.stringify({ impressions })],\n { type: 'application/json' }\n );\n navigator.sendBeacon(`${API_BASE}/impressions/batch`, blob);\n }\n buffer = [];\n }\n }\n\n function handleVisibilityChange() {\n if (document.visibilityState === 'hidden') {\n track(EventTypes.VISIBILITY_CHANGE, { visible: false });\n flush();\n } else {\n track(EventTypes.VISIBILITY_CHANGE, { visible: true });\n }\n }\n\n /**\n * Get analytics statistics.\n *\n * @returns {Object} Current analytics state\n */\n function getStats() {\n return {\n running,\n enabled,\n sessionId,\n bufferSize: buffer.length,\n totalTracked,\n totalFlushed,\n totalErrors,\n fingerprint\n };\n }\n\n /**\n * Destroy the analytics tracker and clean up all resources.\n */\n async function destroy() {\n await stop();\n buffer = [];\n totalTracked = 0;\n totalFlushed = 0;\n totalErrors = 0;\n }\n\n return {\n track,\n flush,\n start,\n stop,\n destroy,\n getStats,\n EventTypes\n };\n}\n\nmodule.exports = {\n createAnalytics,\n EventTypes\n};\n","/**\n * Trillboards Connect SDK\n *\n * Zero-friction partner onboarding SDK for digital signage ad monetization.\n *\n * Usage:\n * const trillboards = TrillboardsConnect.init({\n * apiKey: 'trb_partner_xxx',\n * deviceId: 'my-screen-001',\n * containerId: 'ad-container',\n * options: {\n * heartbeatInterval: 60000,\n * adInterval: 60,\n * muted: true,\n * offlineCache: true,\n * analytics: true\n * }\n * });\n *\n * Exports as both CommonJS module and window.TrillboardsConnect for browser use.\n */\n\nconst { DeviceTracker, DeviceStatus, generateFingerprint, registerDevice, startHeartbeat } = require('./device');\nconst { fetchAds, renderAd, trackImpression, parseVast, createAdRotation } = require('./auction');\nconst { cacheAds, getCachedAds, queueImpression, syncImpressions, createAutoSync, clearExpiredAds } = require('./offline');\nconst { applyBranding, injectDefaultBranding, loadCustomCss, removeBranding } = require('./branding');\nconst { createAnalytics, EventTypes } = require('./analytics');\n\nconst SDK_VERSION = '1.0.0';\nconst API_BASE = 'https://api.trillboards.com/v1/partner';\n\n/**\n * Validate initialization configuration.\n *\n * @param {Object} config - Init configuration\n * @throws {Error} If required fields are missing\n */\nfunction validateConfig(config) {\n if (!config) {\n throw new Error('TrillboardsConnect.init() requires a configuration object');\n }\n if (!config.apiKey) {\n throw new Error('apiKey is required. Get your API key from https://trillboards.com/earner');\n }\n if (!config.deviceId) {\n throw new Error('deviceId is required. Provide a unique identifier for this device.');\n }\n}\n\n/**\n * Resolve a container element from an ID string or DOM element.\n *\n * @param {string|HTMLElement} containerId - Container ID or DOM element\n * @returns {HTMLElement|null} The resolved container element\n */\nfunction resolveContainer(containerId) {\n if (!containerId) return null;\n if (typeof containerId === 'string') {\n return typeof document !== 'undefined' ? document.getElementById(containerId) : null;\n }\n return containerId;\n}\n\n/**\n * Main SDK state object.\n * Tracks the lifecycle and provides access to all subsystems.\n */\nclass ConnectInstance {\n constructor(config) {\n this.config = config;\n this.apiKey = config.apiKey;\n this.deviceId = config.deviceId;\n this.containerId = config.containerId;\n this.options = config.options || {};\n\n // Subsystems\n this.deviceTracker = null;\n this.adRotation = null;\n this.autoSync = null;\n this.analytics = null;\n this.socketConnection = null;\n\n // State\n this.initialized = false;\n this.destroyed = false;\n this.fingerprint = null;\n this.screenId = null;\n this.embedUrl = null;\n this.sdkConfig = null;\n\n // Event listeners\n this._listeners = {};\n }\n\n /**\n * Register an event listener.\n *\n * @param {string} event - Event name\n * @param {Function} callback - Event handler\n * @returns {ConnectInstance} This instance for chaining\n */\n on(event, callback) {\n if (!this._listeners[event]) {\n this._listeners[event] = [];\n }\n this._listeners[event].push(callback);\n return this;\n }\n\n /**\n * Remove an event listener.\n *\n * @param {string} event - Event name\n * @param {Function} callback - Handler to remove\n * @returns {ConnectInstance} This instance for chaining\n */\n off(event, callback) {\n if (this._listeners[event]) {\n this._listeners[event] = this._listeners[event].filter(cb => cb !== callback);\n }\n return this;\n }\n\n /**\n * Emit an event to all registered listeners.\n *\n * @param {string} event - Event name\n * @param {Object} data - Event data\n */\n _emit(event, data) {\n if (this._listeners[event]) {\n this._listeners[event].forEach(cb => {\n try {\n cb(data);\n } catch (err) {\n console.error(`[Trillboards SDK] Listener error for '${event}':`, err);\n }\n });\n }\n }\n\n /**\n * Initialize the SDK: register device, start heartbeat, set up ad rotation.\n *\n * @returns {Promise<ConnectInstance>} This instance\n */\n async initialize() {\n if (this.initialized) {\n console.warn('[Trillboards SDK] Already initialized');\n return this;\n }\n\n this._emit('initializing', { deviceId: this.deviceId, timestamp: new Date().toISOString() });\n\n // Step 1: Set up analytics (if enabled)\n if (this.options.analytics !== false) {\n const tempFingerprint = generateFingerprint(this.deviceId);\n this.analytics = createAnalytics({\n fingerprint: tempFingerprint,\n apiKey: this.apiKey,\n flushIntervalMs: this.options.analyticsFlushInterval || 30000,\n maxBufferSize: this.options.analyticsBufferSize || 50,\n onFlush: (result) => {\n this._emit('analytics_flush', result);\n },\n onError: (err) => {\n this._emit('analytics_error', err);\n }\n });\n this.analytics.start();\n }\n\n // Step 2: Register device\n this.deviceTracker = new DeviceTracker(this.deviceId);\n\n // Forward device events\n this.deviceTracker.on('status_change', (data) => this._emit('device_status', data));\n this.deviceTracker.on('registered', (data) => this._emit('device_registered', data));\n this.deviceTracker.on('error', (data) => this._emit('device_error', data));\n this.deviceTracker.on('heartbeat', (data) => this._emit('heartbeat', data));\n this.deviceTracker.on('heartbeat_error', (data) => this._emit('heartbeat_error', data));\n\n try {\n const registrationResult = await this.deviceTracker.register(this.apiKey, {\n device_type: this.options.deviceType || 'digital_signage',\n name: this.options.deviceName || `SDK Device: ${this.deviceId}`,\n display: this.options.display,\n location: this.options.location,\n custom: {\n sdk_version: SDK_VERSION,\n ...(this.options.metadata || {})\n }\n }, {\n interval: this.options.heartbeatInterval || 60000,\n maxRetries: this.options.heartbeatMaxRetries || 10,\n retryBackoff: 1.5\n });\n\n this.fingerprint = registrationResult.fingerprint;\n this.screenId = registrationResult.screen_id;\n this.embedUrl = registrationResult.embed_url;\n\n // Update analytics fingerprint now that we have the real one\n if (this.analytics) {\n this.analytics.destroy();\n this.analytics = createAnalytics({\n fingerprint: this.fingerprint,\n apiKey: this.apiKey,\n flushIntervalMs: this.options.analyticsFlushInterval || 30000,\n maxBufferSize: this.options.analyticsBufferSize || 50,\n onFlush: (result) => this._emit('analytics_flush', result),\n onError: (err) => this._emit('analytics_error', err)\n });\n this.analytics.start();\n\n this.analytics.track(EventTypes.DEVICE_REGISTERED, {\n fingerprint: this.fingerprint,\n screen_id: this.screenId\n });\n }\n\n } catch (err) {\n this._emit('error', {\n phase: 'device_registration',\n error: err.message,\n statusCode: err.statusCode\n });\n\n if (this.analytics) {\n this.analytics.track(EventTypes.SDK_ERROR, {\n phase: 'device_registration',\n error: err.message\n });\n }\n\n throw err;\n }\n\n // Step 3: Fetch partner SDK config\n try {\n const infoResponse = await fetch(`${API_BASE}/info`, {\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n 'X-SDK-Version': SDK_VERSION\n }\n });\n if (infoResponse.ok) {\n const partnerInfo = await infoResponse.json();\n this.sdkConfig = partnerInfo.sdk_config || {};\n }\n } catch (err) {\n // Non-fatal - use defaults\n this.sdkConfig = {};\n }\n\n // Step 4: Apply branding\n try {\n await applyBranding(this.sdkConfig);\n } catch (err) {\n // Non-fatal\n console.warn('[Trillboards SDK] Branding error:', err.message);\n }\n\n // Step 5: Set up ad rotation (if container provided)\n const container = resolveContainer(this.containerId);\n if (container && this.fingerprint) {\n this.adRotation = createAdRotation({\n fingerprint: this.fingerprint,\n container,\n intervalSeconds: this.sdkConfig.ad_interval || this.options.adInterval || 60,\n refreshMinutes: this.options.adRefreshMinutes || 5,\n muted: this.options.muted !== false,\n objectFit: this.options.objectFit || 'cover',\n analytics: this.analytics,\n\n onAdStart: (data) => {\n this._emit('ad_start', data);\n },\n onAdComplete: (data) => {\n this._emit('ad_complete', data);\n },\n onError: (data) => {\n this._emit('ad_error', data);\n },\n onImpression: (data) => {\n this._emit('impression', data);\n }\n });\n\n this.adRotation.start();\n }\n\n // Step 6: Set up offline sync (if enabled)\n if (this.options.offlineCache !== false && typeof indexedDB !== 'undefined') {\n this.autoSync = createAutoSync({\n syncIntervalMs: this.options.syncInterval || 5 * 60 * 1000,\n onSyncComplete: (result) => {\n this._emit('sync_complete', result);\n\n if (this.analytics) {\n this.analytics.track(EventTypes.OFFLINE_SYNC, {\n synced: result.synced,\n failed: result.failed\n });\n }\n },\n onStatusChange: (status) => {\n this._emit('connectivity_change', status);\n }\n });\n this.autoSync.start();\n }\n\n // Step 7: Set up Socket.io connection (if available)\n this._setupSocket();\n\n this.initialized = true;\n this._emit('ready', {\n deviceId: this.deviceId,\n fingerprint: this.fingerprint,\n screenId: this.screenId,\n embedUrl: this.embedUrl,\n timestamp: new Date().toISOString()\n });\n\n return this;\n }\n\n /**\n * Set up Socket.io connection for real-time commands.\n * Connects to the API's Socket.io endpoint for live updates.\n */\n _setupSocket() {\n // Socket.io is optional - only set up if io is globally available\n const io = typeof window !== 'undefined' && window.io ? window.io : null;\n\n if (!io || !this.fingerprint) return;\n\n try {\n this.socketConnection = io(API_BASE.replace('/v1/partner', ''), {\n transports: ['websocket', 'polling'],\n query: {\n fingerprint: this.fingerprint,\n sdk_version: SDK_VERSION,\n type: 'partner_sdk'\n },\n reconnection: true,\n reconnectionAttempts: 10,\n reconnectionDelay: 1000,\n reconnectionDelayMax: 30000\n });\n\n this.socketConnection.on('connect', () => {\n this._emit('socket_connected', { socketId: this.socketConnection.id });\n\n // Join the screen room\n this.socketConnection.emit('join_screen', {\n fingerprint: this.fingerprint,\n screen_id: this.screenId\n });\n });\n\n this.socketConnection.on('disconnect', (reason) => {\n this._emit('socket_disconnected', { reason });\n });\n\n // Listen for real-time commands\n this.socketConnection.on('refresh_ads', () => {\n if (this.adRotation) {\n this.adRotation.loadAds().then(() => {\n this._emit('ads_refreshed', { source: 'socket' });\n });\n }\n });\n\n this.socketConnection.on('update_config', (data) => {\n if (data && data.sdk_config) {\n this.sdkConfig = { ...this.sdkConfig, ...data.sdk_config };\n this._emit('config_updated', { config: this.sdkConfig, source: 'socket' });\n }\n });\n\n this.socketConnection.on('force_reload', () => {\n this._emit('force_reload', { timestamp: new Date().toISOString() });\n });\n\n this.socketConnection.on('content_update', (data) => {\n this._emit('content_update', data);\n if (this.adRotation) {\n this.adRotation.loadAds();\n }\n });\n\n this.socketConnection.on('connect_error', (err) => {\n this._emit('socket_error', { error: err.message });\n });\n\n } catch (err) {\n // Socket.io is optional, don't fail init\n console.warn('[Trillboards SDK] Socket.io setup failed:', err.message);\n }\n }\n\n /**\n * Manually trigger the next ad in rotation.\n */\n nextAd() {\n if (this.adRotation) {\n this.adRotation.next();\n }\n }\n\n /**\n * Pause ad rotation.\n */\n pause() {\n if (this.adRotation) {\n this.adRotation.stop();\n }\n }\n\n /**\n * Resume ad rotation.\n */\n resume() {\n if (this.adRotation) {\n this.adRotation.start();\n }\n }\n\n /**\n * Get the current state of all subsystems.\n *\n * @returns {Object} Complete SDK state\n */\n getState() {\n return {\n version: SDK_VERSION,\n initialized: this.initialized,\n destroyed: this.destroyed,\n device: this.deviceTracker ? this.deviceTracker.getState() : null,\n adRotation: this.adRotation ? this.adRotation.getStatus() : null,\n offlineSync: this.autoSync ? this.autoSync.getStatus() : null,\n analytics: this.analytics ? this.analytics.getStats() : null,\n socket: this.socketConnection ? {\n connected: this.socketConnection.connected,\n id: this.socketConnection.id\n } : null,\n config: this.sdkConfig\n };\n }\n\n /**\n * Queue an impression for offline sync.\n *\n * @param {Object} impressionData - Impression data\n * @returns {Promise<number>} Queue ID\n */\n async queueOfflineImpression(impressionData) {\n return queueImpression({\n ...impressionData,\n device_fingerprint: this.fingerprint\n });\n }\n\n /**\n * Force sync of offline impressions.\n *\n * @returns {Promise<Object>} Sync results\n */\n async syncOffline() {\n return syncImpressions({\n batchSize: 50,\n onError: (err) => {\n this._emit('sync_error', err);\n }\n });\n }\n\n /**\n * Cache ads for offline use.\n *\n * @param {Array} ads - Array of ad objects\n * @returns {Promise<number>} Number of ads cached\n */\n async cacheAdsOffline(ads) {\n if (!this.fingerprint) return 0;\n const maxCache = this.sdkConfig?.cache_size || this.options.cacheSize || 10;\n return cacheAds(this.fingerprint, ads, maxCache);\n }\n\n /**\n * Destroy the SDK instance and clean up all resources.\n */\n async destroy() {\n if (this.destroyed) return;\n this.destroyed = true;\n\n this._emit('destroying', { timestamp: new Date().toISOString() });\n\n // Stop ad rotation\n if (this.adRotation) {\n this.adRotation.stop();\n this.adRotation = null;\n }\n\n // Disconnect socket\n if (this.socketConnection) {\n this.socketConnection.disconnect();\n this.socketConnection = null;\n }\n\n // Stop offline sync\n if (this.autoSync) {\n this.autoSync.stop();\n this.autoSync = null;\n }\n\n // Flush and stop analytics\n if (this.analytics) {\n await this.analytics.destroy();\n this.analytics = null;\n }\n\n // Stop heartbeat and clean up device tracker\n if (this.deviceTracker) {\n this.deviceTracker.destroy();\n this.deviceTracker = null;\n }\n\n // Remove branding\n removeBranding();\n\n this.initialized = false;\n this._listeners = {};\n }\n}\n\n/**\n * TrillboardsConnect - Main SDK entry point.\n *\n * @example\n * const trillboards = TrillboardsConnect.init({\n * apiKey: 'trb_partner_xxx',\n * deviceId: 'screen-lobby-01',\n * containerId: 'ad-container',\n * options: {\n * heartbeatInterval: 60000,\n * adInterval: 60,\n * muted: true,\n * offlineCache: true,\n * analytics: true\n * }\n * });\n *\n * trillboards.on('ready', (data) => {\n * console.log('SDK ready:', data.fingerprint);\n * });\n *\n * trillboards.on('ad_start', (data) => {\n * console.log('Ad started:', data.ad.id);\n * });\n */\nconst TrillboardsConnect = {\n /**\n * SDK version\n */\n version: SDK_VERSION,\n\n /**\n * Initialize the SDK with configuration.\n *\n * @param {Object} config - SDK configuration\n * @param {string} config.apiKey - Partner API key (trb_partner_xxx)\n * @param {string} config.deviceId - Unique device identifier\n * @param {string|HTMLElement} config.containerId - DOM container for ad rendering\n * @param {Object} config.options - Additional options\n * @param {number} config.options.heartbeatInterval - Heartbeat interval ms (default 60000)\n * @param {number} config.options.adInterval - Ad rotation interval seconds (default 60)\n * @param {boolean} config.options.muted - Start ads muted (default true)\n * @param {boolean} config.options.offlineCache - Enable offline caching (default true)\n * @param {boolean} config.options.analytics - Enable analytics (default true)\n * @param {string} config.options.deviceType - Device type (default 'digital_signage')\n * @param {string} config.options.deviceName - Friendly device name\n * @param {Object} config.options.display - Display specs { width, height }\n * @param {Object} config.options.location - Location { lat, lng, address, venue_type }\n * @param {Object} config.options.metadata - Custom metadata\n * @returns {ConnectInstance} SDK instance (call .on('ready', ...) for initialization)\n */\n init(config) {\n validateConfig(config);\n\n const instance = new ConnectInstance(config);\n\n // Start initialization asynchronously\n instance.initialize().catch(err => {\n console.error('[Trillboards SDK] Initialization failed:', err.message);\n instance._emit('error', {\n phase: 'init',\n error: err.message,\n fatal: true\n });\n });\n\n return instance;\n },\n\n /**\n * Create an instance without auto-initialization (for advanced use cases).\n *\n * @param {Object} config - SDK configuration\n * @returns {ConnectInstance} Uninitialized SDK instance\n */\n create(config) {\n validateConfig(config);\n return new ConnectInstance(config);\n },\n\n // Expose sub-modules for advanced usage\n device: {\n register: registerDevice,\n generateFingerprint,\n startHeartbeat,\n DeviceTracker,\n DeviceStatus\n },\n\n auction: {\n fetchAds,\n renderAd,\n trackImpression,\n parseVast,\n createAdRotation\n },\n\n offline: {\n cacheAds,\n getCachedAds,\n queueImpression,\n syncImpressions,\n createAutoSync,\n clearExpiredAds\n },\n\n branding: {\n applyBranding,\n injectDefaultBranding,\n loadCustomCss,\n removeBranding\n },\n\n analytics: {\n createAnalytics,\n EventTypes\n }\n};\n\n// Export for module systems\nmodule.exports = TrillboardsConnect;\nmodule.exports.TrillboardsConnect = TrillboardsConnect;\nmodule.exports.ConnectInstance = ConnectInstance;\n\n// Export for browser global (UMD pattern)\nif (typeof window !== 'undefined') {\n window.TrillboardsConnect = TrillboardsConnect;\n}\n"],"mappings":"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","names":["require_device","__commonJSMin","exports","module","API_BASE","DeviceStatus","generateFingerprint","deviceId","hash","i","hashHex","extHash","extHex","collectDeviceMetadata","metadata","registerDevice","apiKey","browserMetadata","displayInfo","body","__spreadValues","response","errorData","err","startHeartbeat","fingerprint","intervalMs","options","maxRetries","retryBackoff","onSuccess","onError","timerId","consecutiveFailures","totalBeats","lastBeatAt","running","currentInterval","sendBeat","data","stop","scheduleNext","start","getStatus","DeviceTracker","event","callback","cb","newStatus","oldStatus","heartbeatOptions","result","interval","require_auction","__commonJSMin","exports","module","API_BASE","parseVast","vastXml","doc","result","imp","url","err","mf","te","event","clickThrough","ct","duration","parts","firePixel","resolve","separator","pixelUrl","img","fetchAds","fingerprint","options","headers","response","errorData","data","renderAd","ad","container","containerEl","muted","autoplay","objectFit","onStart","onComplete","onError","element","completionTimer","startTime","destroyed","e","trackImpression","results","vastUrl","fired","createAdRotation","config","intervalSeconds","refreshMinutes","onAdStart","onAdComplete","onImpression","ads","settings","currentIndex","currentController","rotationTimer","refreshTimer","running","etag","totalAdsShown","totalImpressions","loadAds","getNextAd","showNextAd","__spreadProps","__spreadValues","waitTime","displayTime","start","stop","next","getStatus","require_offline","__commonJSMin","exports","module","DB_NAME","AD_STORE","IMPRESSION_STORE","META_STORE","API_BASE","openDatabase","resolve","reject","request","event","db","adStore","impStore","cacheAds","fingerprint","ads","maxCacheSize","now","expiresAt","adsToCache","ad","__spreadProps","__spreadValues","tx","store","count","req","getCachedAds","valid","clearExpiredAds","index","range","removed","cursor","queueImpression","impressionData","entry","queueId","getUnsyncedImpressions","limit","markSynced","queueIds","id","getReq","purgeSyncedImpressions","maxAgeHours","cutoff","syncImpressions","options","batchSize","onProgress","onError","results","unsynced","i","batch","impressions","response","data","syncedIds","err","createAutoSync","syncIntervalMs","onSyncComplete","onStatusChange","intervalId","running","syncing","doSync","result","handleOnline","handleOffline","start","stop","getStatus","require_branding","__commonJSMin","exports","module","DEFAULT_BRANDING_ID","CUSTOM_CSS_ID","DEFAULT_CSS","injectDefaultBranding","style","loadCustomCss","cssUrl","options","timeout","onLoad","onError","existing","resolve","link","timer","removeBranding","defaultStyle","customLink","applyBranding","sdkConfig","state","defaultEl","customEl","require_analytics","__commonJSMin","exports","module","API_BASE","EventTypes","createAnalytics","config","fingerprint","apiKey","flushIntervalMs","maxBufferSize","onFlush","onError","enabled","buffer","flushTimer","flushing","running","totalTracked","totalFlushed","totalErrors","sessionId","generateSessionId","timestamp","random","track","eventType","data","event","__spreadProps","__spreadValues","flush","eventsToSend","adEvents","e","otherEvents","flushed","impressions","headers","response","result","err","errorResult","start","handleBeforeUnload","handleVisibilityChange","stop","blob","getStats","destroy","require_src","__commonJSMin","exports","module","DeviceTracker","DeviceStatus","generateFingerprint","registerDevice","startHeartbeat","fetchAds","renderAd","trackImpression","parseVast","createAdRotation","cacheAds","getCachedAds","queueImpression","syncImpressions","createAutoSync","clearExpiredAds","applyBranding","injectDefaultBranding","loadCustomCss","removeBranding","createAnalytics","EventTypes","SDK_VERSION","API_BASE","validateConfig","config","resolveContainer","containerId","ConnectInstance","event","callback","cb","data","err","tempFingerprint","result","registrationResult","__spreadValues","infoResponse","partnerInfo","container","status","io","reason","impressionData","__spreadProps","ads","_a","maxCache","TrillboardsConnect","instance"]}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@trillboards/connect",
3
+ "version": "1.0.0",
4
+ "description": "Trillboards Connect SDK - Zero-friction partner onboarding for digital signage ad monetization",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "browser": "./dist/trillboards-connect.global.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "CHANGELOG.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "test": "jest --coverage",
22
+ "test:watch": "jest --watch",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "keywords": [
26
+ "trillboards",
27
+ "digital-signage",
28
+ "ad-monetization",
29
+ "dooh",
30
+ "programmatic",
31
+ "vast",
32
+ "sdk",
33
+ "connect"
34
+ ],
35
+ "author": "Trillboards <engineering@trillboards.com>",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/trillboards/packages.git",
40
+ "directory": "trillboards-connect"
41
+ },
42
+ "engines": {
43
+ "node": ">=14.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "jest": "^29.7.0",
47
+ "jest-environment-jsdom": "^29.7.0",
48
+ "tsup": "^8.0.0",
49
+ "typescript": "^5.3.0"
50
+ }
51
+ }