@seed-ship/mcp-ui-solid 2.2.8 → 2.2.10
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.
- package/dist/components/UIResourceRenderer.cjs +313 -302
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +313 -302
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/dist/services/validation.cjs +12 -0
- package/dist/services/validation.cjs.map +1 -1
- package/dist/services/validation.d.ts.map +1 -1
- package/dist/services/validation.js +12 -0
- package/dist/services/validation.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ChartRenderer.test.tsx +43 -0
- package/src/components/UIResourceRenderer.tsx +59 -49
- package/src/services/validation.test.ts +40 -2
- package/src/services/validation.ts +16 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.cjs","sources":["../../src/services/validation.ts"],"sourcesContent":["/**\n * Component Validation Service\n * Phase 0: Resource Limits & Schema Validation\n *\n * Validates LLM-generated components against:\n * - JSON schema\n * - Resource limits (data points, payload size, grid bounds)\n * - Security constraints (domain whitelist, XSS prevention)\n */\n\nimport type {\n UIComponent,\n UILayout,\n ValidationResult,\n ResourceLimits,\n ChartComponentParams,\n TableComponentParams,\n FormFieldParams,\n IframePolicy,\n ValidationOptions,\n ComponentType,\n} from '../types'\n\n/**\n * All known ComponentType values — used to distinguish known-but-unvalidated\n * types (pass through) from truly unknown strings (reject).\n */\nconst KNOWN_COMPONENT_TYPES: Set<string> = new Set<ComponentType>([\n 'chart', 'table', 'metric', 'text', 'grid', 'iframe', 'image', 'link',\n 'action', 'footer', 'carousel', 'artifact', 'form', 'modal',\n 'action-group', 'image-gallery', 'video', 'code', 'map',\n])\n\n/**\n * Default resource limits (configurable via env)\n */\nexport const DEFAULT_RESOURCE_LIMITS: ResourceLimits = {\n maxDataPoints: 1000,\n maxTableRows: 100,\n maxPayloadSize: 50 * 1024, // 50KB\n renderTimeout: 5000, // 5 seconds\n}\n\n/**\n * Default allowed iframe domains (whitelist)\n * Must match CSP frame-src directive\n * Updated Sprint 7: Added code, design, docs, and map providers\n *\n * This list is exported for transparency and can be extended via ValidationOptions\n */\nexport const DEFAULT_IFRAME_DOMAINS = [\n // Charts\n 'quickchart.io',\n 'www.quickchart.io',\n\n // Deposium\n 'deposium.com',\n 'deposium.vip',\n 'deposium.ai',\n\n // Development\n 'localhost',\n\n // Video providers (Sprint 5)\n 'youtube.com',\n 'www.youtube.com',\n 'youtube-nocookie.com',\n 'www.youtube-nocookie.com',\n 'youtu.be',\n 'vimeo.com',\n 'player.vimeo.com',\n\n // Code playgrounds (Sprint 7)\n 'codepen.io',\n 'codesandbox.io',\n 'stackblitz.com',\n 'jsfiddle.net',\n\n // Design tools (Sprint 7)\n 'figma.com',\n 'www.figma.com',\n 'miro.com',\n\n // Google services (Sprint 7)\n 'docs.google.com',\n 'drive.google.com',\n 'sheets.google.com',\n 'slides.google.com',\n 'maps.google.com',\n 'www.google.com',\n 'datastudio.google.com',\n 'lookerstudio.google.com',\n\n // Productivity (Sprint 7)\n 'airtable.com',\n 'notion.so',\n 'www.notion.so',\n\n // Maps (Sprint 7)\n 'openstreetmap.org',\n 'www.openstreetmap.org',\n\n // Analytics/Dashboards (Sprint 7)\n 'public.tableau.com',\n 'app.powerbi.com',\n 'observablehq.com',\n\n // Diagrams & Whiteboards (v2.0.0)\n 'mermaid.live',\n 'excalidraw.com',\n 'lucidchart.com',\n 'lucid.app',\n\n // Video - Business (v2.0.0)\n 'loom.com',\n 'www.loom.com',\n 'cloudflarestream.com',\n 'streamable.com',\n\n // Code repositories (v2.0.0)\n 'github.com',\n 'gist.github.com',\n 'gitlab.com',\n 'replit.com',\n 'glitch.com',\n\n // Business tools (v2.0.0)\n 'calendly.com',\n 'typeform.com',\n 'cal.com',\n\n // Design (v2.0.0)\n 'canva.com',\n\n // Deploy previews (v2.0.0)\n 'vercel.app',\n 'netlify.app',\n\n // E-commerce (v2.0.0)\n 'amazon.com',\n 'amazon.fr',\n 'amazon.de',\n 'amazon.co.uk',\n 'amazon.es',\n 'amazon.it',\n 'amazon.ca',\n 'amazon.co.jp',\n 'images-amazon.com',\n 'media-amazon.com',\n 'ws-na.amazon-adsystem.com',\n\n // MCP Connectors — embed-capable services (v2.2.7)\n 'gamma.app',\n 'www.gamma.app',\n 'app.hubspot.com',\n 'share.hubspot.com',\n 'www.data.gouv.fr',\n 'data.gouv.fr',\n 'clinicaltrials.gov',\n 'www.clinicaltrials.gov',\n 'linear.app',\n 'www.linear.app',\n]\n\n/**\n * Validate grid position bounds (1-12 columns)\n */\nexport function validateGridPosition(position: UIComponent['position']): ValidationResult {\n const errors: ValidationResult['errors'] = []\n\n // ✅ PHASE 3 FIX: Defensive check for undefined position\n if (!position) {\n return {\n valid: false,\n errors: [\n {\n path: 'position',\n message: 'Position is required',\n code: 'MISSING_POSITION',\n },\n ],\n }\n }\n\n if (position.colStart < 1 || position.colStart > 12) {\n errors.push({\n path: 'position.colStart',\n message: 'Column start must be between 1 and 12',\n code: 'INVALID_GRID_COL_START',\n })\n }\n\n if (position.colSpan < 1 || position.colSpan > 12) {\n errors.push({\n path: 'position.colSpan',\n message: 'Column span must be between 1 and 12',\n code: 'INVALID_GRID_COL_SPAN',\n })\n }\n\n if (position.colStart + position.colSpan - 1 > 12) {\n errors.push({\n path: 'position',\n message: 'Column start + span exceeds grid width (12)',\n code: 'GRID_OVERFLOW',\n })\n }\n\n if (position.rowStart !== undefined && position.rowStart < 1) {\n errors.push({\n path: 'position.rowStart',\n message: 'Row start must be >= 1',\n code: 'INVALID_GRID_ROW_START',\n })\n }\n\n if (position.rowSpan !== undefined && position.rowSpan < 1) {\n errors.push({\n path: 'position.rowSpan',\n message: 'Row span must be >= 1',\n code: 'INVALID_GRID_ROW_SPAN',\n })\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate chart component against resource limits\n */\nexport function validateChartComponent(\n params: ChartComponentParams,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\n): ValidationResult {\n const errors: ValidationResult['errors'] = []\n\n // Validate data points count\n const totalDataPoints = params.data.datasets.reduce(\n (sum, dataset) => sum + dataset.data.length,\n 0\n )\n\n if (totalDataPoints > limits.maxDataPoints) {\n errors.push({\n path: 'params.data',\n message: `Chart exceeds max data points: ${totalDataPoints} > ${limits.maxDataPoints}`,\n code: 'RESOURCE_LIMIT_EXCEEDED',\n })\n }\n\n // Validate labels match dataset length\n const expectedLength = params.data.labels.length\n for (const [index, dataset] of params.data.datasets.entries()) {\n if (dataset.data.length !== expectedLength) {\n errors.push({\n path: `params.data.datasets[${index}]`,\n message: `Dataset length mismatch: expected ${expectedLength}, got ${dataset.data.length}`,\n code: 'DATA_LENGTH_MISMATCH',\n })\n }\n }\n\n // Validate numeric data\n for (const [index, dataset] of params.data.datasets.entries()) {\n for (const [dataIndex, value] of dataset.data.entries()) {\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n errors.push({\n path: `params.data.datasets[${index}].data[${dataIndex}]`,\n message: `Invalid data value: ${value} (must be finite number)`,\n code: 'INVALID_DATA_TYPE',\n })\n }\n }\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate table component against resource limits\n */\nexport function validateTableComponent(\n params: TableComponentParams,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\n): ValidationResult {\n const errors: ValidationResult['errors'] = []\n\n // Validate row count\n if (params.rows.length > limits.maxTableRows) {\n errors.push({\n path: 'params.rows',\n message: `Table exceeds max rows: ${params.rows.length} > ${limits.maxTableRows}`,\n code: 'RESOURCE_LIMIT_EXCEEDED',\n })\n }\n\n // Validate columns\n if (params.columns.length === 0) {\n errors.push({\n path: 'params.columns',\n message: 'Table must have at least one column',\n code: 'EMPTY_COLUMNS',\n })\n }\n\n // Validate column keys are unique\n const columnKeys = new Set<string>()\n for (const [index, column] of params.columns.entries()) {\n if (columnKeys.has(column.key)) {\n errors.push({\n path: `params.columns[${index}]`,\n message: `Duplicate column key: ${column.key}`,\n code: 'DUPLICATE_COLUMN_KEY',\n })\n }\n columnKeys.add(column.key)\n }\n\n // Validate rows have valid data for defined columns\n for (const [rowIndex, row] of params.rows.entries()) {\n for (const column of params.columns) {\n if (!(column.key in row)) {\n errors.push({\n path: `params.rows[${rowIndex}]`,\n message: `Missing column key: ${column.key}`,\n code: 'MISSING_COLUMN_DATA',\n })\n }\n }\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate payload size\n */\nexport function validatePayloadSize(\n component: UIComponent,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\n): ValidationResult {\n const payloadSize = JSON.stringify(component).length\n\n if (payloadSize > limits.maxPayloadSize) {\n return {\n valid: false,\n errors: [\n {\n path: 'component',\n message: `Payload size exceeds limit: ${payloadSize} > ${limits.maxPayloadSize} bytes`,\n code: 'PAYLOAD_TOO_LARGE',\n },\n ],\n }\n }\n\n return { valid: true }\n}\n\n/**\n * Sanitize string to prevent XSS\n * Basic implementation - DOMPurify used at render time\n */\nexport function sanitizeString(input: string): string {\n return input\n .replace(/<script\\b[^<]*(?:(?!<\\/script>)<[^<]*)*<\\/script>/gi, '')\n .replace(/on\\w+=\"[^\"]*\"/gi, '')\n .replace(/javascript:/gi, '')\n}\n\n/**\n * Validate iframe domain against whitelist\n *\n * @param url - The URL to validate\n * @param options - Optional validation options\n * @param options.policy - 'strict' (default), 'extend', or 'allow-all'\n * @param options.customDomains - Additional domains when policy is 'extend'\n */\nexport function validateIframeDomain(\n url: string,\n options?: { policy?: IframePolicy; customDomains?: string[] }\n): ValidationResult {\n // If allow-all, skip validation\n if (options?.policy === 'allow-all') {\n return { valid: true }\n }\n\n try {\n const parsedUrl = new URL(url)\n const domain = parsedUrl.hostname\n\n // Build effective whitelist\n let effectiveWhitelist = DEFAULT_IFRAME_DOMAINS\n if (options?.policy === 'extend' && options.customDomains) {\n effectiveWhitelist = [...DEFAULT_IFRAME_DOMAINS, ...options.customDomains]\n }\n\n const isAllowed = effectiveWhitelist.some(\n (allowed) => domain === allowed || domain.endsWith(`.${allowed}`) || allowed === 'localhost'\n )\n\n if (!isAllowed) {\n return {\n valid: false,\n errors: [\n {\n path: 'url',\n message: `Domain not whitelisted: ${domain}`,\n code: 'DOMAIN_NOT_WHITELISTED',\n },\n ],\n }\n }\n\n return { valid: true }\n } catch (error) {\n return {\n valid: false,\n errors: [\n {\n path: 'url',\n message: 'Invalid URL format',\n code: 'INVALID_URL',\n },\n ],\n }\n }\n}\n\n/**\n * Validate entire component\n *\n * @param component - The component to validate\n * @param options - Optional validation options (limits, iframePolicy, customIframeDomains)\n */\nexport function validateComponent(\n component: UIComponent,\n options?: ValidationOptions\n): ValidationResult {\n const limits = options?.limits ?? DEFAULT_RESOURCE_LIMITS\n const errors: ValidationResult['errors'] = []\n\n // Validate grid position\n const gridResult = validateGridPosition(component.position)\n if (!gridResult.valid) {\n errors.push(...(gridResult.errors || []))\n }\n\n // Validate payload size\n const sizeResult = validatePayloadSize(component, limits)\n if (!sizeResult.valid) {\n errors.push(...(sizeResult.errors || []))\n }\n\n // Type-specific validation\n switch (component.type) {\n case 'chart': {\n const chartResult = validateChartComponent(component.params as ChartComponentParams, limits)\n if (!chartResult.valid) {\n errors.push(...(chartResult.errors || []))\n }\n break\n }\n\n case 'table': {\n const tableResult = validateTableComponent(component.params as TableComponentParams, limits)\n if (!tableResult.valid) {\n errors.push(...(tableResult.errors || []))\n }\n break\n }\n\n case 'metric': {\n // Basic validation for metrics\n const metricParams = component.params as any\n if (!metricParams.title || !metricParams.value) {\n errors.push({\n path: 'params',\n message: 'Metric must have title and value',\n code: 'INVALID_METRIC',\n })\n }\n break\n }\n\n case 'text': {\n // Basic validation for text\n const textParams = component.params as any\n if (!textParams.content) {\n errors.push({\n path: 'params',\n message: 'Text component must have content',\n code: 'INVALID_TEXT',\n })\n }\n break\n }\n\n case 'iframe': {\n // Basic validation for iframe\n const iframeParams = component.params as any\n if (!iframeParams.url) {\n errors.push({\n path: 'params',\n message: 'Iframe component must have url',\n code: 'INVALID_IFRAME',\n })\n } else {\n // Validate iframe domain against whitelist\n const iframeResult = validateIframeDomain(iframeParams.url, {\n policy: options?.iframePolicy,\n customDomains: options?.customIframeDomains,\n })\n if (!iframeResult.valid) {\n errors.push(...(iframeResult.errors || []))\n }\n }\n break\n }\n\n case 'image': {\n // Basic validation for image\n const imageParams = component.params as any\n if (!imageParams.url) {\n errors.push({\n path: 'params',\n message: 'Image component must have url',\n code: 'INVALID_IMAGE',\n })\n }\n break\n }\n\n case 'link': {\n // Basic validation for link\n const linkParams = component.params as any\n if (!linkParams.url) {\n errors.push({\n path: 'params',\n message: 'Link component must have url',\n code: 'INVALID_LINK',\n })\n }\n break\n }\n\n case 'action': {\n // Basic validation for action\n const actionParams = component.params as any\n if (!actionParams.label) {\n errors.push({\n path: 'params',\n message: 'Action component must have label',\n code: 'INVALID_ACTION',\n })\n }\n break\n }\n\n default:\n // Known types without specific validation pass through — renderer handles errors\n // Truly unknown types (e.g. typos in streamed JSON) are rejected\n if (!KNOWN_COMPONENT_TYPES.has(component.type)) {\n errors.push({\n path: 'type',\n message: `Unknown component type: ${component.type}`,\n code: 'UNKNOWN_COMPONENT_TYPE',\n })\n }\n break\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate entire layout\n *\n * @param layout - The layout to validate\n * @param options - Optional validation options (limits, iframePolicy, customIframeDomains)\n */\nexport function validateLayout(\n layout: UILayout,\n options?: ValidationOptions\n): ValidationResult {\n const errors: ValidationResult['errors'] = []\n\n // Validate component count\n if (layout.components.length === 0) {\n errors.push({\n path: 'components',\n message: 'Layout must have at least one component',\n code: 'EMPTY_LAYOUT',\n })\n }\n\n if (layout.components.length > 12) {\n errors.push({\n path: 'components',\n message: `Layout exceeds max components: ${layout.components.length} > 12`,\n code: 'TOO_MANY_COMPONENTS',\n })\n }\n\n // Validate each component\n for (const [index, component] of layout.components.entries()) {\n const result = validateComponent(component, options)\n if (!result.valid) {\n errors.push(\n ...(result.errors?.map((error) => ({\n ...error,\n path: `components[${index}].${error.path}`,\n })) || [])\n )\n }\n }\n\n // Validate grid configuration\n if (layout.grid.columns !== 12) {\n errors.push({\n path: 'grid.columns',\n message: 'Grid must have 12 columns (Bootstrap-like)',\n code: 'INVALID_GRID_COLUMNS',\n })\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate a single form field value against field rules\n */\nexport function validateFieldValue(\n value: any,\n field: FormFieldParams\n): { valid: boolean; error?: string } {\n // Required check\n if (field.required) {\n if (value === undefined || value === null || value === '') {\n return { valid: false, error: `${field.label || field.name} is required` }\n }\n if (field.type === 'checkbox' && value !== true) {\n return { valid: false, error: `${field.label || field.name} must be checked` }\n }\n }\n\n // Skip further validation if value is empty and not required\n if (value === undefined || value === null || value === '') {\n return { valid: true }\n }\n\n // Type-specific validation\n switch (field.type) {\n case 'text':\n case 'textarea':\n case 'password':\n if (field.minLength && String(value).length < field.minLength) {\n return { valid: false, error: `Minimum ${field.minLength} characters required` }\n }\n if (field.maxLength && String(value).length > field.maxLength) {\n return { valid: false, error: `Maximum ${field.maxLength} characters allowed` }\n }\n if (field.pattern && !new RegExp(field.pattern).test(String(value))) {\n return { valid: false, error: 'Invalid format' }\n }\n break\n\n case 'email':\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(String(value))) {\n return { valid: false, error: 'Invalid email address' }\n }\n break\n\n case 'number': {\n const numValue = Number(value)\n if (isNaN(numValue)) {\n return { valid: false, error: 'Must be a valid number' }\n }\n if (field.min !== undefined && numValue < field.min) {\n return { valid: false, error: `Minimum value is ${field.min}` }\n }\n if (field.max !== undefined && numValue > field.max) {\n return { valid: false, error: `Maximum value is ${field.max}` }\n }\n break\n }\n\n case 'date':\n if (field.minDate && value < field.minDate) {\n return { valid: false, error: `Date must be after ${field.minDate}` }\n }\n if (field.maxDate && value > field.maxDate) {\n return { valid: false, error: `Date must be before ${field.maxDate}` }\n }\n break\n\n case 'select':\n case 'radio':\n // Validate that value is one of the options\n if (field.options && field.options.length > 0) {\n const validValues = field.options.map((opt) => opt.value)\n if (!validValues.includes(String(value))) {\n return { valid: false, error: 'Please select a valid option' }\n }\n }\n break\n }\n\n return { valid: true }\n}\n\n/**\n * Validate entire form data against field definitions\n */\nexport function validateFormData(\n data: Record<string, any>,\n fields: FormFieldParams[]\n): { valid: boolean; errors: Record<string, string> } {\n const errors: Record<string, string> = {}\n\n for (const field of fields) {\n const result = validateFieldValue(data[field.name], field)\n if (!result.valid && result.error) {\n errors[field.name] = result.error\n }\n }\n\n return {\n valid: Object.keys(errors).length === 0,\n errors,\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"validation.cjs","sources":["../../src/services/validation.ts"],"sourcesContent":["/**\n * Component Validation Service\n * Phase 0: Resource Limits & Schema Validation\n *\n * Validates LLM-generated components against:\n * - JSON schema\n * - Resource limits (data points, payload size, grid bounds)\n * - Security constraints (domain whitelist, XSS prevention)\n */\n\nimport type {\n UIComponent,\n UILayout,\n ValidationResult,\n ResourceLimits,\n ChartComponentParams,\n TableComponentParams,\n FormFieldParams,\n IframePolicy,\n ValidationOptions,\n ComponentType,\n} from '../types'\n\n/**\n * All known ComponentType values — used to distinguish known-but-unvalidated\n * types (pass through) from truly unknown strings (reject).\n */\nconst KNOWN_COMPONENT_TYPES: Set<string> = new Set<ComponentType>([\n 'chart', 'table', 'metric', 'text', 'grid', 'iframe', 'image', 'link',\n 'action', 'footer', 'carousel', 'artifact', 'form', 'modal',\n 'action-group', 'image-gallery', 'video', 'code', 'map',\n])\n\n/**\n * Default resource limits (configurable via env)\n */\nexport const DEFAULT_RESOURCE_LIMITS: ResourceLimits = {\n maxDataPoints: 1000,\n maxTableRows: 100,\n maxPayloadSize: 50 * 1024, // 50KB\n renderTimeout: 5000, // 5 seconds\n}\n\n/**\n * Default allowed iframe domains (whitelist)\n * Must match CSP frame-src directive\n * Updated Sprint 7: Added code, design, docs, and map providers\n *\n * This list is exported for transparency and can be extended via ValidationOptions\n */\nexport const DEFAULT_IFRAME_DOMAINS = [\n // Charts\n 'quickchart.io',\n 'www.quickchart.io',\n\n // Deposium\n 'deposium.com',\n 'deposium.vip',\n 'deposium.ai',\n\n // Development\n 'localhost',\n\n // Video providers (Sprint 5)\n 'youtube.com',\n 'www.youtube.com',\n 'youtube-nocookie.com',\n 'www.youtube-nocookie.com',\n 'youtu.be',\n 'vimeo.com',\n 'player.vimeo.com',\n\n // Code playgrounds (Sprint 7)\n 'codepen.io',\n 'codesandbox.io',\n 'stackblitz.com',\n 'jsfiddle.net',\n\n // Design tools (Sprint 7)\n 'figma.com',\n 'www.figma.com',\n 'miro.com',\n\n // Google services (Sprint 7)\n 'docs.google.com',\n 'drive.google.com',\n 'sheets.google.com',\n 'slides.google.com',\n 'maps.google.com',\n 'www.google.com',\n 'datastudio.google.com',\n 'lookerstudio.google.com',\n\n // Productivity (Sprint 7)\n 'airtable.com',\n 'notion.so',\n 'www.notion.so',\n\n // Maps (Sprint 7)\n 'openstreetmap.org',\n 'www.openstreetmap.org',\n\n // Analytics/Dashboards (Sprint 7)\n 'public.tableau.com',\n 'app.powerbi.com',\n 'observablehq.com',\n\n // Diagrams & Whiteboards (v2.0.0)\n 'mermaid.live',\n 'excalidraw.com',\n 'lucidchart.com',\n 'lucid.app',\n\n // Video - Business (v2.0.0)\n 'loom.com',\n 'www.loom.com',\n 'cloudflarestream.com',\n 'streamable.com',\n\n // Code repositories (v2.0.0)\n 'github.com',\n 'gist.github.com',\n 'gitlab.com',\n 'replit.com',\n 'glitch.com',\n\n // Business tools (v2.0.0)\n 'calendly.com',\n 'typeform.com',\n 'cal.com',\n\n // Design (v2.0.0)\n 'canva.com',\n\n // Deploy previews (v2.0.0)\n 'vercel.app',\n 'netlify.app',\n\n // E-commerce (v2.0.0)\n 'amazon.com',\n 'amazon.fr',\n 'amazon.de',\n 'amazon.co.uk',\n 'amazon.es',\n 'amazon.it',\n 'amazon.ca',\n 'amazon.co.jp',\n 'images-amazon.com',\n 'media-amazon.com',\n 'ws-na.amazon-adsystem.com',\n\n // MCP Connectors — embed-capable services (v2.2.7)\n 'gamma.app',\n 'www.gamma.app',\n 'app.hubspot.com',\n 'share.hubspot.com',\n 'www.data.gouv.fr',\n 'data.gouv.fr',\n 'clinicaltrials.gov',\n 'www.clinicaltrials.gov',\n 'linear.app',\n 'www.linear.app',\n]\n\n/**\n * Validate grid position bounds (1-12 columns)\n */\nexport function validateGridPosition(position: UIComponent['position']): ValidationResult {\n const errors: ValidationResult['errors'] = []\n\n // ✅ PHASE 3 FIX: Defensive check for undefined position\n if (!position) {\n return {\n valid: false,\n errors: [\n {\n path: 'position',\n message: 'Position is required',\n code: 'MISSING_POSITION',\n },\n ],\n }\n }\n\n if (position.colStart < 1 || position.colStart > 12) {\n errors.push({\n path: 'position.colStart',\n message: 'Column start must be between 1 and 12',\n code: 'INVALID_GRID_COL_START',\n })\n }\n\n if (position.colSpan < 1 || position.colSpan > 12) {\n errors.push({\n path: 'position.colSpan',\n message: 'Column span must be between 1 and 12',\n code: 'INVALID_GRID_COL_SPAN',\n })\n }\n\n if (position.colStart + position.colSpan - 1 > 12) {\n errors.push({\n path: 'position',\n message: 'Column start + span exceeds grid width (12)',\n code: 'GRID_OVERFLOW',\n })\n }\n\n if (position.rowStart !== undefined && position.rowStart < 1) {\n errors.push({\n path: 'position.rowStart',\n message: 'Row start must be >= 1',\n code: 'INVALID_GRID_ROW_START',\n })\n }\n\n if (position.rowSpan !== undefined && position.rowSpan < 1) {\n errors.push({\n path: 'position.rowSpan',\n message: 'Row span must be >= 1',\n code: 'INVALID_GRID_ROW_SPAN',\n })\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate chart component against resource limits\n */\nexport function validateChartComponent(\n params: ChartComponentParams,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\n): ValidationResult {\n const errors: ValidationResult['errors'] = []\n\n // Guard: params.data must exist with labels + datasets\n if (!params?.data) {\n return { valid: false, errors: [{ path: 'params.data', message: 'Missing chart data object', code: 'MISSING_DATA' }] }\n }\n if (!Array.isArray(params.data.datasets)) {\n return { valid: false, errors: [{ path: 'params.data.datasets', message: 'Missing or invalid datasets array', code: 'MISSING_DATASETS' }] }\n }\n if (!Array.isArray(params.data.labels)) {\n return { valid: false, errors: [{ path: 'params.data.labels', message: 'Missing or invalid labels array', code: 'MISSING_LABELS' }] }\n }\n\n // Validate data points count\n const totalDataPoints = params.data.datasets.reduce(\n (sum, dataset) => sum + dataset.data.length,\n 0\n )\n\n if (totalDataPoints > limits.maxDataPoints) {\n errors.push({\n path: 'params.data',\n message: `Chart exceeds max data points: ${totalDataPoints} > ${limits.maxDataPoints}`,\n code: 'RESOURCE_LIMIT_EXCEEDED',\n })\n }\n\n // Validate labels match dataset length\n const expectedLength = params.data.labels.length\n for (const [index, dataset] of params.data.datasets.entries()) {\n if (dataset.data.length !== expectedLength) {\n errors.push({\n path: `params.data.datasets[${index}]`,\n message: `Dataset length mismatch: expected ${expectedLength}, got ${dataset.data.length}`,\n code: 'DATA_LENGTH_MISMATCH',\n })\n }\n }\n\n // Validate numeric data\n for (const [index, dataset] of params.data.datasets.entries()) {\n for (const [dataIndex, value] of dataset.data.entries()) {\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n errors.push({\n path: `params.data.datasets[${index}].data[${dataIndex}]`,\n message: `Invalid data value: ${value} (must be finite number)`,\n code: 'INVALID_DATA_TYPE',\n })\n }\n }\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate table component against resource limits\n */\nexport function validateTableComponent(\n params: TableComponentParams,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\n): ValidationResult {\n const errors: ValidationResult['errors'] = []\n\n // Validate row count\n if (params.rows.length > limits.maxTableRows) {\n errors.push({\n path: 'params.rows',\n message: `Table exceeds max rows: ${params.rows.length} > ${limits.maxTableRows}`,\n code: 'RESOURCE_LIMIT_EXCEEDED',\n })\n }\n\n // Validate columns\n if (params.columns.length === 0) {\n errors.push({\n path: 'params.columns',\n message: 'Table must have at least one column',\n code: 'EMPTY_COLUMNS',\n })\n }\n\n // Validate column keys are unique\n const columnKeys = new Set<string>()\n for (const [index, column] of params.columns.entries()) {\n if (columnKeys.has(column.key)) {\n errors.push({\n path: `params.columns[${index}]`,\n message: `Duplicate column key: ${column.key}`,\n code: 'DUPLICATE_COLUMN_KEY',\n })\n }\n columnKeys.add(column.key)\n }\n\n // Validate rows have valid data for defined columns\n for (const [rowIndex, row] of params.rows.entries()) {\n for (const column of params.columns) {\n if (!(column.key in row)) {\n errors.push({\n path: `params.rows[${rowIndex}]`,\n message: `Missing column key: ${column.key}`,\n code: 'MISSING_COLUMN_DATA',\n })\n }\n }\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate payload size\n */\nexport function validatePayloadSize(\n component: UIComponent,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\n): ValidationResult {\n const payloadSize = JSON.stringify(component).length\n\n if (payloadSize > limits.maxPayloadSize) {\n return {\n valid: false,\n errors: [\n {\n path: 'component',\n message: `Payload size exceeds limit: ${payloadSize} > ${limits.maxPayloadSize} bytes`,\n code: 'PAYLOAD_TOO_LARGE',\n },\n ],\n }\n }\n\n return { valid: true }\n}\n\n/**\n * Sanitize string to prevent XSS\n * Basic implementation - DOMPurify used at render time\n */\nexport function sanitizeString(input: string): string {\n return input\n .replace(/<script\\b[^<]*(?:(?!<\\/script>)<[^<]*)*<\\/script>/gi, '')\n .replace(/on\\w+=\"[^\"]*\"/gi, '')\n .replace(/javascript:/gi, '')\n}\n\n/**\n * Validate iframe domain against whitelist\n *\n * @param url - The URL to validate\n * @param options - Optional validation options\n * @param options.policy - 'strict' (default), 'extend', or 'allow-all'\n * @param options.customDomains - Additional domains when policy is 'extend'\n */\nexport function validateIframeDomain(\n url: string,\n options?: { policy?: IframePolicy; customDomains?: string[] }\n): ValidationResult {\n // If allow-all, skip validation\n if (options?.policy === 'allow-all') {\n return { valid: true }\n }\n\n try {\n const parsedUrl = new URL(url)\n const domain = parsedUrl.hostname\n\n // Build effective whitelist\n let effectiveWhitelist = DEFAULT_IFRAME_DOMAINS\n if (options?.policy === 'extend' && options.customDomains) {\n effectiveWhitelist = [...DEFAULT_IFRAME_DOMAINS, ...options.customDomains]\n }\n\n const isAllowed = effectiveWhitelist.some(\n (allowed) => domain === allowed || domain.endsWith(`.${allowed}`) || allowed === 'localhost'\n )\n\n if (!isAllowed) {\n return {\n valid: false,\n errors: [\n {\n path: 'url',\n message: `Domain not whitelisted: ${domain}`,\n code: 'DOMAIN_NOT_WHITELISTED',\n },\n ],\n }\n }\n\n return { valid: true }\n } catch (error) {\n return {\n valid: false,\n errors: [\n {\n path: 'url',\n message: 'Invalid URL format',\n code: 'INVALID_URL',\n },\n ],\n }\n }\n}\n\n/**\n * Validate entire component\n *\n * @param component - The component to validate\n * @param options - Optional validation options (limits, iframePolicy, customIframeDomains)\n */\nexport function validateComponent(\n component: UIComponent,\n options?: ValidationOptions\n): ValidationResult {\n const limits = options?.limits ?? DEFAULT_RESOURCE_LIMITS\n const errors: ValidationResult['errors'] = []\n\n // Guard: params must exist\n if (!component.params) {\n return { valid: false, errors: [{ path: 'params', message: 'Missing component params', code: 'MISSING_PARAMS' }] }\n }\n\n // Validate grid position\n const gridResult = validateGridPosition(component.position)\n if (!gridResult.valid) {\n errors.push(...(gridResult.errors || []))\n }\n\n // Validate payload size\n const sizeResult = validatePayloadSize(component, limits)\n if (!sizeResult.valid) {\n errors.push(...(sizeResult.errors || []))\n }\n\n // Type-specific validation\n switch (component.type) {\n case 'chart': {\n const chartResult = validateChartComponent(component.params as ChartComponentParams, limits)\n if (!chartResult.valid) {\n errors.push(...(chartResult.errors || []))\n }\n break\n }\n\n case 'table': {\n const tableResult = validateTableComponent(component.params as TableComponentParams, limits)\n if (!tableResult.valid) {\n errors.push(...(tableResult.errors || []))\n }\n break\n }\n\n case 'metric': {\n // Basic validation for metrics\n const metricParams = component.params as any\n if (!metricParams.title || !metricParams.value) {\n errors.push({\n path: 'params',\n message: 'Metric must have title and value',\n code: 'INVALID_METRIC',\n })\n }\n break\n }\n\n case 'text': {\n // Basic validation for text\n const textParams = component.params as any\n if (!textParams.content) {\n errors.push({\n path: 'params',\n message: 'Text component must have content',\n code: 'INVALID_TEXT',\n })\n }\n break\n }\n\n case 'iframe': {\n // Basic validation for iframe\n const iframeParams = component.params as any\n if (!iframeParams.url) {\n errors.push({\n path: 'params',\n message: 'Iframe component must have url',\n code: 'INVALID_IFRAME',\n })\n } else {\n // Validate iframe domain against whitelist\n const iframeResult = validateIframeDomain(iframeParams.url, {\n policy: options?.iframePolicy,\n customDomains: options?.customIframeDomains,\n })\n if (!iframeResult.valid) {\n errors.push(...(iframeResult.errors || []))\n }\n }\n break\n }\n\n case 'image': {\n // Basic validation for image\n const imageParams = component.params as any\n if (!imageParams.url) {\n errors.push({\n path: 'params',\n message: 'Image component must have url',\n code: 'INVALID_IMAGE',\n })\n }\n break\n }\n\n case 'link': {\n // Basic validation for link\n const linkParams = component.params as any\n if (!linkParams.url) {\n errors.push({\n path: 'params',\n message: 'Link component must have url',\n code: 'INVALID_LINK',\n })\n }\n break\n }\n\n case 'action': {\n // Basic validation for action\n const actionParams = component.params as any\n if (!actionParams.label) {\n errors.push({\n path: 'params',\n message: 'Action component must have label',\n code: 'INVALID_ACTION',\n })\n }\n break\n }\n\n default:\n // Known types without specific validation pass through — renderer handles errors\n // Truly unknown types (e.g. typos in streamed JSON) are rejected\n if (!KNOWN_COMPONENT_TYPES.has(component.type)) {\n errors.push({\n path: 'type',\n message: `Unknown component type: ${component.type}`,\n code: 'UNKNOWN_COMPONENT_TYPE',\n })\n }\n break\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate entire layout\n *\n * @param layout - The layout to validate\n * @param options - Optional validation options (limits, iframePolicy, customIframeDomains)\n */\nexport function validateLayout(\n layout: UILayout,\n options?: ValidationOptions\n): ValidationResult {\n const errors: ValidationResult['errors'] = []\n\n // Validate component count\n if (layout.components.length === 0) {\n errors.push({\n path: 'components',\n message: 'Layout must have at least one component',\n code: 'EMPTY_LAYOUT',\n })\n }\n\n if (layout.components.length > 12) {\n errors.push({\n path: 'components',\n message: `Layout exceeds max components: ${layout.components.length} > 12`,\n code: 'TOO_MANY_COMPONENTS',\n })\n }\n\n // Validate each component\n for (const [index, component] of layout.components.entries()) {\n const result = validateComponent(component, options)\n if (!result.valid) {\n errors.push(\n ...(result.errors?.map((error) => ({\n ...error,\n path: `components[${index}].${error.path}`,\n })) || [])\n )\n }\n }\n\n // Validate grid configuration\n if (layout.grid.columns !== 12) {\n errors.push({\n path: 'grid.columns',\n message: 'Grid must have 12 columns (Bootstrap-like)',\n code: 'INVALID_GRID_COLUMNS',\n })\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate a single form field value against field rules\n */\nexport function validateFieldValue(\n value: any,\n field: FormFieldParams\n): { valid: boolean; error?: string } {\n // Required check\n if (field.required) {\n if (value === undefined || value === null || value === '') {\n return { valid: false, error: `${field.label || field.name} is required` }\n }\n if (field.type === 'checkbox' && value !== true) {\n return { valid: false, error: `${field.label || field.name} must be checked` }\n }\n }\n\n // Skip further validation if value is empty and not required\n if (value === undefined || value === null || value === '') {\n return { valid: true }\n }\n\n // Type-specific validation\n switch (field.type) {\n case 'text':\n case 'textarea':\n case 'password':\n if (field.minLength && String(value).length < field.minLength) {\n return { valid: false, error: `Minimum ${field.minLength} characters required` }\n }\n if (field.maxLength && String(value).length > field.maxLength) {\n return { valid: false, error: `Maximum ${field.maxLength} characters allowed` }\n }\n if (field.pattern && !new RegExp(field.pattern).test(String(value))) {\n return { valid: false, error: 'Invalid format' }\n }\n break\n\n case 'email':\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(String(value))) {\n return { valid: false, error: 'Invalid email address' }\n }\n break\n\n case 'number': {\n const numValue = Number(value)\n if (isNaN(numValue)) {\n return { valid: false, error: 'Must be a valid number' }\n }\n if (field.min !== undefined && numValue < field.min) {\n return { valid: false, error: `Minimum value is ${field.min}` }\n }\n if (field.max !== undefined && numValue > field.max) {\n return { valid: false, error: `Maximum value is ${field.max}` }\n }\n break\n }\n\n case 'date':\n if (field.minDate && value < field.minDate) {\n return { valid: false, error: `Date must be after ${field.minDate}` }\n }\n if (field.maxDate && value > field.maxDate) {\n return { valid: false, error: `Date must be before ${field.maxDate}` }\n }\n break\n\n case 'select':\n case 'radio':\n // Validate that value is one of the options\n if (field.options && field.options.length > 0) {\n const validValues = field.options.map((opt) => opt.value)\n if (!validValues.includes(String(value))) {\n return { valid: false, error: 'Please select a valid option' }\n }\n }\n break\n }\n\n return { valid: true }\n}\n\n/**\n * Validate entire form data against field definitions\n */\nexport function validateFormData(\n data: Record<string, any>,\n fields: FormFieldParams[]\n): { valid: boolean; errors: Record<string, string> } {\n const errors: Record<string, string> = {}\n\n for (const field of fields) {\n const result = validateFieldValue(data[field.name], field)\n if (!result.valid && result.error) {\n errors[field.name] = result.error\n }\n }\n\n return {\n valid: Object.keys(errors).length === 0,\n errors,\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/services/validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EACR,gBAAgB,EAChB,cAAc,EACd,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,EACf,YAAY,EACZ,iBAAiB,EAElB,MAAM,UAAU,CAAA;AAYjB;;GAEG;AACH,eAAO,MAAM,uBAAuB,EAAE,cAKrC,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,UAgHlC,CAAA;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,WAAW,CAAC,UAAU,CAAC,GAAG,gBAAgB,CA6DxF;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,oBAAoB,EAC5B,MAAM,GAAE,cAAwC,GAC/C,gBAAgB,
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/services/validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EACR,gBAAgB,EAChB,cAAc,EACd,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,EACf,YAAY,EACZ,iBAAiB,EAElB,MAAM,UAAU,CAAA;AAYjB;;GAEG;AACH,eAAO,MAAM,uBAAuB,EAAE,cAKrC,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,UAgHlC,CAAA;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,WAAW,CAAC,UAAU,CAAC,GAAG,gBAAgB,CA6DxF;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,oBAAoB,EAC5B,MAAM,GAAE,cAAwC,GAC/C,gBAAgB,CAyDlB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,oBAAoB,EAC5B,MAAM,GAAE,cAAwC,GAC/C,gBAAgB,CAmDlB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,WAAW,EACtB,MAAM,GAAE,cAAwC,GAC/C,gBAAgB,CAiBlB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKpD;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,YAAY,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAC5D,gBAAgB,CA8ClB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,WAAW,EACtB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,gBAAgB,CA+IlB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,QAAQ,EAChB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,gBAAgB,CA8ClB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,GAAG,EACV,KAAK,EAAE,eAAe,GACrB;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CA0EpC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzB,MAAM,EAAE,eAAe,EAAE,GACxB;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAcpD"}
|
|
@@ -179,6 +179,15 @@ function validateGridPosition(position) {
|
|
|
179
179
|
}
|
|
180
180
|
function validateChartComponent(params, limits = DEFAULT_RESOURCE_LIMITS) {
|
|
181
181
|
const errors = [];
|
|
182
|
+
if (!(params == null ? void 0 : params.data)) {
|
|
183
|
+
return { valid: false, errors: [{ path: "params.data", message: "Missing chart data object", code: "MISSING_DATA" }] };
|
|
184
|
+
}
|
|
185
|
+
if (!Array.isArray(params.data.datasets)) {
|
|
186
|
+
return { valid: false, errors: [{ path: "params.data.datasets", message: "Missing or invalid datasets array", code: "MISSING_DATASETS" }] };
|
|
187
|
+
}
|
|
188
|
+
if (!Array.isArray(params.data.labels)) {
|
|
189
|
+
return { valid: false, errors: [{ path: "params.data.labels", message: "Missing or invalid labels array", code: "MISSING_LABELS" }] };
|
|
190
|
+
}
|
|
182
191
|
const totalDataPoints = params.data.datasets.reduce(
|
|
183
192
|
(sum, dataset) => sum + dataset.data.length,
|
|
184
193
|
0
|
|
@@ -321,6 +330,9 @@ function validateIframeDomain(url, options) {
|
|
|
321
330
|
function validateComponent(component, options) {
|
|
322
331
|
const limits = (options == null ? void 0 : options.limits) ?? DEFAULT_RESOURCE_LIMITS;
|
|
323
332
|
const errors = [];
|
|
333
|
+
if (!component.params) {
|
|
334
|
+
return { valid: false, errors: [{ path: "params", message: "Missing component params", code: "MISSING_PARAMS" }] };
|
|
335
|
+
}
|
|
324
336
|
const gridResult = validateGridPosition(component.position);
|
|
325
337
|
if (!gridResult.valid) {
|
|
326
338
|
errors.push(...gridResult.errors || []);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.js","sources":["../../src/services/validation.ts"],"sourcesContent":["/**\n * Component Validation Service\n * Phase 0: Resource Limits & Schema Validation\n *\n * Validates LLM-generated components against:\n * - JSON schema\n * - Resource limits (data points, payload size, grid bounds)\n * - Security constraints (domain whitelist, XSS prevention)\n */\n\nimport type {\n UIComponent,\n UILayout,\n ValidationResult,\n ResourceLimits,\n ChartComponentParams,\n TableComponentParams,\n FormFieldParams,\n IframePolicy,\n ValidationOptions,\n ComponentType,\n} from '../types'\n\n/**\n * All known ComponentType values — used to distinguish known-but-unvalidated\n * types (pass through) from truly unknown strings (reject).\n */\nconst KNOWN_COMPONENT_TYPES: Set<string> = new Set<ComponentType>([\n 'chart', 'table', 'metric', 'text', 'grid', 'iframe', 'image', 'link',\n 'action', 'footer', 'carousel', 'artifact', 'form', 'modal',\n 'action-group', 'image-gallery', 'video', 'code', 'map',\n])\n\n/**\n * Default resource limits (configurable via env)\n */\nexport const DEFAULT_RESOURCE_LIMITS: ResourceLimits = {\n maxDataPoints: 1000,\n maxTableRows: 100,\n maxPayloadSize: 50 * 1024, // 50KB\n renderTimeout: 5000, // 5 seconds\n}\n\n/**\n * Default allowed iframe domains (whitelist)\n * Must match CSP frame-src directive\n * Updated Sprint 7: Added code, design, docs, and map providers\n *\n * This list is exported for transparency and can be extended via ValidationOptions\n */\nexport const DEFAULT_IFRAME_DOMAINS = [\n // Charts\n 'quickchart.io',\n 'www.quickchart.io',\n\n // Deposium\n 'deposium.com',\n 'deposium.vip',\n 'deposium.ai',\n\n // Development\n 'localhost',\n\n // Video providers (Sprint 5)\n 'youtube.com',\n 'www.youtube.com',\n 'youtube-nocookie.com',\n 'www.youtube-nocookie.com',\n 'youtu.be',\n 'vimeo.com',\n 'player.vimeo.com',\n\n // Code playgrounds (Sprint 7)\n 'codepen.io',\n 'codesandbox.io',\n 'stackblitz.com',\n 'jsfiddle.net',\n\n // Design tools (Sprint 7)\n 'figma.com',\n 'www.figma.com',\n 'miro.com',\n\n // Google services (Sprint 7)\n 'docs.google.com',\n 'drive.google.com',\n 'sheets.google.com',\n 'slides.google.com',\n 'maps.google.com',\n 'www.google.com',\n 'datastudio.google.com',\n 'lookerstudio.google.com',\n\n // Productivity (Sprint 7)\n 'airtable.com',\n 'notion.so',\n 'www.notion.so',\n\n // Maps (Sprint 7)\n 'openstreetmap.org',\n 'www.openstreetmap.org',\n\n // Analytics/Dashboards (Sprint 7)\n 'public.tableau.com',\n 'app.powerbi.com',\n 'observablehq.com',\n\n // Diagrams & Whiteboards (v2.0.0)\n 'mermaid.live',\n 'excalidraw.com',\n 'lucidchart.com',\n 'lucid.app',\n\n // Video - Business (v2.0.0)\n 'loom.com',\n 'www.loom.com',\n 'cloudflarestream.com',\n 'streamable.com',\n\n // Code repositories (v2.0.0)\n 'github.com',\n 'gist.github.com',\n 'gitlab.com',\n 'replit.com',\n 'glitch.com',\n\n // Business tools (v2.0.0)\n 'calendly.com',\n 'typeform.com',\n 'cal.com',\n\n // Design (v2.0.0)\n 'canva.com',\n\n // Deploy previews (v2.0.0)\n 'vercel.app',\n 'netlify.app',\n\n // E-commerce (v2.0.0)\n 'amazon.com',\n 'amazon.fr',\n 'amazon.de',\n 'amazon.co.uk',\n 'amazon.es',\n 'amazon.it',\n 'amazon.ca',\n 'amazon.co.jp',\n 'images-amazon.com',\n 'media-amazon.com',\n 'ws-na.amazon-adsystem.com',\n\n // MCP Connectors — embed-capable services (v2.2.7)\n 'gamma.app',\n 'www.gamma.app',\n 'app.hubspot.com',\n 'share.hubspot.com',\n 'www.data.gouv.fr',\n 'data.gouv.fr',\n 'clinicaltrials.gov',\n 'www.clinicaltrials.gov',\n 'linear.app',\n 'www.linear.app',\n]\n\n/**\n * Validate grid position bounds (1-12 columns)\n */\nexport function validateGridPosition(position: UIComponent['position']): ValidationResult {\n const errors: ValidationResult['errors'] = []\n\n // ✅ PHASE 3 FIX: Defensive check for undefined position\n if (!position) {\n return {\n valid: false,\n errors: [\n {\n path: 'position',\n message: 'Position is required',\n code: 'MISSING_POSITION',\n },\n ],\n }\n }\n\n if (position.colStart < 1 || position.colStart > 12) {\n errors.push({\n path: 'position.colStart',\n message: 'Column start must be between 1 and 12',\n code: 'INVALID_GRID_COL_START',\n })\n }\n\n if (position.colSpan < 1 || position.colSpan > 12) {\n errors.push({\n path: 'position.colSpan',\n message: 'Column span must be between 1 and 12',\n code: 'INVALID_GRID_COL_SPAN',\n })\n }\n\n if (position.colStart + position.colSpan - 1 > 12) {\n errors.push({\n path: 'position',\n message: 'Column start + span exceeds grid width (12)',\n code: 'GRID_OVERFLOW',\n })\n }\n\n if (position.rowStart !== undefined && position.rowStart < 1) {\n errors.push({\n path: 'position.rowStart',\n message: 'Row start must be >= 1',\n code: 'INVALID_GRID_ROW_START',\n })\n }\n\n if (position.rowSpan !== undefined && position.rowSpan < 1) {\n errors.push({\n path: 'position.rowSpan',\n message: 'Row span must be >= 1',\n code: 'INVALID_GRID_ROW_SPAN',\n })\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate chart component against resource limits\n */\nexport function validateChartComponent(\n params: ChartComponentParams,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\n): ValidationResult {\n const errors: ValidationResult['errors'] = []\n\n // Validate data points count\n const totalDataPoints = params.data.datasets.reduce(\n (sum, dataset) => sum + dataset.data.length,\n 0\n )\n\n if (totalDataPoints > limits.maxDataPoints) {\n errors.push({\n path: 'params.data',\n message: `Chart exceeds max data points: ${totalDataPoints} > ${limits.maxDataPoints}`,\n code: 'RESOURCE_LIMIT_EXCEEDED',\n })\n }\n\n // Validate labels match dataset length\n const expectedLength = params.data.labels.length\n for (const [index, dataset] of params.data.datasets.entries()) {\n if (dataset.data.length !== expectedLength) {\n errors.push({\n path: `params.data.datasets[${index}]`,\n message: `Dataset length mismatch: expected ${expectedLength}, got ${dataset.data.length}`,\n code: 'DATA_LENGTH_MISMATCH',\n })\n }\n }\n\n // Validate numeric data\n for (const [index, dataset] of params.data.datasets.entries()) {\n for (const [dataIndex, value] of dataset.data.entries()) {\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n errors.push({\n path: `params.data.datasets[${index}].data[${dataIndex}]`,\n message: `Invalid data value: ${value} (must be finite number)`,\n code: 'INVALID_DATA_TYPE',\n })\n }\n }\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate table component against resource limits\n */\nexport function validateTableComponent(\n params: TableComponentParams,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\n): ValidationResult {\n const errors: ValidationResult['errors'] = []\n\n // Validate row count\n if (params.rows.length > limits.maxTableRows) {\n errors.push({\n path: 'params.rows',\n message: `Table exceeds max rows: ${params.rows.length} > ${limits.maxTableRows}`,\n code: 'RESOURCE_LIMIT_EXCEEDED',\n })\n }\n\n // Validate columns\n if (params.columns.length === 0) {\n errors.push({\n path: 'params.columns',\n message: 'Table must have at least one column',\n code: 'EMPTY_COLUMNS',\n })\n }\n\n // Validate column keys are unique\n const columnKeys = new Set<string>()\n for (const [index, column] of params.columns.entries()) {\n if (columnKeys.has(column.key)) {\n errors.push({\n path: `params.columns[${index}]`,\n message: `Duplicate column key: ${column.key}`,\n code: 'DUPLICATE_COLUMN_KEY',\n })\n }\n columnKeys.add(column.key)\n }\n\n // Validate rows have valid data for defined columns\n for (const [rowIndex, row] of params.rows.entries()) {\n for (const column of params.columns) {\n if (!(column.key in row)) {\n errors.push({\n path: `params.rows[${rowIndex}]`,\n message: `Missing column key: ${column.key}`,\n code: 'MISSING_COLUMN_DATA',\n })\n }\n }\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate payload size\n */\nexport function validatePayloadSize(\n component: UIComponent,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\n): ValidationResult {\n const payloadSize = JSON.stringify(component).length\n\n if (payloadSize > limits.maxPayloadSize) {\n return {\n valid: false,\n errors: [\n {\n path: 'component',\n message: `Payload size exceeds limit: ${payloadSize} > ${limits.maxPayloadSize} bytes`,\n code: 'PAYLOAD_TOO_LARGE',\n },\n ],\n }\n }\n\n return { valid: true }\n}\n\n/**\n * Sanitize string to prevent XSS\n * Basic implementation - DOMPurify used at render time\n */\nexport function sanitizeString(input: string): string {\n return input\n .replace(/<script\\b[^<]*(?:(?!<\\/script>)<[^<]*)*<\\/script>/gi, '')\n .replace(/on\\w+=\"[^\"]*\"/gi, '')\n .replace(/javascript:/gi, '')\n}\n\n/**\n * Validate iframe domain against whitelist\n *\n * @param url - The URL to validate\n * @param options - Optional validation options\n * @param options.policy - 'strict' (default), 'extend', or 'allow-all'\n * @param options.customDomains - Additional domains when policy is 'extend'\n */\nexport function validateIframeDomain(\n url: string,\n options?: { policy?: IframePolicy; customDomains?: string[] }\n): ValidationResult {\n // If allow-all, skip validation\n if (options?.policy === 'allow-all') {\n return { valid: true }\n }\n\n try {\n const parsedUrl = new URL(url)\n const domain = parsedUrl.hostname\n\n // Build effective whitelist\n let effectiveWhitelist = DEFAULT_IFRAME_DOMAINS\n if (options?.policy === 'extend' && options.customDomains) {\n effectiveWhitelist = [...DEFAULT_IFRAME_DOMAINS, ...options.customDomains]\n }\n\n const isAllowed = effectiveWhitelist.some(\n (allowed) => domain === allowed || domain.endsWith(`.${allowed}`) || allowed === 'localhost'\n )\n\n if (!isAllowed) {\n return {\n valid: false,\n errors: [\n {\n path: 'url',\n message: `Domain not whitelisted: ${domain}`,\n code: 'DOMAIN_NOT_WHITELISTED',\n },\n ],\n }\n }\n\n return { valid: true }\n } catch (error) {\n return {\n valid: false,\n errors: [\n {\n path: 'url',\n message: 'Invalid URL format',\n code: 'INVALID_URL',\n },\n ],\n }\n }\n}\n\n/**\n * Validate entire component\n *\n * @param component - The component to validate\n * @param options - Optional validation options (limits, iframePolicy, customIframeDomains)\n */\nexport function validateComponent(\n component: UIComponent,\n options?: ValidationOptions\n): ValidationResult {\n const limits = options?.limits ?? DEFAULT_RESOURCE_LIMITS\n const errors: ValidationResult['errors'] = []\n\n // Validate grid position\n const gridResult = validateGridPosition(component.position)\n if (!gridResult.valid) {\n errors.push(...(gridResult.errors || []))\n }\n\n // Validate payload size\n const sizeResult = validatePayloadSize(component, limits)\n if (!sizeResult.valid) {\n errors.push(...(sizeResult.errors || []))\n }\n\n // Type-specific validation\n switch (component.type) {\n case 'chart': {\n const chartResult = validateChartComponent(component.params as ChartComponentParams, limits)\n if (!chartResult.valid) {\n errors.push(...(chartResult.errors || []))\n }\n break\n }\n\n case 'table': {\n const tableResult = validateTableComponent(component.params as TableComponentParams, limits)\n if (!tableResult.valid) {\n errors.push(...(tableResult.errors || []))\n }\n break\n }\n\n case 'metric': {\n // Basic validation for metrics\n const metricParams = component.params as any\n if (!metricParams.title || !metricParams.value) {\n errors.push({\n path: 'params',\n message: 'Metric must have title and value',\n code: 'INVALID_METRIC',\n })\n }\n break\n }\n\n case 'text': {\n // Basic validation for text\n const textParams = component.params as any\n if (!textParams.content) {\n errors.push({\n path: 'params',\n message: 'Text component must have content',\n code: 'INVALID_TEXT',\n })\n }\n break\n }\n\n case 'iframe': {\n // Basic validation for iframe\n const iframeParams = component.params as any\n if (!iframeParams.url) {\n errors.push({\n path: 'params',\n message: 'Iframe component must have url',\n code: 'INVALID_IFRAME',\n })\n } else {\n // Validate iframe domain against whitelist\n const iframeResult = validateIframeDomain(iframeParams.url, {\n policy: options?.iframePolicy,\n customDomains: options?.customIframeDomains,\n })\n if (!iframeResult.valid) {\n errors.push(...(iframeResult.errors || []))\n }\n }\n break\n }\n\n case 'image': {\n // Basic validation for image\n const imageParams = component.params as any\n if (!imageParams.url) {\n errors.push({\n path: 'params',\n message: 'Image component must have url',\n code: 'INVALID_IMAGE',\n })\n }\n break\n }\n\n case 'link': {\n // Basic validation for link\n const linkParams = component.params as any\n if (!linkParams.url) {\n errors.push({\n path: 'params',\n message: 'Link component must have url',\n code: 'INVALID_LINK',\n })\n }\n break\n }\n\n case 'action': {\n // Basic validation for action\n const actionParams = component.params as any\n if (!actionParams.label) {\n errors.push({\n path: 'params',\n message: 'Action component must have label',\n code: 'INVALID_ACTION',\n })\n }\n break\n }\n\n default:\n // Known types without specific validation pass through — renderer handles errors\n // Truly unknown types (e.g. typos in streamed JSON) are rejected\n if (!KNOWN_COMPONENT_TYPES.has(component.type)) {\n errors.push({\n path: 'type',\n message: `Unknown component type: ${component.type}`,\n code: 'UNKNOWN_COMPONENT_TYPE',\n })\n }\n break\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate entire layout\n *\n * @param layout - The layout to validate\n * @param options - Optional validation options (limits, iframePolicy, customIframeDomains)\n */\nexport function validateLayout(\n layout: UILayout,\n options?: ValidationOptions\n): ValidationResult {\n const errors: ValidationResult['errors'] = []\n\n // Validate component count\n if (layout.components.length === 0) {\n errors.push({\n path: 'components',\n message: 'Layout must have at least one component',\n code: 'EMPTY_LAYOUT',\n })\n }\n\n if (layout.components.length > 12) {\n errors.push({\n path: 'components',\n message: `Layout exceeds max components: ${layout.components.length} > 12`,\n code: 'TOO_MANY_COMPONENTS',\n })\n }\n\n // Validate each component\n for (const [index, component] of layout.components.entries()) {\n const result = validateComponent(component, options)\n if (!result.valid) {\n errors.push(\n ...(result.errors?.map((error) => ({\n ...error,\n path: `components[${index}].${error.path}`,\n })) || [])\n )\n }\n }\n\n // Validate grid configuration\n if (layout.grid.columns !== 12) {\n errors.push({\n path: 'grid.columns',\n message: 'Grid must have 12 columns (Bootstrap-like)',\n code: 'INVALID_GRID_COLUMNS',\n })\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate a single form field value against field rules\n */\nexport function validateFieldValue(\n value: any,\n field: FormFieldParams\n): { valid: boolean; error?: string } {\n // Required check\n if (field.required) {\n if (value === undefined || value === null || value === '') {\n return { valid: false, error: `${field.label || field.name} is required` }\n }\n if (field.type === 'checkbox' && value !== true) {\n return { valid: false, error: `${field.label || field.name} must be checked` }\n }\n }\n\n // Skip further validation if value is empty and not required\n if (value === undefined || value === null || value === '') {\n return { valid: true }\n }\n\n // Type-specific validation\n switch (field.type) {\n case 'text':\n case 'textarea':\n case 'password':\n if (field.minLength && String(value).length < field.minLength) {\n return { valid: false, error: `Minimum ${field.minLength} characters required` }\n }\n if (field.maxLength && String(value).length > field.maxLength) {\n return { valid: false, error: `Maximum ${field.maxLength} characters allowed` }\n }\n if (field.pattern && !new RegExp(field.pattern).test(String(value))) {\n return { valid: false, error: 'Invalid format' }\n }\n break\n\n case 'email':\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(String(value))) {\n return { valid: false, error: 'Invalid email address' }\n }\n break\n\n case 'number': {\n const numValue = Number(value)\n if (isNaN(numValue)) {\n return { valid: false, error: 'Must be a valid number' }\n }\n if (field.min !== undefined && numValue < field.min) {\n return { valid: false, error: `Minimum value is ${field.min}` }\n }\n if (field.max !== undefined && numValue > field.max) {\n return { valid: false, error: `Maximum value is ${field.max}` }\n }\n break\n }\n\n case 'date':\n if (field.minDate && value < field.minDate) {\n return { valid: false, error: `Date must be after ${field.minDate}` }\n }\n if (field.maxDate && value > field.maxDate) {\n return { valid: false, error: `Date must be before ${field.maxDate}` }\n }\n break\n\n case 'select':\n case 'radio':\n // Validate that value is one of the options\n if (field.options && field.options.length > 0) {\n const validValues = field.options.map((opt) => opt.value)\n if (!validValues.includes(String(value))) {\n return { valid: false, error: 'Please select a valid option' }\n }\n }\n break\n }\n\n return { valid: true }\n}\n\n/**\n * Validate entire form data against field definitions\n */\nexport function validateFormData(\n data: Record<string, any>,\n fields: FormFieldParams[]\n): { valid: boolean; errors: Record<string, string> } {\n const errors: Record<string, string> = {}\n\n for (const field of fields) {\n const result = validateFieldValue(data[field.name], field)\n if (!result.valid && result.error) {\n errors[field.name] = result.error\n }\n }\n\n return {\n valid: Object.keys(errors).length === 0,\n errors,\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"validation.js","sources":["../../src/services/validation.ts"],"sourcesContent":["/**\n * Component Validation Service\n * Phase 0: Resource Limits & Schema Validation\n *\n * Validates LLM-generated components against:\n * - JSON schema\n * - Resource limits (data points, payload size, grid bounds)\n * - Security constraints (domain whitelist, XSS prevention)\n */\n\nimport type {\n UIComponent,\n UILayout,\n ValidationResult,\n ResourceLimits,\n ChartComponentParams,\n TableComponentParams,\n FormFieldParams,\n IframePolicy,\n ValidationOptions,\n ComponentType,\n} from '../types'\n\n/**\n * All known ComponentType values — used to distinguish known-but-unvalidated\n * types (pass through) from truly unknown strings (reject).\n */\nconst KNOWN_COMPONENT_TYPES: Set<string> = new Set<ComponentType>([\n 'chart', 'table', 'metric', 'text', 'grid', 'iframe', 'image', 'link',\n 'action', 'footer', 'carousel', 'artifact', 'form', 'modal',\n 'action-group', 'image-gallery', 'video', 'code', 'map',\n])\n\n/**\n * Default resource limits (configurable via env)\n */\nexport const DEFAULT_RESOURCE_LIMITS: ResourceLimits = {\n maxDataPoints: 1000,\n maxTableRows: 100,\n maxPayloadSize: 50 * 1024, // 50KB\n renderTimeout: 5000, // 5 seconds\n}\n\n/**\n * Default allowed iframe domains (whitelist)\n * Must match CSP frame-src directive\n * Updated Sprint 7: Added code, design, docs, and map providers\n *\n * This list is exported for transparency and can be extended via ValidationOptions\n */\nexport const DEFAULT_IFRAME_DOMAINS = [\n // Charts\n 'quickchart.io',\n 'www.quickchart.io',\n\n // Deposium\n 'deposium.com',\n 'deposium.vip',\n 'deposium.ai',\n\n // Development\n 'localhost',\n\n // Video providers (Sprint 5)\n 'youtube.com',\n 'www.youtube.com',\n 'youtube-nocookie.com',\n 'www.youtube-nocookie.com',\n 'youtu.be',\n 'vimeo.com',\n 'player.vimeo.com',\n\n // Code playgrounds (Sprint 7)\n 'codepen.io',\n 'codesandbox.io',\n 'stackblitz.com',\n 'jsfiddle.net',\n\n // Design tools (Sprint 7)\n 'figma.com',\n 'www.figma.com',\n 'miro.com',\n\n // Google services (Sprint 7)\n 'docs.google.com',\n 'drive.google.com',\n 'sheets.google.com',\n 'slides.google.com',\n 'maps.google.com',\n 'www.google.com',\n 'datastudio.google.com',\n 'lookerstudio.google.com',\n\n // Productivity (Sprint 7)\n 'airtable.com',\n 'notion.so',\n 'www.notion.so',\n\n // Maps (Sprint 7)\n 'openstreetmap.org',\n 'www.openstreetmap.org',\n\n // Analytics/Dashboards (Sprint 7)\n 'public.tableau.com',\n 'app.powerbi.com',\n 'observablehq.com',\n\n // Diagrams & Whiteboards (v2.0.0)\n 'mermaid.live',\n 'excalidraw.com',\n 'lucidchart.com',\n 'lucid.app',\n\n // Video - Business (v2.0.0)\n 'loom.com',\n 'www.loom.com',\n 'cloudflarestream.com',\n 'streamable.com',\n\n // Code repositories (v2.0.0)\n 'github.com',\n 'gist.github.com',\n 'gitlab.com',\n 'replit.com',\n 'glitch.com',\n\n // Business tools (v2.0.0)\n 'calendly.com',\n 'typeform.com',\n 'cal.com',\n\n // Design (v2.0.0)\n 'canva.com',\n\n // Deploy previews (v2.0.0)\n 'vercel.app',\n 'netlify.app',\n\n // E-commerce (v2.0.0)\n 'amazon.com',\n 'amazon.fr',\n 'amazon.de',\n 'amazon.co.uk',\n 'amazon.es',\n 'amazon.it',\n 'amazon.ca',\n 'amazon.co.jp',\n 'images-amazon.com',\n 'media-amazon.com',\n 'ws-na.amazon-adsystem.com',\n\n // MCP Connectors — embed-capable services (v2.2.7)\n 'gamma.app',\n 'www.gamma.app',\n 'app.hubspot.com',\n 'share.hubspot.com',\n 'www.data.gouv.fr',\n 'data.gouv.fr',\n 'clinicaltrials.gov',\n 'www.clinicaltrials.gov',\n 'linear.app',\n 'www.linear.app',\n]\n\n/**\n * Validate grid position bounds (1-12 columns)\n */\nexport function validateGridPosition(position: UIComponent['position']): ValidationResult {\n const errors: ValidationResult['errors'] = []\n\n // ✅ PHASE 3 FIX: Defensive check for undefined position\n if (!position) {\n return {\n valid: false,\n errors: [\n {\n path: 'position',\n message: 'Position is required',\n code: 'MISSING_POSITION',\n },\n ],\n }\n }\n\n if (position.colStart < 1 || position.colStart > 12) {\n errors.push({\n path: 'position.colStart',\n message: 'Column start must be between 1 and 12',\n code: 'INVALID_GRID_COL_START',\n })\n }\n\n if (position.colSpan < 1 || position.colSpan > 12) {\n errors.push({\n path: 'position.colSpan',\n message: 'Column span must be between 1 and 12',\n code: 'INVALID_GRID_COL_SPAN',\n })\n }\n\n if (position.colStart + position.colSpan - 1 > 12) {\n errors.push({\n path: 'position',\n message: 'Column start + span exceeds grid width (12)',\n code: 'GRID_OVERFLOW',\n })\n }\n\n if (position.rowStart !== undefined && position.rowStart < 1) {\n errors.push({\n path: 'position.rowStart',\n message: 'Row start must be >= 1',\n code: 'INVALID_GRID_ROW_START',\n })\n }\n\n if (position.rowSpan !== undefined && position.rowSpan < 1) {\n errors.push({\n path: 'position.rowSpan',\n message: 'Row span must be >= 1',\n code: 'INVALID_GRID_ROW_SPAN',\n })\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate chart component against resource limits\n */\nexport function validateChartComponent(\n params: ChartComponentParams,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\n): ValidationResult {\n const errors: ValidationResult['errors'] = []\n\n // Guard: params.data must exist with labels + datasets\n if (!params?.data) {\n return { valid: false, errors: [{ path: 'params.data', message: 'Missing chart data object', code: 'MISSING_DATA' }] }\n }\n if (!Array.isArray(params.data.datasets)) {\n return { valid: false, errors: [{ path: 'params.data.datasets', message: 'Missing or invalid datasets array', code: 'MISSING_DATASETS' }] }\n }\n if (!Array.isArray(params.data.labels)) {\n return { valid: false, errors: [{ path: 'params.data.labels', message: 'Missing or invalid labels array', code: 'MISSING_LABELS' }] }\n }\n\n // Validate data points count\n const totalDataPoints = params.data.datasets.reduce(\n (sum, dataset) => sum + dataset.data.length,\n 0\n )\n\n if (totalDataPoints > limits.maxDataPoints) {\n errors.push({\n path: 'params.data',\n message: `Chart exceeds max data points: ${totalDataPoints} > ${limits.maxDataPoints}`,\n code: 'RESOURCE_LIMIT_EXCEEDED',\n })\n }\n\n // Validate labels match dataset length\n const expectedLength = params.data.labels.length\n for (const [index, dataset] of params.data.datasets.entries()) {\n if (dataset.data.length !== expectedLength) {\n errors.push({\n path: `params.data.datasets[${index}]`,\n message: `Dataset length mismatch: expected ${expectedLength}, got ${dataset.data.length}`,\n code: 'DATA_LENGTH_MISMATCH',\n })\n }\n }\n\n // Validate numeric data\n for (const [index, dataset] of params.data.datasets.entries()) {\n for (const [dataIndex, value] of dataset.data.entries()) {\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n errors.push({\n path: `params.data.datasets[${index}].data[${dataIndex}]`,\n message: `Invalid data value: ${value} (must be finite number)`,\n code: 'INVALID_DATA_TYPE',\n })\n }\n }\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate table component against resource limits\n */\nexport function validateTableComponent(\n params: TableComponentParams,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\n): ValidationResult {\n const errors: ValidationResult['errors'] = []\n\n // Validate row count\n if (params.rows.length > limits.maxTableRows) {\n errors.push({\n path: 'params.rows',\n message: `Table exceeds max rows: ${params.rows.length} > ${limits.maxTableRows}`,\n code: 'RESOURCE_LIMIT_EXCEEDED',\n })\n }\n\n // Validate columns\n if (params.columns.length === 0) {\n errors.push({\n path: 'params.columns',\n message: 'Table must have at least one column',\n code: 'EMPTY_COLUMNS',\n })\n }\n\n // Validate column keys are unique\n const columnKeys = new Set<string>()\n for (const [index, column] of params.columns.entries()) {\n if (columnKeys.has(column.key)) {\n errors.push({\n path: `params.columns[${index}]`,\n message: `Duplicate column key: ${column.key}`,\n code: 'DUPLICATE_COLUMN_KEY',\n })\n }\n columnKeys.add(column.key)\n }\n\n // Validate rows have valid data for defined columns\n for (const [rowIndex, row] of params.rows.entries()) {\n for (const column of params.columns) {\n if (!(column.key in row)) {\n errors.push({\n path: `params.rows[${rowIndex}]`,\n message: `Missing column key: ${column.key}`,\n code: 'MISSING_COLUMN_DATA',\n })\n }\n }\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate payload size\n */\nexport function validatePayloadSize(\n component: UIComponent,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\n): ValidationResult {\n const payloadSize = JSON.stringify(component).length\n\n if (payloadSize > limits.maxPayloadSize) {\n return {\n valid: false,\n errors: [\n {\n path: 'component',\n message: `Payload size exceeds limit: ${payloadSize} > ${limits.maxPayloadSize} bytes`,\n code: 'PAYLOAD_TOO_LARGE',\n },\n ],\n }\n }\n\n return { valid: true }\n}\n\n/**\n * Sanitize string to prevent XSS\n * Basic implementation - DOMPurify used at render time\n */\nexport function sanitizeString(input: string): string {\n return input\n .replace(/<script\\b[^<]*(?:(?!<\\/script>)<[^<]*)*<\\/script>/gi, '')\n .replace(/on\\w+=\"[^\"]*\"/gi, '')\n .replace(/javascript:/gi, '')\n}\n\n/**\n * Validate iframe domain against whitelist\n *\n * @param url - The URL to validate\n * @param options - Optional validation options\n * @param options.policy - 'strict' (default), 'extend', or 'allow-all'\n * @param options.customDomains - Additional domains when policy is 'extend'\n */\nexport function validateIframeDomain(\n url: string,\n options?: { policy?: IframePolicy; customDomains?: string[] }\n): ValidationResult {\n // If allow-all, skip validation\n if (options?.policy === 'allow-all') {\n return { valid: true }\n }\n\n try {\n const parsedUrl = new URL(url)\n const domain = parsedUrl.hostname\n\n // Build effective whitelist\n let effectiveWhitelist = DEFAULT_IFRAME_DOMAINS\n if (options?.policy === 'extend' && options.customDomains) {\n effectiveWhitelist = [...DEFAULT_IFRAME_DOMAINS, ...options.customDomains]\n }\n\n const isAllowed = effectiveWhitelist.some(\n (allowed) => domain === allowed || domain.endsWith(`.${allowed}`) || allowed === 'localhost'\n )\n\n if (!isAllowed) {\n return {\n valid: false,\n errors: [\n {\n path: 'url',\n message: `Domain not whitelisted: ${domain}`,\n code: 'DOMAIN_NOT_WHITELISTED',\n },\n ],\n }\n }\n\n return { valid: true }\n } catch (error) {\n return {\n valid: false,\n errors: [\n {\n path: 'url',\n message: 'Invalid URL format',\n code: 'INVALID_URL',\n },\n ],\n }\n }\n}\n\n/**\n * Validate entire component\n *\n * @param component - The component to validate\n * @param options - Optional validation options (limits, iframePolicy, customIframeDomains)\n */\nexport function validateComponent(\n component: UIComponent,\n options?: ValidationOptions\n): ValidationResult {\n const limits = options?.limits ?? DEFAULT_RESOURCE_LIMITS\n const errors: ValidationResult['errors'] = []\n\n // Guard: params must exist\n if (!component.params) {\n return { valid: false, errors: [{ path: 'params', message: 'Missing component params', code: 'MISSING_PARAMS' }] }\n }\n\n // Validate grid position\n const gridResult = validateGridPosition(component.position)\n if (!gridResult.valid) {\n errors.push(...(gridResult.errors || []))\n }\n\n // Validate payload size\n const sizeResult = validatePayloadSize(component, limits)\n if (!sizeResult.valid) {\n errors.push(...(sizeResult.errors || []))\n }\n\n // Type-specific validation\n switch (component.type) {\n case 'chart': {\n const chartResult = validateChartComponent(component.params as ChartComponentParams, limits)\n if (!chartResult.valid) {\n errors.push(...(chartResult.errors || []))\n }\n break\n }\n\n case 'table': {\n const tableResult = validateTableComponent(component.params as TableComponentParams, limits)\n if (!tableResult.valid) {\n errors.push(...(tableResult.errors || []))\n }\n break\n }\n\n case 'metric': {\n // Basic validation for metrics\n const metricParams = component.params as any\n if (!metricParams.title || !metricParams.value) {\n errors.push({\n path: 'params',\n message: 'Metric must have title and value',\n code: 'INVALID_METRIC',\n })\n }\n break\n }\n\n case 'text': {\n // Basic validation for text\n const textParams = component.params as any\n if (!textParams.content) {\n errors.push({\n path: 'params',\n message: 'Text component must have content',\n code: 'INVALID_TEXT',\n })\n }\n break\n }\n\n case 'iframe': {\n // Basic validation for iframe\n const iframeParams = component.params as any\n if (!iframeParams.url) {\n errors.push({\n path: 'params',\n message: 'Iframe component must have url',\n code: 'INVALID_IFRAME',\n })\n } else {\n // Validate iframe domain against whitelist\n const iframeResult = validateIframeDomain(iframeParams.url, {\n policy: options?.iframePolicy,\n customDomains: options?.customIframeDomains,\n })\n if (!iframeResult.valid) {\n errors.push(...(iframeResult.errors || []))\n }\n }\n break\n }\n\n case 'image': {\n // Basic validation for image\n const imageParams = component.params as any\n if (!imageParams.url) {\n errors.push({\n path: 'params',\n message: 'Image component must have url',\n code: 'INVALID_IMAGE',\n })\n }\n break\n }\n\n case 'link': {\n // Basic validation for link\n const linkParams = component.params as any\n if (!linkParams.url) {\n errors.push({\n path: 'params',\n message: 'Link component must have url',\n code: 'INVALID_LINK',\n })\n }\n break\n }\n\n case 'action': {\n // Basic validation for action\n const actionParams = component.params as any\n if (!actionParams.label) {\n errors.push({\n path: 'params',\n message: 'Action component must have label',\n code: 'INVALID_ACTION',\n })\n }\n break\n }\n\n default:\n // Known types without specific validation pass through — renderer handles errors\n // Truly unknown types (e.g. typos in streamed JSON) are rejected\n if (!KNOWN_COMPONENT_TYPES.has(component.type)) {\n errors.push({\n path: 'type',\n message: `Unknown component type: ${component.type}`,\n code: 'UNKNOWN_COMPONENT_TYPE',\n })\n }\n break\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate entire layout\n *\n * @param layout - The layout to validate\n * @param options - Optional validation options (limits, iframePolicy, customIframeDomains)\n */\nexport function validateLayout(\n layout: UILayout,\n options?: ValidationOptions\n): ValidationResult {\n const errors: ValidationResult['errors'] = []\n\n // Validate component count\n if (layout.components.length === 0) {\n errors.push({\n path: 'components',\n message: 'Layout must have at least one component',\n code: 'EMPTY_LAYOUT',\n })\n }\n\n if (layout.components.length > 12) {\n errors.push({\n path: 'components',\n message: `Layout exceeds max components: ${layout.components.length} > 12`,\n code: 'TOO_MANY_COMPONENTS',\n })\n }\n\n // Validate each component\n for (const [index, component] of layout.components.entries()) {\n const result = validateComponent(component, options)\n if (!result.valid) {\n errors.push(\n ...(result.errors?.map((error) => ({\n ...error,\n path: `components[${index}].${error.path}`,\n })) || [])\n )\n }\n }\n\n // Validate grid configuration\n if (layout.grid.columns !== 12) {\n errors.push({\n path: 'grid.columns',\n message: 'Grid must have 12 columns (Bootstrap-like)',\n code: 'INVALID_GRID_COLUMNS',\n })\n }\n\n return {\n valid: errors.length === 0,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Validate a single form field value against field rules\n */\nexport function validateFieldValue(\n value: any,\n field: FormFieldParams\n): { valid: boolean; error?: string } {\n // Required check\n if (field.required) {\n if (value === undefined || value === null || value === '') {\n return { valid: false, error: `${field.label || field.name} is required` }\n }\n if (field.type === 'checkbox' && value !== true) {\n return { valid: false, error: `${field.label || field.name} must be checked` }\n }\n }\n\n // Skip further validation if value is empty and not required\n if (value === undefined || value === null || value === '') {\n return { valid: true }\n }\n\n // Type-specific validation\n switch (field.type) {\n case 'text':\n case 'textarea':\n case 'password':\n if (field.minLength && String(value).length < field.minLength) {\n return { valid: false, error: `Minimum ${field.minLength} characters required` }\n }\n if (field.maxLength && String(value).length > field.maxLength) {\n return { valid: false, error: `Maximum ${field.maxLength} characters allowed` }\n }\n if (field.pattern && !new RegExp(field.pattern).test(String(value))) {\n return { valid: false, error: 'Invalid format' }\n }\n break\n\n case 'email':\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(String(value))) {\n return { valid: false, error: 'Invalid email address' }\n }\n break\n\n case 'number': {\n const numValue = Number(value)\n if (isNaN(numValue)) {\n return { valid: false, error: 'Must be a valid number' }\n }\n if (field.min !== undefined && numValue < field.min) {\n return { valid: false, error: `Minimum value is ${field.min}` }\n }\n if (field.max !== undefined && numValue > field.max) {\n return { valid: false, error: `Maximum value is ${field.max}` }\n }\n break\n }\n\n case 'date':\n if (field.minDate && value < field.minDate) {\n return { valid: false, error: `Date must be after ${field.minDate}` }\n }\n if (field.maxDate && value > field.maxDate) {\n return { valid: false, error: `Date must be before ${field.maxDate}` }\n }\n break\n\n case 'select':\n case 'radio':\n // Validate that value is one of the options\n if (field.options && field.options.length > 0) {\n const validValues = field.options.map((opt) => opt.value)\n if (!validValues.includes(String(value))) {\n return { valid: false, error: 'Please select a valid option' }\n }\n }\n break\n }\n\n return { valid: true }\n}\n\n/**\n * Validate entire form data against field definitions\n */\nexport function validateFormData(\n data: Record<string, any>,\n fields: FormFieldParams[]\n): { valid: boolean; errors: Record<string, string> } {\n const errors: Record<string, string> = {}\n\n for (const field of fields) {\n const result = validateFieldValue(data[field.name], field)\n if (!result.valid && result.error) {\n errors[field.name] = result.error\n }\n }\n\n return {\n valid: Object.keys(errors).length === 0,\n errors,\n }\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ChartRenderer reactivity fix
|
|
3
|
+
* Verifies the <Show> pattern is used instead of synchronous if
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest'
|
|
7
|
+
import { readFileSync } from 'fs'
|
|
8
|
+
import { resolve } from 'path'
|
|
9
|
+
|
|
10
|
+
describe('ChartRenderer reactivity', () => {
|
|
11
|
+
const source = readFileSync(
|
|
12
|
+
resolve(__dirname, 'UIResourceRenderer.tsx'),
|
|
13
|
+
'utf-8'
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
it('does NOT use synchronous if(useNative()) for rendering', () => {
|
|
17
|
+
// The bug was: if (useNative()) { return <ChartJSRenderer /> }
|
|
18
|
+
// This pattern evaluates once at mount — useNative() is always false
|
|
19
|
+
const hasSyncIf = /if\s*\(useNative\(\)\)\s*\{[\s\S]*?return\s*\(?\s*<ChartJSRenderer/.test(source)
|
|
20
|
+
expect(hasSyncIf).toBe(false)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('uses reactive <Show when={useNative()}> pattern', () => {
|
|
24
|
+
// The fix: <Show when={useNative()}> ... <ChartJSRenderer /> ... </Show>
|
|
25
|
+
const hasReactiveShow = /Show[\s\S]*?when=\{useNative\(\)\}/.test(source)
|
|
26
|
+
expect(hasReactiveShow).toBe(true)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('has ChartJSRenderer as Show children (not in fallback)', () => {
|
|
30
|
+
// ChartJSRenderer is between </Show>'s closing tag of the fallback and the outer </Show>
|
|
31
|
+
// i.e. it's the children of <Show when={useNative()}>, rendered when native is true
|
|
32
|
+
const showIdx = source.indexOf('when={useNative()}')
|
|
33
|
+
expect(showIdx).toBeGreaterThan(-1)
|
|
34
|
+
// The entire Show block should contain ChartJSRenderer
|
|
35
|
+
const afterShow = source.slice(showIdx, showIdx + 3000)
|
|
36
|
+
expect(afterShow).toContain('<ChartJSRenderer')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('has quickchart.io URL in the iframe fallback', () => {
|
|
40
|
+
// The fallback should build a quickchart URL
|
|
41
|
+
expect(source).toContain('quickchart.io')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -114,6 +114,15 @@ function ChartRenderer(props: {
|
|
|
114
114
|
const params = () => props.component.params as any
|
|
115
115
|
const rendererPref = () => params()?.renderer || 'auto'
|
|
116
116
|
|
|
117
|
+
// Guard: if data or datasets missing, show error instead of crashing Chart.js
|
|
118
|
+
if (!params()?.data?.datasets) {
|
|
119
|
+
return (
|
|
120
|
+
<div class="p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
|
121
|
+
<p class="text-red-500 dark:text-red-400 text-sm">Invalid chart data: missing data.datasets</p>
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
117
126
|
// Check renderer preference and Chart.js availability
|
|
118
127
|
createEffect(async () => {
|
|
119
128
|
const pref = rendererPref()
|
|
@@ -168,9 +177,55 @@ function ChartRenderer(props: {
|
|
|
168
177
|
setIsLoading(false)
|
|
169
178
|
}
|
|
170
179
|
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
180
|
+
// Reactive switch between native Chart.js and iframe fallback
|
|
181
|
+
// Must use <Show> — signals in component body are not reactive in SolidJS
|
|
182
|
+
return (
|
|
183
|
+
<Show
|
|
184
|
+
when={useNative()}
|
|
185
|
+
fallback={
|
|
186
|
+
<div class="relative w-full h-full min-h-[300px] bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
|
187
|
+
<Show when={isLoading()}>
|
|
188
|
+
<div class="absolute inset-0 flex items-center justify-center">
|
|
189
|
+
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
|
190
|
+
</div>
|
|
191
|
+
</Show>
|
|
192
|
+
|
|
193
|
+
<Show when={error()}>
|
|
194
|
+
<div class="absolute inset-0 flex items-center justify-center p-4">
|
|
195
|
+
<div class="text-center">
|
|
196
|
+
<p class="text-red-600 dark:text-red-400 text-sm font-medium">Chart Error</p>
|
|
197
|
+
<p class="text-gray-600 dark:text-gray-400 text-xs mt-1">{error()}</p>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</Show>
|
|
201
|
+
|
|
202
|
+
<Show when={iframeUrl() && !error()}>
|
|
203
|
+
<div class="w-full h-full p-4">
|
|
204
|
+
<Show when={params()?.title}>
|
|
205
|
+
<h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-3">
|
|
206
|
+
{params()?.title}
|
|
207
|
+
</h3>
|
|
208
|
+
</Show>
|
|
209
|
+
<div class="w-full h-full" role="img" aria-label={params()?.title ? `Chart: ${params()?.title}` : 'Chart visualization'}>
|
|
210
|
+
<img
|
|
211
|
+
src={iframeUrl()}
|
|
212
|
+
alt={params()?.title ? `Chart: ${params()?.title}` : 'Chart visualization'}
|
|
213
|
+
class="w-full h-auto max-h-[300px] object-contain"
|
|
214
|
+
onError={() => {
|
|
215
|
+
setError('Failed to load chart')
|
|
216
|
+
props.onError?.({
|
|
217
|
+
type: 'render',
|
|
218
|
+
message: 'Chart rendering failed',
|
|
219
|
+
componentId: props.component.id,
|
|
220
|
+
})
|
|
221
|
+
}}
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</Show>
|
|
226
|
+
</div>
|
|
227
|
+
}
|
|
228
|
+
>
|
|
174
229
|
<ChartJSRenderer
|
|
175
230
|
component={props.component}
|
|
176
231
|
onError={(err) => props.onError?.({
|
|
@@ -179,52 +234,7 @@ function ChartRenderer(props: {
|
|
|
179
234
|
componentId: props.component.id,
|
|
180
235
|
})}
|
|
181
236
|
/>
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Iframe fallback (Quickchart.io)
|
|
186
|
-
return (
|
|
187
|
-
<div class="relative w-full h-full min-h-[300px] bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
|
188
|
-
<Show when={isLoading()}>
|
|
189
|
-
<div class="absolute inset-0 flex items-center justify-center">
|
|
190
|
-
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
|
191
|
-
</div>
|
|
192
|
-
</Show>
|
|
193
|
-
|
|
194
|
-
<Show when={error()}>
|
|
195
|
-
<div class="absolute inset-0 flex items-center justify-center p-4">
|
|
196
|
-
<div class="text-center">
|
|
197
|
-
<p class="text-red-600 dark:text-red-400 text-sm font-medium">Chart Error</p>
|
|
198
|
-
<p class="text-gray-600 dark:text-gray-400 text-xs mt-1">{error()}</p>
|
|
199
|
-
</div>
|
|
200
|
-
</div>
|
|
201
|
-
</Show>
|
|
202
|
-
|
|
203
|
-
<Show when={iframeUrl() && !error()}>
|
|
204
|
-
<div class="w-full h-full p-4">
|
|
205
|
-
<Show when={params()?.title}>
|
|
206
|
-
<h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-3">
|
|
207
|
-
{params()?.title}
|
|
208
|
-
</h3>
|
|
209
|
-
</Show>
|
|
210
|
-
<div class="w-full h-full" role="img" aria-label={params()?.title ? `Chart: ${params()?.title}` : 'Chart visualization'}>
|
|
211
|
-
<img
|
|
212
|
-
src={iframeUrl()}
|
|
213
|
-
alt={params()?.title ? `Chart: ${params()?.title}` : 'Chart visualization'}
|
|
214
|
-
class="w-full h-auto max-h-[300px] object-contain"
|
|
215
|
-
onError={() => {
|
|
216
|
-
setError('Failed to load chart')
|
|
217
|
-
props.onError?.({
|
|
218
|
-
type: 'render',
|
|
219
|
-
message: 'Chart rendering failed',
|
|
220
|
-
componentId: props.component.id,
|
|
221
|
-
})
|
|
222
|
-
}}
|
|
223
|
-
/>
|
|
224
|
-
</div>
|
|
225
|
-
</div>
|
|
226
|
-
</Show>
|
|
227
|
-
</div>
|
|
237
|
+
</Show>
|
|
228
238
|
)
|
|
229
239
|
}
|
|
230
240
|
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* without UNKNOWN_COMPONENT_TYPE errors.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { describe, it, expect } from 'vitest'
|
|
9
|
-
import { validateComponent } from './validation'
|
|
8
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
9
|
+
import { validateComponent, validateChartComponent } from './validation'
|
|
10
10
|
import type { UIComponent, ComponentType } from '../types'
|
|
11
11
|
|
|
12
12
|
/** Helper to create a minimal valid UIComponent for testing */
|
|
@@ -131,4 +131,42 @@ describe('validateComponent', () => {
|
|
|
131
131
|
expect(result.valid).toBe(false)
|
|
132
132
|
})
|
|
133
133
|
})
|
|
134
|
+
|
|
135
|
+
describe('H2: missing component.params guard', () => {
|
|
136
|
+
it('rejects component with undefined params', () => {
|
|
137
|
+
const component = { id: 'test', type: 'chart' as any, position: { colStart: 1, colSpan: 12 }, params: undefined as any }
|
|
138
|
+
const result = validateComponent(component)
|
|
139
|
+
expect(result.valid).toBe(false)
|
|
140
|
+
expect(result.errors?.some((e) => e.code === 'MISSING_PARAMS')).toBe(true)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('validateChartComponent — H1 null guards', () => {
|
|
146
|
+
|
|
147
|
+
it('rejects chart with undefined data', () => {
|
|
148
|
+
const result = validateChartComponent({ type: 'bar', data: undefined as any } as any)
|
|
149
|
+
expect(result.valid).toBe(false)
|
|
150
|
+
expect(result.errors?.[0].code).toBe('MISSING_DATA')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('rejects chart with missing datasets', () => {
|
|
154
|
+
const result = validateChartComponent({ type: 'bar', data: { labels: ['A'] } } as any)
|
|
155
|
+
expect(result.valid).toBe(false)
|
|
156
|
+
expect(result.errors?.[0].code).toBe('MISSING_DATASETS')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('rejects chart with missing labels', () => {
|
|
160
|
+
const result = validateChartComponent({ type: 'bar', data: { datasets: [{ label: 'X', data: [1] }] } } as any)
|
|
161
|
+
expect(result.valid).toBe(false)
|
|
162
|
+
expect(result.errors?.[0].code).toBe('MISSING_LABELS')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('validates chart with proper data', () => {
|
|
166
|
+
const result = validateChartComponent({
|
|
167
|
+
type: 'bar',
|
|
168
|
+
data: { labels: ['A', 'B'], datasets: [{ label: 'X', data: [1, 2] }] },
|
|
169
|
+
} as any)
|
|
170
|
+
expect(result.valid).toBe(true)
|
|
171
|
+
})
|
|
134
172
|
})
|
|
@@ -237,6 +237,17 @@ export function validateChartComponent(
|
|
|
237
237
|
): ValidationResult {
|
|
238
238
|
const errors: ValidationResult['errors'] = []
|
|
239
239
|
|
|
240
|
+
// Guard: params.data must exist with labels + datasets
|
|
241
|
+
if (!params?.data) {
|
|
242
|
+
return { valid: false, errors: [{ path: 'params.data', message: 'Missing chart data object', code: 'MISSING_DATA' }] }
|
|
243
|
+
}
|
|
244
|
+
if (!Array.isArray(params.data.datasets)) {
|
|
245
|
+
return { valid: false, errors: [{ path: 'params.data.datasets', message: 'Missing or invalid datasets array', code: 'MISSING_DATASETS' }] }
|
|
246
|
+
}
|
|
247
|
+
if (!Array.isArray(params.data.labels)) {
|
|
248
|
+
return { valid: false, errors: [{ path: 'params.data.labels', message: 'Missing or invalid labels array', code: 'MISSING_LABELS' }] }
|
|
249
|
+
}
|
|
250
|
+
|
|
240
251
|
// Validate data points count
|
|
241
252
|
const totalDataPoints = params.data.datasets.reduce(
|
|
242
253
|
(sum, dataset) => sum + dataset.data.length,
|
|
@@ -449,6 +460,11 @@ export function validateComponent(
|
|
|
449
460
|
const limits = options?.limits ?? DEFAULT_RESOURCE_LIMITS
|
|
450
461
|
const errors: ValidationResult['errors'] = []
|
|
451
462
|
|
|
463
|
+
// Guard: params must exist
|
|
464
|
+
if (!component.params) {
|
|
465
|
+
return { valid: false, errors: [{ path: 'params', message: 'Missing component params', code: 'MISSING_PARAMS' }] }
|
|
466
|
+
}
|
|
467
|
+
|
|
452
468
|
// Validate grid position
|
|
453
469
|
const gridResult = validateGridPosition(component.position)
|
|
454
470
|
if (!gridResult.valid) {
|