@prosdevlab/experience-sdk-plugins 0.1.0 → 0.1.3

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/utils/sanitize.ts","../src/banner/banner.ts","../src/debug/debug.ts","../src/frequency/frequency.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * HTML Sanitizer\n *\n * Lightweight HTML sanitizer for experience content (messages, titles).\n * Whitelist-based approach that only allows safe formatting tags.\n *\n * Security: Prevents XSS attacks by stripping dangerous tags and attributes.\n */\n\n/**\n * Allowed HTML tags for sanitization\n * Only safe formatting tags are permitted\n */\nconst ALLOWED_TAGS = ['strong', 'em', 'a', 'br', 'span', 'b', 'i', 'p'] as const;\n\n/**\n * Allowed attributes per tag\n */\nconst ALLOWED_ATTRIBUTES: Record<string, string[]> = {\n a: ['href', 'class', 'style', 'title'],\n span: ['class', 'style'],\n p: ['class', 'style'],\n // Other tags have no attributes allowed\n};\n\n/**\n * Sanitize HTML string by removing dangerous tags and attributes\n *\n * @param html - HTML string to sanitize\n * @returns Sanitized HTML string safe for innerHTML\n *\n * @example\n * ```typescript\n * sanitizeHTML('<strong>Hello</strong><script>alert(\"xss\")</script>');\n * // Returns: '<strong>Hello</strong>'\n * ```\n */\nexport function sanitizeHTML(html: string): string {\n if (!html || typeof html !== 'string') {\n return '';\n }\n\n // Create a temporary DOM element to parse HTML\n const temp = document.createElement('div');\n temp.innerHTML = html;\n\n /**\n * Recursively sanitize a DOM node\n */\n function sanitizeNode(node: Node): string {\n // Text nodes - escape HTML entities\n if (node.nodeType === Node.TEXT_NODE) {\n return escapeHTML(node.textContent || '');\n }\n\n // Element nodes\n if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as Element;\n const tagName = element.tagName.toLowerCase();\n\n // Handle tags with whitespace (malformed HTML like \"< script >\")\n // Browser normalizes these, but if we see a tag that's not in our list,\n // it might be a dangerous tag that was normalized\n if (!tagName || tagName.includes(' ')) {\n return '';\n }\n\n // If tag is not allowed, return empty string\n if (!ALLOWED_TAGS.includes(tagName as any)) {\n return '';\n }\n\n // Get allowed attributes for this tag\n const allowedAttrs = ALLOWED_ATTRIBUTES[tagName] || [];\n\n // Build attribute string\n const attrs: string[] = [];\n for (const attr of allowedAttrs) {\n const value = element.getAttribute(attr);\n if (value !== null) {\n // Sanitize attribute values\n if (attr === 'href') {\n // Only allow safe URLs (http, https, mailto, tel, relative)\n const sanitizedHref = sanitizeURL(value);\n if (sanitizedHref) {\n attrs.push(`href=\"${escapeAttribute(sanitizedHref)}\"`);\n }\n } else {\n // For all other attributes (title, class, style), escape HTML entities\n attrs.push(`${attr}=\"${escapeAttribute(value)}\"`);\n }\n }\n }\n\n const attrString = attrs.length > 0 ? ' ' + attrs.join(' ') : '';\n\n // Process child nodes\n let innerHTML = '';\n for (const child of Array.from(element.childNodes)) {\n innerHTML += sanitizeNode(child);\n }\n\n // Self-closing tags\n if (tagName === 'br') {\n return `<br${attrString} />`;\n }\n\n return `<${tagName}${attrString}>${innerHTML}</${tagName}>`;\n }\n\n return '';\n }\n\n // Sanitize all nodes\n let sanitized = '';\n for (const child of Array.from(temp.childNodes)) {\n sanitized += sanitizeNode(child);\n }\n\n return sanitized;\n}\n\n/**\n * Escape HTML entities to prevent XSS in text content\n */\nfunction escapeHTML(text: string): string {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n}\n\n/**\n * Escape HTML entities for use in attribute values\n */\nfunction escapeAttribute(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\n/**\n * Sanitize URL to prevent javascript: and data: XSS attacks\n *\n * @param url - URL to sanitize\n * @returns Sanitized URL or empty string if unsafe\n */\nfunction sanitizeURL(url: string): string {\n if (!url || typeof url !== 'string') {\n return '';\n }\n\n // Decode URL-encoded characters to check for encoded attacks\n let decoded: string;\n try {\n decoded = decodeURIComponent(url);\n } catch {\n // If decoding fails, use original\n decoded = url;\n }\n\n const trimmed = decoded.trim().toLowerCase();\n\n // Block javascript: and data: protocols (check both original and decoded)\n if (\n trimmed.startsWith('javascript:') ||\n trimmed.startsWith('data:') ||\n url.toLowerCase().trim().startsWith('javascript:') ||\n url.toLowerCase().trim().startsWith('data:')\n ) {\n return '';\n }\n\n // Allow http, https, mailto, tel, and relative URLs\n if (\n trimmed.startsWith('http://') ||\n trimmed.startsWith('https://') ||\n trimmed.startsWith('mailto:') ||\n trimmed.startsWith('tel:') ||\n trimmed.startsWith('/') ||\n trimmed.startsWith('#') ||\n trimmed.startsWith('?')\n ) {\n return url; // Return original (case preserved)\n }\n\n // Allow relative paths without protocol\n if (!trimmed.includes(':')) {\n return url;\n }\n\n // Block everything else\n return '';\n}\n","/**\n * Banner Plugin\n *\n * Renders banner experiences at the top or bottom of the page.\n * Auto-shows banners when experiences are evaluated.\n */\n\nimport type { PluginFunction } from '@lytics/sdk-kit';\nimport type { BannerContent, Decision, Experience } from '../types';\nimport { sanitizeHTML } from '../utils/sanitize';\n\nexport interface BannerPluginConfig {\n banner?: {\n position?: 'top' | 'bottom';\n dismissable?: boolean;\n zIndex?: number;\n };\n}\n\nexport interface BannerPlugin {\n show(experience: Experience): void;\n remove(): void;\n isShowing(): boolean;\n}\n\n/**\n * Banner Plugin\n *\n * Automatically renders banner experiences when they are evaluated.\n *\n * @example\n * ```typescript\n * import { createInstance } from '@prosdevlab/experience-sdk';\n * import { bannerPlugin } from '@prosdevlab/experience-sdk-plugins';\n *\n * const sdk = createInstance({ banner: { position: 'top', dismissable: true } });\n * sdk.use(bannerPlugin);\n * ```\n */\nexport const bannerPlugin: PluginFunction = (plugin, instance, config) => {\n plugin.ns('banner');\n\n // Set defaults\n plugin.defaults({\n banner: {\n position: 'top',\n dismissable: true,\n zIndex: 10000,\n },\n });\n\n // Track multiple active banners by experience ID\n const activeBanners = new Map<string, HTMLElement>();\n\n /**\n * Inject default banner styles if not already present\n */\n function injectDefaultStyles(): void {\n const styleId = 'xp-banner-styles';\n if (document.getElementById(styleId)) {\n return; // Already injected\n }\n\n const style = document.createElement('style');\n style.id = styleId;\n style.textContent = `\n .xp-banner {\n position: fixed;\n left: 0;\n right: 0;\n width: 100%;\n padding: 16px 20px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n line-height: 1.5;\n display: flex;\n align-items: center;\n justify-content: space-between;\n box-sizing: border-box;\n z-index: 10000;\n background: #f9fafb;\n color: #111827;\n border-bottom: 1px solid #e5e7eb;\n box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);\n }\n \n .xp-banner--top {\n top: 0;\n }\n \n .xp-banner--bottom {\n bottom: 0;\n border-bottom: none;\n border-top: 1px solid #e5e7eb;\n box-shadow: 0 -1px 3px 0 rgba(0, 0, 0, 0.05);\n }\n \n .xp-banner__container {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 20px;\n width: 100%;\n }\n \n .xp-banner__content {\n flex: 1;\n min-width: 0;\n }\n \n .xp-banner__title {\n font-weight: 600;\n margin-bottom: 4px;\n margin-top: 0;\n font-size: 14px;\n }\n \n .xp-banner__message {\n margin: 0;\n font-size: 14px;\n }\n \n .xp-banner__buttons {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n flex-shrink: 0;\n }\n \n .xp-banner__button {\n padding: 8px 16px;\n border: none;\n border-radius: 6px;\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.2s;\n text-decoration: none;\n }\n \n .xp-banner__button--primary {\n background: #2563eb;\n color: #ffffff;\n }\n \n .xp-banner__button--primary:hover {\n background: #1d4ed8;\n }\n \n .xp-banner__button--secondary {\n background: #ffffff;\n color: #374151;\n border: 1px solid #d1d5db;\n }\n \n .xp-banner__button--secondary:hover {\n background: #f9fafb;\n }\n \n .xp-banner__button--link {\n background: transparent;\n color: #2563eb;\n padding: 4px 8px;\n font-weight: 400;\n text-decoration: underline;\n }\n \n .xp-banner__button--link:hover {\n background: rgba(0, 0, 0, 0.05);\n }\n \n .xp-banner__close {\n background: transparent;\n border: none;\n color: #6b7280;\n font-size: 24px;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n margin: 0;\n opacity: 0.7;\n transition: opacity 0.2s;\n flex-shrink: 0;\n }\n \n .xp-banner__close:hover {\n opacity: 1;\n }\n \n @media (max-width: 640px) {\n .xp-banner__container {\n flex-direction: column;\n align-items: stretch;\n }\n \n .xp-banner__buttons {\n width: 100%;\n flex-direction: column;\n }\n \n .xp-banner__button {\n width: 100%;\n }\n }\n \n /* Dark mode support */\n @media (prefers-color-scheme: dark) {\n .xp-banner {\n background: #1f2937;\n color: #f3f4f6;\n border-bottom-color: #374151;\n }\n \n .xp-banner--bottom {\n border-top-color: #374151;\n }\n \n .xp-banner__button--primary {\n background: #3b82f6;\n }\n \n .xp-banner__button--primary:hover {\n background: #2563eb;\n }\n \n .xp-banner__button--secondary {\n background: #374151;\n color: #f3f4f6;\n border-color: #4b5563;\n }\n \n .xp-banner__button--secondary:hover {\n background: #4b5563;\n }\n \n .xp-banner__button--link {\n color: #93c5fd;\n }\n \n .xp-banner__close {\n color: #9ca3af;\n }\n }\n `;\n document.head.appendChild(style);\n }\n\n /**\n * Create banner DOM element\n */\n function createBannerElement(experience: Experience): HTMLElement {\n const content = experience.content as BannerContent;\n // Allow per-experience position override, fall back to global config\n const position = content.position ?? config.get('banner.position') ?? 'top';\n const dismissable = content.dismissable ?? config.get('banner.dismissable') ?? true;\n const zIndex = config.get('banner.zIndex') ?? 10000;\n\n // Inject default styles if needed\n injectDefaultStyles();\n\n // Create banner container\n const banner = document.createElement('div');\n banner.setAttribute('data-experience-id', experience.id);\n\n // Build className: base classes + position + user's custom class\n const baseClasses = ['xp-banner', `xp-banner--${position}`];\n if (content.className) {\n baseClasses.push(content.className);\n }\n banner.className = baseClasses.join(' ');\n\n // Apply user's custom styles\n if (content.style) {\n Object.assign(banner.style, content.style);\n }\n\n // Override z-index if configured\n if (zIndex !== 10000) {\n banner.style.zIndex = String(zIndex);\n }\n\n // Create container\n const container = document.createElement('div');\n container.className = 'xp-banner__container';\n banner.appendChild(container);\n\n // Create content container\n const contentDiv = document.createElement('div');\n contentDiv.className = 'xp-banner__content';\n\n // Add title if present\n if (content.title) {\n const title = document.createElement('h3');\n title.className = 'xp-banner__title';\n // Sanitize HTML to prevent XSS attacks\n title.innerHTML = sanitizeHTML(content.title);\n contentDiv.appendChild(title);\n }\n\n // Add message\n const message = document.createElement('p');\n message.className = 'xp-banner__message';\n // Sanitize HTML to prevent XSS attacks\n message.innerHTML = sanitizeHTML(content.message);\n contentDiv.appendChild(message);\n\n container.appendChild(contentDiv);\n\n banner.appendChild(contentDiv);\n\n // Create button container for actions and/or dismiss\n const buttonContainer = document.createElement('div');\n buttonContainer.style.cssText = `\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n `;\n\n // Create buttons container\n const buttonsDiv = document.createElement('div');\n buttonsDiv.className = 'xp-banner__buttons';\n\n // Helper function to create button with variant styling\n function createButton(buttonConfig: {\n text: string;\n action?: string;\n url?: string;\n variant?: 'primary' | 'secondary' | 'link';\n metadata?: Record<string, unknown>;\n className?: string;\n style?: Record<string, string>;\n }): HTMLButtonElement {\n const button = document.createElement('button');\n button.textContent = buttonConfig.text;\n\n const variant = buttonConfig.variant || 'primary';\n\n // Build className: base class + variant + user's custom class\n const buttonClasses = ['xp-banner__button', `xp-banner__button--${variant}`];\n if (buttonConfig.className) {\n buttonClasses.push(buttonConfig.className);\n }\n button.className = buttonClasses.join(' ');\n\n // Apply user's custom styles\n if (buttonConfig.style) {\n Object.assign(button.style, buttonConfig.style);\n }\n\n button.addEventListener('click', () => {\n // Emit action event\n instance.emit('experiences:action', {\n experienceId: experience.id,\n type: 'banner',\n action: buttonConfig.action,\n url: buttonConfig.url,\n metadata: buttonConfig.metadata,\n variant: variant,\n timestamp: Date.now(),\n });\n\n // Navigate if URL provided\n if (buttonConfig.url) {\n window.location.href = buttonConfig.url;\n }\n });\n\n return button;\n }\n\n // Add buttons from buttons array\n if (content.buttons && content.buttons.length > 0) {\n content.buttons.forEach((buttonConfig) => {\n const button = createButton(buttonConfig);\n buttonsDiv.appendChild(button);\n });\n }\n\n // Add dismiss button if dismissable\n if (dismissable) {\n const closeButton = document.createElement('button');\n closeButton.className = 'xp-banner__close';\n closeButton.innerHTML = '&times;';\n closeButton.setAttribute('aria-label', 'Close banner');\n\n closeButton.addEventListener('click', () => {\n remove(experience.id);\n instance.emit('experiences:dismissed', {\n experienceId: experience.id,\n type: 'banner',\n });\n });\n\n buttonsDiv.appendChild(closeButton);\n }\n\n container.appendChild(buttonsDiv);\n\n return banner;\n }\n\n /**\n * Show a banner experience\n */\n function show(experience: Experience): void {\n // If banner already showing for this experience, skip\n if (activeBanners.has(experience.id)) {\n return;\n }\n\n // Only show if we're in a browser environment\n if (typeof document === 'undefined') {\n return;\n }\n\n const banner = createBannerElement(experience);\n document.body.appendChild(banner);\n activeBanners.set(experience.id, banner);\n\n instance.emit('experiences:shown', {\n experienceId: experience.id,\n type: 'banner',\n timestamp: Date.now(),\n });\n }\n\n /**\n * Remove a banner by experience ID (or all if no ID provided)\n */\n function remove(experienceId?: string): void {\n if (experienceId) {\n // Remove specific banner\n const banner = activeBanners.get(experienceId);\n if (banner?.parentNode) {\n banner.parentNode.removeChild(banner);\n }\n activeBanners.delete(experienceId);\n } else {\n // Remove all banners\n for (const [id, banner] of activeBanners.entries()) {\n if (banner?.parentNode) {\n banner.parentNode.removeChild(banner);\n }\n activeBanners.delete(id);\n }\n }\n }\n\n /**\n * Check if any banner is currently showing\n */\n function isShowing(): boolean {\n return activeBanners.size > 0;\n }\n\n // Expose banner API\n plugin.expose({\n banner: {\n show,\n remove,\n isShowing,\n },\n });\n\n // Auto-show banner on experiences:evaluated event\n instance.on('experiences:evaluated', (payload: unknown) => {\n // Handle both single decision and array of decisions\n // evaluate() emits: { decision, experience }\n // evaluateAll() emits: [{ decision, experience }, ...]\n const items = Array.isArray(payload) ? payload : [payload];\n\n for (const item of items) {\n // Item is { decision, experience }\n const typedItem = item as { decision?: Decision; experience?: Experience };\n const decision = typedItem.decision;\n const experience = typedItem.experience;\n\n // Only handle banner-type experiences\n if (experience?.type === 'banner') {\n if (decision?.show) {\n show(experience);\n } else if (experience.id && activeBanners.has(experience.id)) {\n // Hide specific banner if decision says don't show\n remove(experience.id);\n }\n }\n }\n });\n\n // Cleanup on destroy\n instance.on('sdk:destroy', () => {\n remove();\n });\n};\n","/**\n * Debug Plugin\n *\n * Emits structured debug events to window and optionally logs to console.\n * Useful for debugging and Chrome extension integration.\n */\n\nimport type { PluginFunction } from '@lytics/sdk-kit';\n\nexport interface DebugPluginConfig {\n debug?: {\n enabled?: boolean;\n console?: boolean;\n window?: boolean;\n };\n}\n\nexport interface DebugPlugin {\n log(message: string, data?: unknown): void;\n isEnabled(): boolean;\n}\n\n/**\n * Debug Plugin\n *\n * Listens to all SDK events and emits them as window events for debugging.\n * Also optionally logs to console.\n *\n * @example\n * ```typescript\n * import { createInstance } from '@prosdevlab/experience-sdk';\n * import { debugPlugin } from '@prosdevlab/experience-sdk-plugins';\n *\n * const sdk = createInstance({ debug: { enabled: true, console: true } });\n * sdk.use(debugPlugin);\n * ```\n */\nexport const debugPlugin: PluginFunction = (plugin, instance, config) => {\n plugin.ns('debug');\n\n // Set defaults\n plugin.defaults({\n debug: {\n enabled: false,\n console: false,\n window: true,\n },\n });\n\n // Helper to check if debug is enabled\n const isEnabled = (): boolean => config.get('debug.enabled') ?? false;\n const shouldLogConsole = (): boolean => config.get('debug.console') ?? false;\n const shouldEmitWindow = (): boolean => config.get('debug.window') ?? true;\n\n // Log function\n const log = (message: string, data?: unknown): void => {\n if (!isEnabled()) return;\n\n const timestamp = new Date().toISOString();\n const logData = {\n timestamp,\n message,\n data,\n };\n\n // Console logging\n if (shouldLogConsole()) {\n console.log(`[experiences] ${message}`, data || '');\n }\n\n // Window event emission\n if (shouldEmitWindow() && typeof window !== 'undefined') {\n const event = new CustomEvent('experience-sdk:debug', {\n detail: logData,\n });\n window.dispatchEvent(event);\n }\n };\n\n // Expose debug API\n plugin.expose({\n debug: {\n log,\n isEnabled,\n },\n });\n\n // If debug is enabled, listen to all events\n if (isEnabled()) {\n // Listen to experiences:* events\n instance.on('experiences:ready', () => {\n if (!isEnabled()) return;\n log('SDK initialized and ready');\n });\n\n instance.on('experiences:registered', (payload) => {\n if (!isEnabled()) return;\n log('Experience registered', payload);\n });\n\n instance.on('experiences:evaluated', (payload) => {\n if (!isEnabled()) return;\n log('Experience evaluated', payload);\n });\n }\n};\n","/**\n * Frequency Capping Plugin\n *\n * Tracks experience impressions and enforces frequency caps.\n * Uses sdk-kit's storage plugin for persistence.\n */\n\nimport type { PluginFunction, SDK } from '@lytics/sdk-kit';\nimport { type StoragePlugin, storagePlugin } from '@lytics/sdk-kit-plugins';\nimport type { Decision, TraceStep } from '../types';\n\nexport interface FrequencyPluginConfig {\n frequency?: {\n enabled?: boolean;\n namespace?: string;\n };\n}\n\nexport interface FrequencyPlugin {\n getImpressionCount(experienceId: string): number;\n hasReachedCap(experienceId: string, max: number, per: 'session' | 'day' | 'week'): boolean;\n recordImpression(experienceId: string): void;\n}\n\ninterface ImpressionData {\n count: number;\n lastImpression: number;\n impressions: number[];\n per?: 'session' | 'day' | 'week'; // Track which storage type this uses\n}\n\n/**\n * Frequency Capping Plugin\n *\n * Automatically tracks impressions and enforces frequency caps.\n * Requires storage plugin for persistence.\n *\n * @example\n * ```typescript\n * import { createInstance } from '@prosdevlab/experience-sdk';\n * import { frequencyPlugin } from '@prosdevlab/experience-sdk-plugins';\n *\n * const sdk = createInstance({ frequency: { enabled: true } });\n * sdk.use(frequencyPlugin);\n * ```\n */\nexport const frequencyPlugin: PluginFunction = (plugin, instance, config) => {\n plugin.ns('frequency');\n\n // Set defaults\n plugin.defaults({\n frequency: {\n enabled: true,\n namespace: 'experiences:frequency',\n },\n });\n\n // Track experience frequency configs\n const experienceFrequencyMap = new Map<string, 'session' | 'day' | 'week'>();\n\n // Auto-load storage plugin if not already loaded\n if (!(instance as SDK & { storage?: StoragePlugin }).storage) {\n instance.use(storagePlugin);\n }\n\n const isEnabled = (): boolean => config.get('frequency.enabled') ?? true;\n const getNamespace = (): string => config.get('frequency.namespace') ?? 'experiences:frequency';\n\n // Helper to get the right storage backend based on frequency type\n const getStorageBackend = (per: 'session' | 'day' | 'week'): Storage => {\n return per === 'session' ? sessionStorage : localStorage;\n };\n\n // Helper to get storage key\n const getStorageKey = (experienceId: string): string => {\n return `${getNamespace()}:${experienceId}`;\n };\n\n // Helper to get impression data\n const getImpressionData = (\n experienceId: string,\n per: 'session' | 'day' | 'week'\n ): ImpressionData => {\n const storage = getStorageBackend(per);\n const key = getStorageKey(experienceId);\n const raw = storage.getItem(key);\n\n if (!raw) {\n return {\n count: 0,\n lastImpression: 0,\n impressions: [],\n per,\n };\n }\n\n try {\n return JSON.parse(raw) as ImpressionData;\n } catch {\n return {\n count: 0,\n lastImpression: 0,\n impressions: [],\n per,\n };\n }\n };\n\n // Helper to save impression data\n const saveImpressionData = (experienceId: string, data: ImpressionData): void => {\n const per = data.per || 'session'; // Default to session if not specified\n const storage = getStorageBackend(per);\n const key = getStorageKey(experienceId);\n storage.setItem(key, JSON.stringify(data));\n };\n\n // Get time window in milliseconds\n const getTimeWindow = (per: 'session' | 'day' | 'week'): number => {\n switch (per) {\n case 'session':\n return Number.POSITIVE_INFINITY; // Session storage handles this\n case 'day':\n return 24 * 60 * 60 * 1000; // 24 hours\n case 'week':\n return 7 * 24 * 60 * 60 * 1000; // 7 days\n }\n };\n\n /**\n * Get impression count for an experience\n */\n const getImpressionCount = (\n experienceId: string,\n per: 'session' | 'day' | 'week' = 'session'\n ): number => {\n if (!isEnabled()) return 0;\n const data = getImpressionData(experienceId, per);\n return data.count;\n };\n\n /**\n * Check if an experience has reached its frequency cap\n */\n const hasReachedCap = (\n experienceId: string,\n max: number,\n per: 'session' | 'day' | 'week'\n ): boolean => {\n if (!isEnabled()) return false;\n\n const data = getImpressionData(experienceId, per);\n const timeWindow = getTimeWindow(per);\n const now = Date.now();\n\n // For session caps, just check total count\n if (per === 'session') {\n return data.count >= max;\n }\n\n // For time-based caps, count impressions within the window\n const recentImpressions = data.impressions.filter((timestamp) => now - timestamp < timeWindow);\n\n return recentImpressions.length >= max;\n };\n\n /**\n * Record an impression for an experience\n */\n const recordImpression = (\n experienceId: string,\n per: 'session' | 'day' | 'week' = 'session'\n ): void => {\n if (!isEnabled()) return;\n\n const data = getImpressionData(experienceId, per);\n const now = Date.now();\n\n // Update count and add timestamp\n data.count += 1;\n data.lastImpression = now;\n data.impressions.push(now);\n data.per = per; // Store the frequency type\n\n // Keep only recent impressions (last 7 days)\n const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;\n data.impressions = data.impressions.filter((ts) => ts > sevenDaysAgo);\n\n // Save updated data\n saveImpressionData(experienceId, data);\n\n // Emit event\n instance.emit('experiences:impression-recorded', {\n experienceId,\n count: data.count,\n timestamp: now,\n });\n };\n\n // Expose frequency API\n plugin.expose({\n frequency: {\n getImpressionCount,\n hasReachedCap,\n recordImpression,\n // Internal method to register experience frequency config\n _registerExperience: (experienceId: string, per: 'session' | 'day' | 'week') => {\n experienceFrequencyMap.set(experienceId, per);\n },\n },\n });\n\n // Listen to evaluation events and record impressions\n if (isEnabled()) {\n instance.on('experiences:evaluated', (payload: unknown) => {\n // Handle both single decision and array of decisions\n // evaluate() emits: { decision, experience }\n // evaluateAll() emits: [{ decision, experience }, ...]\n const items = Array.isArray(payload) ? payload : [payload];\n\n for (const item of items) {\n // Item is { decision, experience }\n const decision = (item as { decision?: Decision }).decision;\n\n // Only record if experience was shown\n if (decision?.show && decision.experienceId) {\n // Try to get the 'per' value from our map, fall back to checking the input in trace\n let per: 'session' | 'day' | 'week' =\n experienceFrequencyMap.get(decision.experienceId) || 'session';\n\n // If not in map, try to infer from the decision trace\n if (!experienceFrequencyMap.has(decision.experienceId)) {\n const freqStep = decision.trace.find(\n (t: TraceStep) => t.step === 'check-frequency-cap'\n );\n if (freqStep?.input && typeof freqStep.input === 'object' && 'per' in freqStep.input) {\n per = (freqStep.input as { per: 'session' | 'day' | 'week' }).per;\n // Cache it for next time\n experienceFrequencyMap.set(decision.experienceId, per);\n }\n }\n\n recordImpression(decision.experienceId, per);\n }\n }\n });\n }\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prosdevlab/experience-sdk-plugins",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Official plugins for Experience SDK",
5
5
  "private": false,
6
6
  "type": "module",
@@ -12,16 +12,13 @@
12
12
  "import": "./dist/index.js"
13
13
  }
14
14
  },
15
- "scripts": {
16
- "build": "tsup",
17
- "dev": "tsup --watch",
18
- "clean": "rm -rf dist",
19
- "typecheck": "tsc --noEmit",
20
- "lint": "biome lint ./src",
21
- "format": "biome format --write ./src",
22
- "test": "vitest run",
23
- "test:watch": "vitest"
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/prosdevlab/experience-sdk.git",
18
+ "directory": "packages/plugins"
24
19
  },
20
+ "author": "prosdevlab",
21
+ "license": "MIT",
25
22
  "dependencies": {
26
23
  "@lytics/sdk-kit": "^0.1.1",
27
24
  "@lytics/sdk-kit-plugins": "^0.1.2"
@@ -30,5 +27,15 @@
30
27
  "tsup": "^8.5.1",
31
28
  "typescript": "^5.9.3",
32
29
  "vitest": "^4.0.16"
30
+ },
31
+ "scripts": {
32
+ "build": "tsup",
33
+ "dev": "tsup --watch",
34
+ "clean": "rm -rf dist",
35
+ "typecheck": "tsc --noEmit",
36
+ "lint": "biome lint ./src",
37
+ "format": "biome format --write ./src",
38
+ "test": "vitest run",
39
+ "test:watch": "vitest"
33
40
  }
34
- }
41
+ }
@@ -92,8 +92,8 @@ describe('Banner Plugin', () => {
92
92
  sdk.banner.show(experience);
93
93
 
94
94
  const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement;
95
- expect(banner.style.top).toBe('0px');
96
- expect(banner.style.bottom).toBe('');
95
+ expect(banner.className).toContain('xp-banner--top');
96
+ expect(banner.className).not.toContain('xp-banner--bottom');
97
97
  });
98
98
 
99
99
  it('should show banner at bottom position when configured', () => {
@@ -113,8 +113,8 @@ describe('Banner Plugin', () => {
113
113
  customSdk.banner.show(experience);
114
114
 
115
115
  const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement;
116
- expect(banner.style.bottom).toBe('0px');
117
- expect(banner.style.top).toBe('');
116
+ expect(banner.className).toContain('xp-banner--bottom');
117
+ expect(banner.className).not.toContain('xp-banner--top');
118
118
  });
119
119
 
120
120
  it('should create banner with title and message', () => {
@@ -500,8 +500,8 @@ describe('Banner Plugin', () => {
500
500
  const button = banner?.querySelector('button') as HTMLElement;
501
501
 
502
502
  expect(button).toBeTruthy();
503
- // Primary variant should have white text
504
- expect(button.style.color).toContain('255'); // rgb(255, 255, 255)
503
+ // Primary variant should have the correct class
504
+ expect(button.className).toContain('xp-banner__button--primary');
505
505
  });
506
506
 
507
507
  it('should emit action event with variant and metadata', () => {
@@ -667,7 +667,8 @@ describe('Banner Plugin', () => {
667
667
  sdk.banner.show(experience);
668
668
 
669
669
  const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement;
670
- expect(banner.style.zIndex).toBe('10000');
670
+ // Default z-index is set via CSS, check that banner has the base class
671
+ expect(banner.className).toContain('xp-banner');
671
672
  });
672
673
 
673
674
  it('should apply custom z-index', () => {
@@ -704,7 +705,9 @@ describe('Banner Plugin', () => {
704
705
  sdk.banner.show(experience);
705
706
 
706
707
  const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement;
707
- expect(banner.style.position).toBe('fixed');
708
+ // Position is set via CSS class, check computed style
709
+ const computedStyle = window.getComputedStyle(banner);
710
+ expect(computedStyle.position).toBe('fixed');
708
711
  });
709
712
 
710
713
  it('should span full width', () => {
@@ -721,8 +724,365 @@ describe('Banner Plugin', () => {
721
724
  sdk.banner.show(experience);
722
725
 
723
726
  const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement;
724
- expect(banner.style.left).toBe('0px');
725
- expect(banner.style.right).toBe('0px');
727
+ // Width and positioning are set via CSS class, check computed styles
728
+ const computedStyle = window.getComputedStyle(banner);
729
+ expect(computedStyle.left).toBe('0px');
730
+ expect(computedStyle.right).toBe('0px');
731
+ expect(computedStyle.width).toBe('100%');
732
+ });
733
+
734
+ it('should apply custom className to banner', () => {
735
+ const experience: Experience = {
736
+ id: 'test-banner',
737
+ type: 'banner',
738
+ targeting: {},
739
+ content: {
740
+ message: 'Test message',
741
+ className: 'my-custom-banner custom-class',
742
+ },
743
+ };
744
+
745
+ sdk.banner.show(experience);
746
+
747
+ const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement;
748
+ expect(banner.className).toContain('xp-banner');
749
+ expect(banner.className).toContain('my-custom-banner');
750
+ expect(banner.className).toContain('custom-class');
751
+ });
752
+
753
+ it('should apply custom inline styles to banner', () => {
754
+ const experience: Experience = {
755
+ id: 'test-banner',
756
+ type: 'banner',
757
+ targeting: {},
758
+ content: {
759
+ message: 'Test message',
760
+ style: {
761
+ backgroundColor: '#ff0000',
762
+ padding: '24px',
763
+ borderRadius: '8px',
764
+ },
765
+ },
766
+ };
767
+
768
+ sdk.banner.show(experience);
769
+
770
+ const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement;
771
+ expect(banner.style.backgroundColor).toBe('rgb(255, 0, 0)');
772
+ expect(banner.style.padding).toBe('24px');
773
+ expect(banner.style.borderRadius).toBe('8px');
774
+ });
775
+
776
+ it('should apply custom className to buttons', () => {
777
+ const experience: Experience = {
778
+ id: 'test-banner',
779
+ type: 'banner',
780
+ targeting: {},
781
+ content: {
782
+ message: 'Test message',
783
+ buttons: [
784
+ {
785
+ text: 'Primary Button',
786
+ variant: 'primary',
787
+ className: 'my-primary-btn custom-btn',
788
+ },
789
+ {
790
+ text: 'Secondary Button',
791
+ variant: 'secondary',
792
+ className: 'my-secondary-btn',
793
+ },
794
+ ],
795
+ },
796
+ };
797
+
798
+ sdk.banner.show(experience);
799
+
800
+ const banner = document.querySelector('[data-experience-id="test-banner"]');
801
+ const buttons = banner?.querySelectorAll('.xp-banner__button');
802
+
803
+ expect(buttons?.[0].className).toContain('xp-banner__button--primary');
804
+ expect(buttons?.[0].className).toContain('my-primary-btn');
805
+ expect(buttons?.[0].className).toContain('custom-btn');
806
+
807
+ expect(buttons?.[1].className).toContain('xp-banner__button--secondary');
808
+ expect(buttons?.[1].className).toContain('my-secondary-btn');
809
+ });
810
+
811
+ it('should apply custom inline styles to buttons', () => {
812
+ const experience: Experience = {
813
+ id: 'test-banner',
814
+ type: 'banner',
815
+ targeting: {},
816
+ content: {
817
+ message: 'Test message',
818
+ buttons: [
819
+ {
820
+ text: 'Styled Button',
821
+ variant: 'primary',
822
+ style: {
823
+ backgroundColor: '#00ff00',
824
+ color: '#000000',
825
+ fontWeight: 'bold',
826
+ },
827
+ },
828
+ ],
829
+ },
830
+ };
831
+
832
+ sdk.banner.show(experience);
833
+
834
+ const banner = document.querySelector('[data-experience-id="test-banner"]');
835
+ const button = banner?.querySelector('.xp-banner__button') as HTMLElement;
836
+
837
+ expect(button.style.backgroundColor).toBe('rgb(0, 255, 0)');
838
+ expect(button.style.color).toBe('rgb(0, 0, 0)');
839
+ expect(button.style.fontWeight).toBe('bold');
840
+ });
841
+
842
+ it('should combine className and style props', () => {
843
+ const experience: Experience = {
844
+ id: 'test-banner',
845
+ type: 'banner',
846
+ targeting: {},
847
+ content: {
848
+ message: 'Test message',
849
+ className: 'custom-banner',
850
+ style: {
851
+ backgroundColor: '#0000ff',
852
+ },
853
+ buttons: [
854
+ {
855
+ text: 'Button',
856
+ className: 'custom-button',
857
+ style: {
858
+ color: '#ffffff',
859
+ },
860
+ },
861
+ ],
862
+ },
863
+ };
864
+
865
+ sdk.banner.show(experience);
866
+
867
+ const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement;
868
+ expect(banner.className).toContain('xp-banner');
869
+ expect(banner.className).toContain('custom-banner');
870
+ expect(banner.style.backgroundColor).toBe('rgb(0, 0, 255)');
871
+
872
+ const button = banner.querySelector('.xp-banner__button') as HTMLElement;
873
+ expect(button.className).toContain('xp-banner__button');
874
+ expect(button.className).toContain('custom-button');
875
+ expect(button.style.color).toBe('rgb(255, 255, 255)');
876
+ });
877
+ });
878
+
879
+ describe('HTML Sanitization', () => {
880
+ it('should sanitize HTML in title to prevent XSS', () => {
881
+ const experience: Experience = {
882
+ id: 'xss-test',
883
+ type: 'banner',
884
+ targeting: {},
885
+ content: {
886
+ title: '<script>alert("xss")</script>Safe Title',
887
+ message: 'Test message',
888
+ },
889
+ };
890
+
891
+ sdk.emit('experiences:evaluated', {
892
+ decision: {
893
+ show: true,
894
+ experienceId: 'xss-test',
895
+ reasons: [],
896
+ trace: [],
897
+ context: {} as any,
898
+ metadata: {},
899
+ },
900
+ experience,
901
+ });
902
+
903
+ const banner = document.querySelector('[data-experience-id="xss-test"]') as HTMLElement;
904
+ expect(banner).toBeTruthy();
905
+
906
+ const title = banner.querySelector('.xp-banner__title') as HTMLElement;
907
+ expect(title).toBeTruthy();
908
+ // Script tag should be stripped
909
+ expect(title.innerHTML).not.toContain('<script>');
910
+ expect(title.innerHTML).not.toContain('alert("xss")');
911
+ // Safe content should remain
912
+ expect(title.innerHTML).toContain('Safe Title');
913
+ });
914
+
915
+ it('should sanitize HTML in message to prevent XSS', () => {
916
+ const experience: Experience = {
917
+ id: 'xss-test',
918
+ type: 'banner',
919
+ targeting: {},
920
+ content: {
921
+ message: 'Hello <script>alert("xss")</script>World',
922
+ },
923
+ };
924
+
925
+ sdk.emit('experiences:evaluated', {
926
+ decision: {
927
+ show: true,
928
+ experienceId: 'xss-test',
929
+ reasons: [],
930
+ trace: [],
931
+ context: {} as any,
932
+ metadata: {},
933
+ },
934
+ experience,
935
+ });
936
+
937
+ const banner = document.querySelector('[data-experience-id="xss-test"]') as HTMLElement;
938
+ expect(banner).toBeTruthy();
939
+
940
+ const message = banner.querySelector('.xp-banner__message') as HTMLElement;
941
+ expect(message).toBeTruthy();
942
+ // Script tag should be stripped
943
+ expect(message.innerHTML).not.toContain('<script>');
944
+ expect(message.innerHTML).not.toContain('alert("xss")');
945
+ // Safe content should remain
946
+ expect(message.innerHTML).toContain('Hello');
947
+ expect(message.innerHTML).toContain('World');
948
+ });
949
+
950
+ it('should allow safe HTML tags in title and message', () => {
951
+ const experience: Experience = {
952
+ id: 'html-test',
953
+ type: 'banner',
954
+ targeting: {},
955
+ content: {
956
+ title: '<strong>Flash Sale!</strong>',
957
+ message: 'Get <em>50% off</em> everything. <a href="/shop">Shop Now</a>',
958
+ },
959
+ };
960
+
961
+ sdk.emit('experiences:evaluated', {
962
+ decision: {
963
+ show: true,
964
+ experienceId: 'html-test',
965
+ reasons: [],
966
+ trace: [],
967
+ context: {} as any,
968
+ metadata: {},
969
+ },
970
+ experience,
971
+ });
972
+
973
+ const banner = document.querySelector('[data-experience-id="html-test"]') as HTMLElement;
974
+ expect(banner).toBeTruthy();
975
+
976
+ const title = banner.querySelector('.xp-banner__title') as HTMLElement;
977
+ expect(title.innerHTML).toContain('<strong>Flash Sale!</strong>');
978
+
979
+ const message = banner.querySelector('.xp-banner__message') as HTMLElement;
980
+ expect(message.innerHTML).toContain('<em>50% off</em>');
981
+ expect(message.innerHTML).toContain('<a href="/shop">Shop Now</a>');
982
+ });
983
+
984
+ it('should strip dangerous tags like iframe and object', () => {
985
+ const experience: Experience = {
986
+ id: 'dangerous-test',
987
+ type: 'banner',
988
+ targeting: {},
989
+ content: {
990
+ message: 'Safe text <iframe src="evil.com"></iframe> more text',
991
+ },
992
+ };
993
+
994
+ sdk.emit('experiences:evaluated', {
995
+ decision: {
996
+ show: true,
997
+ experienceId: 'dangerous-test',
998
+ reasons: [],
999
+ trace: [],
1000
+ context: {} as any,
1001
+ metadata: {},
1002
+ },
1003
+ experience,
1004
+ });
1005
+
1006
+ const banner = document.querySelector('[data-experience-id="dangerous-test"]') as HTMLElement;
1007
+ expect(banner).toBeTruthy();
1008
+ const message = banner.querySelector('.xp-banner__message') as HTMLElement;
1009
+
1010
+ // Dangerous tags should be stripped
1011
+ expect(message.innerHTML).not.toContain('<iframe>');
1012
+ expect(message.innerHTML).not.toContain('evil.com');
1013
+ // Safe content should remain
1014
+ expect(message.innerHTML).toContain('Safe text');
1015
+ expect(message.innerHTML).toContain('more text');
1016
+ });
1017
+
1018
+ it('should strip javascript: URLs from links', () => {
1019
+ const experience: Experience = {
1020
+ id: 'js-url-test',
1021
+ type: 'banner',
1022
+ targeting: {},
1023
+ content: {
1024
+ message: 'Click <a href="javascript:alert(\'xss\')">here</a>',
1025
+ },
1026
+ };
1027
+
1028
+ sdk.emit('experiences:evaluated', {
1029
+ decision: {
1030
+ show: true,
1031
+ experienceId: 'js-url-test',
1032
+ reasons: [],
1033
+ trace: [],
1034
+ context: {} as any,
1035
+ metadata: {},
1036
+ },
1037
+ experience,
1038
+ });
1039
+
1040
+ const banner = document.querySelector('[data-experience-id="js-url-test"]') as HTMLElement;
1041
+ expect(banner).toBeTruthy();
1042
+ const message = banner.querySelector('.xp-banner__message') as HTMLElement;
1043
+
1044
+ // javascript: URL should be stripped
1045
+ expect(message.innerHTML).not.toContain('javascript:');
1046
+ expect(message.innerHTML).not.toContain('alert');
1047
+ // Link tag should exist but without href
1048
+ expect(message.innerHTML).toContain('<a>here</a>');
1049
+ });
1050
+
1051
+ it('should allow legitimate URLs and paths in links', () => {
1052
+ const experience: Experience = {
1053
+ id: 'legitimate-urls-test',
1054
+ type: 'banner',
1055
+ targeting: {},
1056
+ content: {
1057
+ message:
1058
+ 'Visit <a href="/shop">our shop</a>, check out <a href="https://example.com">external</a>, or <a href="mailto:support@example.com">email us</a>',
1059
+ },
1060
+ };
1061
+
1062
+ sdk.emit('experiences:evaluated', {
1063
+ decision: {
1064
+ show: true,
1065
+ experienceId: 'legitimate-urls-test',
1066
+ reasons: [],
1067
+ trace: [],
1068
+ context: {} as any,
1069
+ metadata: {},
1070
+ },
1071
+ experience,
1072
+ });
1073
+
1074
+ const banner = document.querySelector(
1075
+ '[data-experience-id="legitimate-urls-test"]'
1076
+ ) as HTMLElement;
1077
+ expect(banner).toBeTruthy();
1078
+ const message = banner.querySelector('.xp-banner__message') as HTMLElement;
1079
+
1080
+ // Relative paths should work
1081
+ expect(message.innerHTML).toContain('<a href="/shop">our shop</a>');
1082
+ // External URLs should work
1083
+ expect(message.innerHTML).toContain('<a href="https://example.com">external</a>');
1084
+ // mailto: URLs should work
1085
+ expect(message.innerHTML).toContain('<a href="mailto:support@example.com">email us</a>');
726
1086
  });
727
1087
  });
728
1088
  });