import { ref, computed, onMounted, onUnmounted } from 'vue' import type { PlatformContext, PlatformContextOptions, PlatformEvent } from '../types' const DEFAULT_CONTEXT: PlatformContext = { isIntegrated: false, theme: 'system', } const platformContext = ref({ ...DEFAULT_CONTEXT }) // Track allowed origins for postMessage security. // Inferred origins come from platform injection and URL params. // Consumer origins and allowAnyOrigin are tracked per composable instance. let inferredOrigins: Set = new Set() let allowedOrigins: Set = new Set() let allowAnyOrigin = false let initialized = false let listenerCount = 0 let nextConsumerId = 0 let currentHandler: ((event: MessageEvent) => void) | null = null const consumerOrigins: Map> = new Map() const allowAnyOriginConsumers: Set = new Set() /** * Derive origin from URL (protocol + host) */ function getOriginFromUrl(url: string): string | null { try { const parsed = new URL(url) return parsed.origin } catch { return null } } function normalizeAllowedOrigins(origins: string[] | undefined): Set { const normalized = new Set() if (!origins) { return normalized } for (const origin of origins) { normalized.add(getOriginFromUrl(origin) || origin) } return normalized } function recomputeOriginPolicy(): void { allowedOrigins = new Set(inferredOrigins) for (const origins of consumerOrigins.values()) { for (const origin of origins) { allowedOrigins.add(origin) } } allowAnyOrigin = allowAnyOriginConsumers.size > 0 } function resetPlatformContextState(): void { inferredOrigins = new Set() allowedOrigins = new Set() allowAnyOrigin = false initialized = false listenerCount = 0 currentHandler = null consumerOrigins.clear() allowAnyOriginConsumers.clear() platformContext.value = { ...DEFAULT_CONTEXT } } /** * Check if an origin is allowed for postMessage communication */ function isOriginAllowed(origin: string): boolean { // Development mode: allow any origin (must be explicitly enabled) if (allowAnyOrigin) { console.warn('[MINT SDK] postMessage origin validation disabled - only use in development') return true } // Same origin is always allowed if (origin === window.location.origin) { return true } // Check against allowed origins list return allowedOrigins.has(origin) } /** * Platform context composable for plugin integration with MINT Platform. * * Provides secure communication with the parent platform via postMessage. * * @param options - Configuration options * @param options.allowedOrigins - List of allowed origins for postMessage * @param options.allowAnyOrigin - Allow any origin (UNSAFE, development only) * * @example * ```typescript * // Basic usage - derives origin from platform injection * const { isIntegrated, user, theme } = usePlatformContext() * * // With explicit allowed origins * const { isIntegrated } = usePlatformContext({ * allowedOrigins: ['https://mint.example.com'] * }) * * // Development mode (UNSAFE) * const { isIntegrated } = usePlatformContext({ * allowAnyOrigin: import.meta.env.DEV * }) * ``` */ /** Connects a plugin to the MINT platform via postMessage, exposing user, theme, and experiment context. */ export function usePlatformContext(options: PlatformContextOptions = {}) { const consumerId = ++nextConsumerId const instanceOrigins = normalizeAllowedOrigins(options.allowedOrigins) const instanceAllowAnyOrigin = options.allowAnyOrigin === true function detectPlatform(): void { const detectedOrigins = new Set() // Check if running under MINT Platform by looking for platform-injected global const platformData = (window as unknown as { __MINT_PLATFORM__?: PlatformContext }).__MINT_PLATFORM__ if (platformData) { platformContext.value = { ...platformData, isIntegrated: true, } // Derive platform origin from injected data if (platformData.platformOrigin) { detectedOrigins.add(platformData.platformOrigin) } else if (platformData.platformApiUrl) { const origin = getOriginFromUrl(platformData.platformApiUrl) if (origin) { detectedOrigins.add(origin) } } } else { // Check for platform indicator in URL or localStorage const urlParams = new URLSearchParams(window.location.search) const hasPluginParam = urlParams.has('mint-plugin') // Try to get platform origin from URL parameter const platformOrigin = urlParams.get('mint-origin') if (platformOrigin) { const origin = getOriginFromUrl(platformOrigin) if (origin) { detectedOrigins.add(origin) } } platformContext.value = { isIntegrated: hasPluginParam, theme: (() => { try { const s = localStorage.getItem('mint-settings') if (s) return (JSON.parse(s).theme as 'light' | 'dark' | 'system') || 'system' } catch { /* ignore */ } return 'system' })(), platformOrigin: platformOrigin || undefined, } } inferredOrigins = detectedOrigins } function handlePlatformMessage(event: MessageEvent): void { // Only accept messages from parent window (platform) if (event.source !== window.parent) return // Validate origin for security if (!isOriginAllowed(event.origin)) { console.warn(`[MINT SDK] Rejected postMessage from untrusted origin: ${event.origin}`) return } try { const platformEvent = event.data as PlatformEvent if (!platformEvent.type?.startsWith('mint:')) return switch (platformEvent.type) { case 'mint:theme-changed': platformContext.value.theme = platformEvent.payload as 'light' | 'dark' | 'system' break case 'mint:user-changed': platformContext.value.user = platformEvent.payload as PlatformContext['user'] break } } catch { // Ignore invalid messages } } /** * Send a message to the parent platform. * Uses validated target origin for security. */ function sendToPlatform(event: PlatformEvent): void { if (!platformContext.value.isIntegrated || window.parent === window) { return } // Determine target origin let targetOrigin: string if (platformContext.value.platformOrigin) { // Use explicitly configured platform origin targetOrigin = platformContext.value.platformOrigin } else if (allowedOrigins.size > 0) { // Use first allowed origin (typically the platform) targetOrigin = allowedOrigins.values().next().value as string } else if (allowAnyOrigin) { // Development mode fallback targetOrigin = '*' console.warn('[MINT SDK] Using wildcard origin for postMessage - only use in development') } else { // Safety: if no origin is configured, log warning and don't send console.warn('[MINT SDK] Cannot send postMessage: no platform origin configured') return } window.parent.postMessage(event, targetOrigin) } /** * Request navigation to a path in the platform. */ function navigate(path: string): void { sendToPlatform({ type: 'mint:navigate', payload: path, }) } /** * Show a notification in the platform. */ function notify(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info'): void { sendToPlatform({ type: 'mint:notification', payload: { message, type }, }) } onMounted(() => { consumerOrigins.set(consumerId, instanceOrigins) if (instanceAllowAnyOrigin) { allowAnyOriginConsumers.add(consumerId) } if (!initialized) { detectPlatform() currentHandler = handlePlatformMessage window.addEventListener('message', handlePlatformMessage) initialized = true } listenerCount++ recomputeOriginPolicy() }) onUnmounted(() => { consumerOrigins.delete(consumerId) allowAnyOriginConsumers.delete(consumerId) listenerCount = Math.max(0, listenerCount - 1) if (listenerCount === 0) { if (currentHandler) { window.removeEventListener('message', currentHandler) } resetPlatformContextState() return } recomputeOriginPolicy() }) const isIntegrated = computed(() => platformContext.value.isIntegrated) const plugin = computed(() => platformContext.value.plugin) const user = computed(() => platformContext.value.user) const theme = computed(() => platformContext.value.theme) const features = computed(() => platformContext.value.features) return { context: platformContext, isIntegrated, plugin, user, theme, features, navigate, notify, sendToPlatform, } }