@seed-ship/mcp-ui-solid 1.0.15 → 1.0.18
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/mcp-ui-solid/src/components/UIResourceRenderer.cjs +1 -1
- package/dist/mcp-ui-solid/src/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/mcp-ui-solid/src/components/UIResourceRenderer.js +2 -2
- package/dist/mcp-ui-solid/src/components/UIResourceRenderer.js.map +1 -1
- package/dist/mcp-ui-solid/src/services/validation.cjs +30 -0
- package/dist/mcp-ui-solid/src/services/validation.cjs.map +1 -1
- package/dist/mcp-ui-solid/src/services/validation.js +30 -0
- package/dist/mcp-ui-solid/src/services/validation.js.map +1 -1
- package/dist/services/validation.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -137,7 +137,7 @@ function IframeRenderer(props) {
|
|
|
137
137
|
get children() {
|
|
138
138
|
return web.ssr(_tmpl$14, web.ssrHydrationKey(), web.escape(params.title));
|
|
139
139
|
}
|
|
140
|
-
})), web.ssrAttribute("src", web.escape(params.url, true), false) + web.ssrAttribute("title", web.escape(params.title, true) || "Embedded content", false), web.
|
|
140
|
+
})), web.ssrAttribute("src", web.escape(params.url, true), false) + web.ssrAttribute("title", web.escape(params.title, true) || "Embedded content", false), web.ssrStyle(`height: ${params.height || "400px"}; min-height: 300px;`));
|
|
141
141
|
}
|
|
142
142
|
function ImageRenderer(props) {
|
|
143
143
|
const params = props.component.params;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UIResourceRenderer.cjs","sources":["../../../../src/components/UIResourceRenderer.tsx"],"sourcesContent":["/**\n * UI Resource Renderer Component\n * Phase 0: Foundation with iframe sandbox and composite grid support\n *\n * Security features:\n * - Sandboxed iframes for untrusted content\n * - CSP enforcement via middleware\n * - XSS prevention with DOMPurify\n * - Domain whitelist validation\n *\n * Performance:\n * - Lazy loading with Intersection Observer\n * - Render timeout enforcement\n * - Error boundaries for isolation\n */\n\nimport { Component, createSignal, onMount, Show, For, createMemo } from 'solid-js'\nimport type { UIComponent, UILayout, RendererError, ComponentType } from '../types'\nimport { validateComponent, DEFAULT_RESOURCE_LIMITS } from '../services/validation'\nimport { GenerativeUIErrorBoundary } from './GenerativeUIErrorBoundary'\nimport { marked } from 'marked'\n\n/**\n * Props for UIResourceRenderer\n */\nexport interface UIResourceRendererProps {\n /**\n * Single component or full layout to render\n */\n content: UIComponent | UILayout\n\n /**\n * Lazy loading (default: true)\n */\n lazyLoad?: boolean\n\n /**\n * Error callback\n */\n onError?: (error: RendererError) => void\n\n /**\n * Custom CSS class\n */\n class?: string\n}\n\n/**\n * Render a single chart component in a sandboxed iframe\n */\nfunction ChartRenderer(props: {\n component: UIComponent\n onError?: (error: RendererError) => void\n}) {\n const [iframeUrl, setIframeUrl] = createSignal<string>()\n const [isLoading, setIsLoading] = createSignal(true)\n const [error, setError] = createSignal<string>()\n\n onMount(() => {\n const chartParams = props.component.params as any\n\n // Build Quickchart URL\n const chartConfig = {\n type: chartParams.type,\n data: chartParams.data,\n options: {\n ...chartParams.options,\n responsive: true,\n maintainAspectRatio: false,\n },\n }\n\n // Encode chart configuration for Quickchart API\n const configStr = encodeURIComponent(JSON.stringify(chartConfig))\n const url = `https://quickchart.io/chart?c=${configStr}&width=500&height=300&devicePixelRatio=2`\n\n // Validate domain (should always pass for quickchart.io)\n setIframeUrl(url)\n setIsLoading(false)\n })\n\n return (\n <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\">\n <Show when={isLoading()}>\n <div class=\"absolute inset-0 flex items-center justify-center\">\n <div class=\"animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600\" />\n </div>\n </Show>\n\n <Show when={error()}>\n <div class=\"absolute inset-0 flex items-center justify-center p-4\">\n <div class=\"text-center\">\n <p class=\"text-red-600 dark:text-red-400 text-sm font-medium\">Chart Error</p>\n <p class=\"text-gray-600 dark:text-gray-400 text-xs mt-1\">{error()}</p>\n </div>\n </div>\n </Show>\n\n <Show when={iframeUrl() && !error()}>\n <div class=\"w-full h-full p-4\">\n <Show when={(props.component.params as any).title}>\n <h3 class=\"text-sm font-semibold text-gray-900 dark:text-white mb-3\">\n {(props.component.params as any).title}\n </h3>\n </Show>\n <div class=\"w-full h-full\">\n <img\n src={iframeUrl()}\n alt=\"Chart visualization\"\n class=\"w-full h-auto max-h-[300px] object-contain\"\n onError={() => {\n setError('Failed to load chart')\n props.onError?.({\n type: 'render',\n message: 'Chart rendering failed',\n componentId: props.component.id,\n })\n }}\n />\n </div>\n </div>\n </Show>\n </div>\n )\n}\n\n/**\n * Render a table component\n */\nfunction TableRenderer(props: {\n component: UIComponent\n onError?: (error: RendererError) => void\n}) {\n const tableParams = props.component.params as any\n\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden\">\n <div class=\"p-4\">\n <Show when={tableParams.title}>\n <h3 class=\"text-sm font-semibold text-gray-900 dark:text-white mb-3\">\n {tableParams.title}\n </h3>\n </Show>\n\n <div class=\"overflow-x-auto\">\n <table class=\"min-w-full divide-y divide-gray-200 dark:divide-gray-700\">\n <thead class=\"bg-gray-50 dark:bg-gray-900\">\n <tr>\n <For each={tableParams.columns}>\n {(column: any) => (\n <th\n scope=\"col\"\n class=\"px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider\"\n style={column.width ? { width: column.width } : {}}\n >\n {column.label}\n </th>\n )}\n </For>\n </tr>\n </thead>\n <tbody class=\"bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700\">\n <For each={tableParams.rows.slice(0, DEFAULT_RESOURCE_LIMITS.maxTableRows)}>\n {(row: any) => (\n <tr class=\"hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors\">\n <For each={tableParams.columns}>\n {(column: any) => (\n <td class=\"px-4 py-3 text-sm text-gray-900 dark:text-gray-100 whitespace-nowrap\">\n {row[column.key] || '-'}\n </td>\n )}\n </For>\n </tr>\n )}\n </For>\n </tbody>\n </table>\n </div>\n\n <Show when={tableParams.pagination}>\n <div class=\"mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400\">\n <span>\n Showing {tableParams.pagination.currentPage * tableParams.pagination.pageSize + 1} -{' '}\n {Math.min(\n (tableParams.pagination.currentPage + 1) * tableParams.pagination.pageSize,\n tableParams.pagination.totalRows\n )}{' '}\n of {tableParams.pagination.totalRows}\n </span>\n </div>\n </Show>\n </div>\n </div>\n )\n}\n\n/**\n * Render a metric card component\n */\nfunction MetricRenderer(props: { component: UIComponent }) {\n const metricParams = props.component.params as any\n\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4\">\n <div class=\"flex flex-col h-full justify-between\">\n <div>\n <p class=\"text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide\">\n {metricParams.title}\n </p>\n <div class=\"mt-2 flex items-baseline\">\n <p class=\"text-2xl font-semibold text-gray-900 dark:text-white\">{metricParams.value}</p>\n <Show when={metricParams.unit}>\n <span class=\"ml-2 text-sm font-medium text-gray-500 dark:text-gray-400\">\n {metricParams.unit}\n </span>\n </Show>\n </div>\n </div>\n\n <Show when={metricParams.trend}>\n <div class=\"mt-3 flex items-center\">\n <span\n class={`text-sm font-medium ${metricParams.trend.direction === 'up'\n ? 'text-green-600 dark:text-green-400'\n : metricParams.trend.direction === 'down'\n ? 'text-red-600 dark:text-red-400'\n : 'text-gray-600 dark:text-gray-400'\n }`}\n >\n {metricParams.trend.direction === 'up'\n ? '�'\n : metricParams.trend.direction === 'down'\n ? '�'\n : '�'}{' '}\n {Math.abs(metricParams.trend.value)}%\n </span>\n </div>\n </Show>\n\n <Show when={metricParams.subtitle}>\n <p class=\"mt-2 text-xs text-gray-500 dark:text-gray-400\">{metricParams.subtitle}</p>\n </Show>\n </div>\n </div>\n )\n}\n\n/**\n * Render a text component (with optional markdown)\n */\nfunction TextRenderer(props: { component: UIComponent }) {\n const textParams = props.component.params as any\n\n // Convert markdown to HTML if markdown flag is true\n const htmlContent = createMemo(() => {\n if (textParams.markdown) {\n return marked.parse(textParams.content, { async: false }) as string\n }\n return textParams.content\n })\n\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4\">\n <div\n class={`prose prose-sm dark:prose-invert max-w-none ${textParams.className || ''}`}\n innerHTML={htmlContent()}\n />\n </div>\n )\n}\n\n/**\n * Render an iframe component\n */\nfunction IframeRenderer(props: { component: UIComponent }) {\n const params = props.component.params as any\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col\">\n <Show when={params.title}>\n <div class=\"px-4 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900\">\n <h3 class=\"text-sm font-semibold text-gray-900 dark:text-white\">{params.title}</h3>\n </div>\n </Show>\n <iframe\n src={params.url}\n title={params.title || 'Embedded content'}\n class=\"w-full border-0 flex-1\"\n style={{ height: params.height || '400px', 'min-height': '300px' }}\n sandbox=\"allow-scripts allow-same-origin allow-popups allow-forms\"\n loading=\"lazy\"\n />\n </div>\n )\n}\n\n/**\n * Render an image component\n */\nfunction ImageRenderer(props: { component: UIComponent }) {\n const params = props.component.params as any\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col\">\n <div class=\"flex-1 flex items-center justify-center p-4 bg-gray-50 dark:bg-gray-900 min-h-[200px]\">\n <img\n src={params.url}\n alt={params.alt || 'Image'}\n class=\"max-w-full max-h-[500px] object-contain rounded shadow-sm\"\n loading=\"lazy\"\n />\n </div>\n <Show when={params.caption}>\n <div class=\"p-3 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800\">\n <p class=\"text-sm text-gray-600 dark:text-gray-400 text-center\">{params.caption}</p>\n </div>\n </Show>\n </div>\n )\n}\n\n/**\n * Render a link component\n */\nfunction LinkRenderer(props: { component: UIComponent }) {\n const params = props.component.params as any\n return (\n <a\n href={params.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors group h-full\"\n >\n <div class=\"p-2 bg-blue-50 dark:bg-blue-900/30 rounded-full text-blue-600 dark:text-blue-400 group-hover:bg-blue-100 dark:group-hover:bg-blue-900/50 shrink-0\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"w-5 h-5\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\" />\n <path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\" />\n </svg>\n </div>\n <div class=\"flex-1 min-w-0\">\n <h4 class=\"text-sm font-medium text-gray-900 dark:text-white truncate\">\n {params.label || params.url}\n </h4>\n <Show when={params.description}>\n <p class=\"text-xs text-gray-500 dark:text-gray-400 truncate\">{params.description}</p>\n </Show>\n </div>\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"w-4 h-4 text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300 shrink-0\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n <polyline points=\"15 3 21 3 21 9\" />\n <line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\" />\n </svg>\n </a>\n )\n}\n\n/**\n * Render a single component with error boundary\n */\nfunction ComponentRenderer(props: {\n component: UIComponent\n onError?: (error: RendererError) => void\n}) {\n // Validate component before rendering\n const validation = validateComponent(props.component)\n if (!validation.valid) {\n props.onError?.({\n type: 'validation',\n message: 'Component validation failed',\n componentId: props.component.id,\n details: validation.errors,\n })\n\n return (\n <div class=\"w-full h-full bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4\">\n <p class=\"text-sm font-medium text-red-900 dark:text-red-100\">Validation Error</p>\n <p class=\"text-xs text-red-700 dark:text-red-300 mt-1\">\n {validation.errors?.[0]?.message || 'Unknown validation error'}\n </p>\n </div>\n )\n }\n\n // Render based on component type with enhanced error boundary\n return (\n <GenerativeUIErrorBoundary\n componentId={props.component.id}\n componentType={props.component.type}\n onError={props.onError}\n allowRetry={true}\n >\n <Show when={props.component.type === 'chart'}>\n <ChartRenderer component={props.component} onError={props.onError} />\n </Show>\n <Show when={props.component.type === 'table'}>\n <TableRenderer component={props.component} onError={props.onError} />\n </Show>\n <Show when={props.component.type === 'metric'}>\n <MetricRenderer component={props.component} />\n </Show>\n <Show when={props.component.type === 'text'}>\n <TextRenderer component={props.component} />\n </Show>\n <Show when={props.component.type === 'iframe'}>\n <IframeRenderer component={props.component} />\n </Show>\n <Show when={props.component.type === 'image'}>\n <ImageRenderer component={props.component} />\n </Show>\n <Show when={props.component.type === 'link'}>\n <LinkRenderer component={props.component} />\n </Show>\n </GenerativeUIErrorBoundary>\n )\n}\n\n/**\n * Main UIResourceRenderer component\n */\nexport const UIResourceRenderer: Component<UIResourceRendererProps> = (props) => {\n const layout = () => {\n // ✅ PHASE 3.3 FIX: Check if content is a UIComponent (non-composite) vs UILayout (composite)\n // UILayout has type='composite', UIComponent has type='chart'|'table'|'metric'|'text'\n if ('type' in props.content && (props.content as any).type !== 'composite') {\n return {\n id: 'single-component',\n components: [props.content as UIComponent],\n grid: {\n columns: 12,\n gap: '1rem',\n },\n } as UILayout\n }\n return props.content as UILayout\n }\n\n // Grid position to CSS Grid styles\n const getGridStyles = (component: UIComponent) => {\n // ✅ PHASE 3 FIX: Defensive check for position field\n if (!component.position) {\n console.error('[UIResourceRenderer] Component missing position field:', component)\n return {\n 'grid-column': '1 / span 12',\n 'grid-row': 'auto',\n }\n }\n\n const { colStart, colSpan, rowStart, rowSpan = 1 } = component.position\n\n return {\n 'grid-column': `${colStart} / span ${colSpan}`,\n 'grid-row': rowStart ? `${rowStart} / span ${rowSpan}` : 'auto',\n }\n }\n\n // Convert grid styles to CSS string to avoid setStyleProperty\n const gridContainerStyle = () =>\n `grid-template-columns: repeat(${layout().grid.columns}, 1fr); gap: ${layout().grid.gap}`\n\n // Convert component grid styles to CSS string\n const getGridStyleString = (component: UIComponent) => {\n // ✅ PHASE 3 FIX: Defensive check for position field\n if (!component.position) {\n console.error('[UIResourceRenderer] Component missing position field:', component)\n return 'grid-column: 1 / span 12; grid-row: auto' // Default to full width\n }\n\n const { colStart, colSpan, rowStart, rowSpan = 1 } = component.position\n return `grid-column: ${colStart} / span ${colSpan}; grid-row: ${rowStart ? `${rowStart} / span ${rowSpan}` : 'auto'}`\n }\n\n return (\n <div class={`w-full ${props.class || ''}`}>\n <div class=\"grid gap-4\" style={gridContainerStyle()}>\n <For each={layout().components}>\n {(component) => (\n <div style={getGridStyleString(component)}>\n <ComponentRenderer component={component} onError={props.onError} />\n </div>\n )}\n </For>\n </div>\n </div>\n )\n}\n"],"names":["ChartRenderer","props","iframeUrl","setIframeUrl","createSignal","isLoading","setIsLoading","error","setError","onMount","chartParams","component","params","chartConfig","type","data","options","responsive","maintainAspectRatio","configStr","encodeURIComponent","JSON","stringify","url","_$ssr","_tmpl$5","_$ssrHydrationKey","_$escape","_$createComponent","Show","when","children","_tmpl$","_tmpl$2","_tmpl$4","title","_tmpl$3","_$ssrAttribute","TableRenderer","tableParams","_tmpl$7","For","each","columns","column","_tmpl$8","_$ssrStyle","width","label","rows","slice","DEFAULT_RESOURCE_LIMITS","maxTableRows","row","_tmpl$9","_tmpl$0","key","pagination","_tmpl$6","currentPage","pageSize","Math","min","totalRows","MetricRenderer","metricParams","_tmpl$12","value","unit","_tmpl$1","trend","_tmpl$10","direction","abs","subtitle","_tmpl$11","TextRenderer","textParams","htmlContent","createMemo","markdown","marked","parse","content","async","_tmpl$13","className","IframeRenderer","_tmpl$15","_tmpl$14","_$ssrStyleProperty","height","ImageRenderer","_tmpl$17","alt","caption","_tmpl$16","LinkRenderer","_tmpl$19","description","_tmpl$18","ComponentRenderer","validation","validateComponent","valid","onError","message","componentId","id","details","errors","_tmpl$20","GenerativeUIErrorBoundary","componentType","allowRetry","UIResourceRenderer","layout","components","grid","gap","gridContainerStyle","getGridStyleString","position","console","colStart","colSpan","rowStart","rowSpan","_tmpl$21","class","_tmpl$22"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"UIResourceRenderer.cjs","sources":["../../../../src/components/UIResourceRenderer.tsx"],"sourcesContent":["/**\n * UI Resource Renderer Component\n * Phase 0: Foundation with iframe sandbox and composite grid support\n *\n * Security features:\n * - Sandboxed iframes for untrusted content\n * - CSP enforcement via middleware\n * - XSS prevention with DOMPurify\n * - Domain whitelist validation\n *\n * Performance:\n * - Lazy loading with Intersection Observer\n * - Render timeout enforcement\n * - Error boundaries for isolation\n */\n\nimport { Component, createSignal, onMount, Show, For, createMemo } from 'solid-js'\nimport type { UIComponent, UILayout, RendererError, ComponentType } from '../types'\nimport { validateComponent, DEFAULT_RESOURCE_LIMITS } from '../services/validation'\nimport { GenerativeUIErrorBoundary } from './GenerativeUIErrorBoundary'\nimport { marked } from 'marked'\n\n/**\n * Props for UIResourceRenderer\n */\nexport interface UIResourceRendererProps {\n /**\n * Single component or full layout to render\n */\n content: UIComponent | UILayout\n\n /**\n * Lazy loading (default: true)\n */\n lazyLoad?: boolean\n\n /**\n * Error callback\n */\n onError?: (error: RendererError) => void\n\n /**\n * Custom CSS class\n */\n class?: string\n}\n\n/**\n * Render a single chart component in a sandboxed iframe\n */\nfunction ChartRenderer(props: {\n component: UIComponent\n onError?: (error: RendererError) => void\n}) {\n const [iframeUrl, setIframeUrl] = createSignal<string>()\n const [isLoading, setIsLoading] = createSignal(true)\n const [error, setError] = createSignal<string>()\n\n onMount(() => {\n const chartParams = props.component.params as any\n\n // Build Quickchart URL\n const chartConfig = {\n type: chartParams.type,\n data: chartParams.data,\n options: {\n ...chartParams.options,\n responsive: true,\n maintainAspectRatio: false,\n },\n }\n\n // Encode chart configuration for Quickchart API\n const configStr = encodeURIComponent(JSON.stringify(chartConfig))\n const url = `https://quickchart.io/chart?c=${configStr}&width=500&height=300&devicePixelRatio=2`\n\n // Validate domain (should always pass for quickchart.io)\n setIframeUrl(url)\n setIsLoading(false)\n })\n\n return (\n <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\">\n <Show when={isLoading()}>\n <div class=\"absolute inset-0 flex items-center justify-center\">\n <div class=\"animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600\" />\n </div>\n </Show>\n\n <Show when={error()}>\n <div class=\"absolute inset-0 flex items-center justify-center p-4\">\n <div class=\"text-center\">\n <p class=\"text-red-600 dark:text-red-400 text-sm font-medium\">Chart Error</p>\n <p class=\"text-gray-600 dark:text-gray-400 text-xs mt-1\">{error()}</p>\n </div>\n </div>\n </Show>\n\n <Show when={iframeUrl() && !error()}>\n <div class=\"w-full h-full p-4\">\n <Show when={(props.component.params as any).title}>\n <h3 class=\"text-sm font-semibold text-gray-900 dark:text-white mb-3\">\n {(props.component.params as any).title}\n </h3>\n </Show>\n <div class=\"w-full h-full\">\n <img\n src={iframeUrl()}\n alt=\"Chart visualization\"\n class=\"w-full h-auto max-h-[300px] object-contain\"\n onError={() => {\n setError('Failed to load chart')\n props.onError?.({\n type: 'render',\n message: 'Chart rendering failed',\n componentId: props.component.id,\n })\n }}\n />\n </div>\n </div>\n </Show>\n </div>\n )\n}\n\n/**\n * Render a table component\n */\nfunction TableRenderer(props: {\n component: UIComponent\n onError?: (error: RendererError) => void\n}) {\n const tableParams = props.component.params as any\n\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden\">\n <div class=\"p-4\">\n <Show when={tableParams.title}>\n <h3 class=\"text-sm font-semibold text-gray-900 dark:text-white mb-3\">\n {tableParams.title}\n </h3>\n </Show>\n\n <div class=\"overflow-x-auto\">\n <table class=\"min-w-full divide-y divide-gray-200 dark:divide-gray-700\">\n <thead class=\"bg-gray-50 dark:bg-gray-900\">\n <tr>\n <For each={tableParams.columns}>\n {(column: any) => (\n <th\n scope=\"col\"\n class=\"px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider\"\n style={column.width ? { width: column.width } : {}}\n >\n {column.label}\n </th>\n )}\n </For>\n </tr>\n </thead>\n <tbody class=\"bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700\">\n <For each={tableParams.rows.slice(0, DEFAULT_RESOURCE_LIMITS.maxTableRows)}>\n {(row: any) => (\n <tr class=\"hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors\">\n <For each={tableParams.columns}>\n {(column: any) => (\n <td class=\"px-4 py-3 text-sm text-gray-900 dark:text-gray-100 whitespace-nowrap\">\n {row[column.key] || '-'}\n </td>\n )}\n </For>\n </tr>\n )}\n </For>\n </tbody>\n </table>\n </div>\n\n <Show when={tableParams.pagination}>\n <div class=\"mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400\">\n <span>\n Showing {tableParams.pagination.currentPage * tableParams.pagination.pageSize + 1} -{' '}\n {Math.min(\n (tableParams.pagination.currentPage + 1) * tableParams.pagination.pageSize,\n tableParams.pagination.totalRows\n )}{' '}\n of {tableParams.pagination.totalRows}\n </span>\n </div>\n </Show>\n </div>\n </div>\n )\n}\n\n/**\n * Render a metric card component\n */\nfunction MetricRenderer(props: { component: UIComponent }) {\n const metricParams = props.component.params as any\n\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4\">\n <div class=\"flex flex-col h-full justify-between\">\n <div>\n <p class=\"text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide\">\n {metricParams.title}\n </p>\n <div class=\"mt-2 flex items-baseline\">\n <p class=\"text-2xl font-semibold text-gray-900 dark:text-white\">{metricParams.value}</p>\n <Show when={metricParams.unit}>\n <span class=\"ml-2 text-sm font-medium text-gray-500 dark:text-gray-400\">\n {metricParams.unit}\n </span>\n </Show>\n </div>\n </div>\n\n <Show when={metricParams.trend}>\n <div class=\"mt-3 flex items-center\">\n <span\n class={`text-sm font-medium ${metricParams.trend.direction === 'up'\n ? 'text-green-600 dark:text-green-400'\n : metricParams.trend.direction === 'down'\n ? 'text-red-600 dark:text-red-400'\n : 'text-gray-600 dark:text-gray-400'\n }`}\n >\n {metricParams.trend.direction === 'up'\n ? '�'\n : metricParams.trend.direction === 'down'\n ? '�'\n : '�'}{' '}\n {Math.abs(metricParams.trend.value)}%\n </span>\n </div>\n </Show>\n\n <Show when={metricParams.subtitle}>\n <p class=\"mt-2 text-xs text-gray-500 dark:text-gray-400\">{metricParams.subtitle}</p>\n </Show>\n </div>\n </div>\n )\n}\n\n/**\n * Render a text component (with optional markdown)\n */\nfunction TextRenderer(props: { component: UIComponent }) {\n const textParams = props.component.params as any\n\n // Convert markdown to HTML if markdown flag is true\n const htmlContent = createMemo(() => {\n if (textParams.markdown) {\n return marked.parse(textParams.content, { async: false }) as string\n }\n return textParams.content\n })\n\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4\">\n <div\n class={`prose prose-sm dark:prose-invert max-w-none ${textParams.className || ''}`}\n innerHTML={htmlContent()}\n />\n </div>\n )\n}\n\n/**\n * Render an iframe component\n */\nfunction IframeRenderer(props: { component: UIComponent }) {\n const params = props.component.params as any\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col\">\n <Show when={params.title}>\n <div class=\"px-4 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900\">\n <h3 class=\"text-sm font-semibold text-gray-900 dark:text-white\">{params.title}</h3>\n </div>\n </Show>\n <iframe\n src={params.url}\n title={params.title || 'Embedded content'}\n class=\"w-full border-0 flex-1\"\n style={`height: ${params.height || '400px'}; min-height: 300px;`}\n sandbox=\"allow-scripts allow-same-origin allow-popups allow-forms\"\n loading=\"lazy\"\n />\n </div>\n )\n}\n\n/**\n * Render an image component\n */\nfunction ImageRenderer(props: { component: UIComponent }) {\n const params = props.component.params as any\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col\">\n <div class=\"flex-1 flex items-center justify-center p-4 bg-gray-50 dark:bg-gray-900 min-h-[200px]\">\n <img\n src={params.url}\n alt={params.alt || 'Image'}\n class=\"max-w-full max-h-[500px] object-contain rounded shadow-sm\"\n loading=\"lazy\"\n />\n </div>\n <Show when={params.caption}>\n <div class=\"p-3 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800\">\n <p class=\"text-sm text-gray-600 dark:text-gray-400 text-center\">{params.caption}</p>\n </div>\n </Show>\n </div>\n )\n}\n\n/**\n * Render a link component\n */\nfunction LinkRenderer(props: { component: UIComponent }) {\n const params = props.component.params as any\n return (\n <a\n href={params.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors group h-full\"\n >\n <div class=\"p-2 bg-blue-50 dark:bg-blue-900/30 rounded-full text-blue-600 dark:text-blue-400 group-hover:bg-blue-100 dark:group-hover:bg-blue-900/50 shrink-0\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"w-5 h-5\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\" />\n <path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\" />\n </svg>\n </div>\n <div class=\"flex-1 min-w-0\">\n <h4 class=\"text-sm font-medium text-gray-900 dark:text-white truncate\">\n {params.label || params.url}\n </h4>\n <Show when={params.description}>\n <p class=\"text-xs text-gray-500 dark:text-gray-400 truncate\">{params.description}</p>\n </Show>\n </div>\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"w-4 h-4 text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300 shrink-0\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n <polyline points=\"15 3 21 3 21 9\" />\n <line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\" />\n </svg>\n </a>\n )\n}\n\n/**\n * Render a single component with error boundary\n */\nfunction ComponentRenderer(props: {\n component: UIComponent\n onError?: (error: RendererError) => void\n}) {\n // Validate component before rendering\n const validation = validateComponent(props.component)\n if (!validation.valid) {\n props.onError?.({\n type: 'validation',\n message: 'Component validation failed',\n componentId: props.component.id,\n details: validation.errors,\n })\n\n return (\n <div class=\"w-full h-full bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4\">\n <p class=\"text-sm font-medium text-red-900 dark:text-red-100\">Validation Error</p>\n <p class=\"text-xs text-red-700 dark:text-red-300 mt-1\">\n {validation.errors?.[0]?.message || 'Unknown validation error'}\n </p>\n </div>\n )\n }\n\n // Render based on component type with enhanced error boundary\n return (\n <GenerativeUIErrorBoundary\n componentId={props.component.id}\n componentType={props.component.type}\n onError={props.onError}\n allowRetry={true}\n >\n <Show when={props.component.type === 'chart'}>\n <ChartRenderer component={props.component} onError={props.onError} />\n </Show>\n <Show when={props.component.type === 'table'}>\n <TableRenderer component={props.component} onError={props.onError} />\n </Show>\n <Show when={props.component.type === 'metric'}>\n <MetricRenderer component={props.component} />\n </Show>\n <Show when={props.component.type === 'text'}>\n <TextRenderer component={props.component} />\n </Show>\n <Show when={props.component.type === 'iframe'}>\n <IframeRenderer component={props.component} />\n </Show>\n <Show when={props.component.type === 'image'}>\n <ImageRenderer component={props.component} />\n </Show>\n <Show when={props.component.type === 'link'}>\n <LinkRenderer component={props.component} />\n </Show>\n </GenerativeUIErrorBoundary>\n )\n}\n\n/**\n * Main UIResourceRenderer component\n */\nexport const UIResourceRenderer: Component<UIResourceRendererProps> = (props) => {\n const layout = () => {\n // ✅ PHASE 3.3 FIX: Check if content is a UIComponent (non-composite) vs UILayout (composite)\n // UILayout has type='composite', UIComponent has type='chart'|'table'|'metric'|'text'\n if ('type' in props.content && (props.content as any).type !== 'composite') {\n return {\n id: 'single-component',\n components: [props.content as UIComponent],\n grid: {\n columns: 12,\n gap: '1rem',\n },\n } as UILayout\n }\n return props.content as UILayout\n }\n\n // Grid position to CSS Grid styles\n const getGridStyles = (component: UIComponent) => {\n // ✅ PHASE 3 FIX: Defensive check for position field\n if (!component.position) {\n console.error('[UIResourceRenderer] Component missing position field:', component)\n return {\n 'grid-column': '1 / span 12',\n 'grid-row': 'auto',\n }\n }\n\n const { colStart, colSpan, rowStart, rowSpan = 1 } = component.position\n\n return {\n 'grid-column': `${colStart} / span ${colSpan}`,\n 'grid-row': rowStart ? `${rowStart} / span ${rowSpan}` : 'auto',\n }\n }\n\n // Convert grid styles to CSS string to avoid setStyleProperty\n const gridContainerStyle = () =>\n `grid-template-columns: repeat(${layout().grid.columns}, 1fr); gap: ${layout().grid.gap}`\n\n // Convert component grid styles to CSS string\n const getGridStyleString = (component: UIComponent) => {\n // ✅ PHASE 3 FIX: Defensive check for position field\n if (!component.position) {\n console.error('[UIResourceRenderer] Component missing position field:', component)\n return 'grid-column: 1 / span 12; grid-row: auto' // Default to full width\n }\n\n const { colStart, colSpan, rowStart, rowSpan = 1 } = component.position\n return `grid-column: ${colStart} / span ${colSpan}; grid-row: ${rowStart ? `${rowStart} / span ${rowSpan}` : 'auto'}`\n }\n\n return (\n <div class={`w-full ${props.class || ''}`}>\n <div class=\"grid gap-4\" style={gridContainerStyle()}>\n <For each={layout().components}>\n {(component) => (\n <div style={getGridStyleString(component)}>\n <ComponentRenderer component={component} onError={props.onError} />\n </div>\n )}\n </For>\n </div>\n </div>\n )\n}\n"],"names":["ChartRenderer","props","iframeUrl","setIframeUrl","createSignal","isLoading","setIsLoading","error","setError","onMount","chartParams","component","params","chartConfig","type","data","options","responsive","maintainAspectRatio","configStr","encodeURIComponent","JSON","stringify","url","_$ssr","_tmpl$5","_$ssrHydrationKey","_$escape","_$createComponent","Show","when","children","_tmpl$","_tmpl$2","_tmpl$4","title","_tmpl$3","_$ssrAttribute","TableRenderer","tableParams","_tmpl$7","For","each","columns","column","_tmpl$8","_$ssrStyle","width","label","rows","slice","DEFAULT_RESOURCE_LIMITS","maxTableRows","row","_tmpl$9","_tmpl$0","key","pagination","_tmpl$6","currentPage","pageSize","Math","min","totalRows","MetricRenderer","metricParams","_tmpl$12","value","unit","_tmpl$1","trend","_tmpl$10","direction","abs","subtitle","_tmpl$11","TextRenderer","textParams","htmlContent","createMemo","markdown","marked","parse","content","async","_tmpl$13","className","IframeRenderer","_tmpl$15","_tmpl$14","height","ImageRenderer","_tmpl$17","alt","caption","_tmpl$16","LinkRenderer","_tmpl$19","description","_tmpl$18","ComponentRenderer","validation","validateComponent","valid","onError","message","componentId","id","details","errors","_tmpl$20","GenerativeUIErrorBoundary","componentType","allowRetry","UIResourceRenderer","layout","components","grid","gap","gridContainerStyle","getGridStyleString","position","console","colStart","colSpan","rowStart","rowSpan","_tmpl$21","class","_tmpl$22"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ssr, ssrHydrationKey, escape, ssrStyle, createComponent, ssrAttribute
|
|
1
|
+
import { ssr, ssrHydrationKey, escape, ssrStyle, createComponent, ssrAttribute } from "solid-js/web";
|
|
2
2
|
import { For, Show, createSignal, onMount, createMemo } from "solid-js";
|
|
3
3
|
import { validateComponent, DEFAULT_RESOURCE_LIMITS } from "../services/validation.js";
|
|
4
4
|
import { GenerativeUIErrorBoundary } from "./GenerativeUIErrorBoundary.js";
|
|
@@ -135,7 +135,7 @@ function IframeRenderer(props) {
|
|
|
135
135
|
get children() {
|
|
136
136
|
return ssr(_tmpl$14, ssrHydrationKey(), escape(params.title));
|
|
137
137
|
}
|
|
138
|
-
})), ssrAttribute("src", escape(params.url, true), false) + ssrAttribute("title", escape(params.title, true) || "Embedded content", false),
|
|
138
|
+
})), ssrAttribute("src", escape(params.url, true), false) + ssrAttribute("title", escape(params.title, true) || "Embedded content", false), ssrStyle(`height: ${params.height || "400px"}; min-height: 300px;`));
|
|
139
139
|
}
|
|
140
140
|
function ImageRenderer(props) {
|
|
141
141
|
const params = props.component.params;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UIResourceRenderer.js","sources":["../../../../src/components/UIResourceRenderer.tsx"],"sourcesContent":["/**\n * UI Resource Renderer Component\n * Phase 0: Foundation with iframe sandbox and composite grid support\n *\n * Security features:\n * - Sandboxed iframes for untrusted content\n * - CSP enforcement via middleware\n * - XSS prevention with DOMPurify\n * - Domain whitelist validation\n *\n * Performance:\n * - Lazy loading with Intersection Observer\n * - Render timeout enforcement\n * - Error boundaries for isolation\n */\n\nimport { Component, createSignal, onMount, Show, For, createMemo } from 'solid-js'\nimport type { UIComponent, UILayout, RendererError, ComponentType } from '../types'\nimport { validateComponent, DEFAULT_RESOURCE_LIMITS } from '../services/validation'\nimport { GenerativeUIErrorBoundary } from './GenerativeUIErrorBoundary'\nimport { marked } from 'marked'\n\n/**\n * Props for UIResourceRenderer\n */\nexport interface UIResourceRendererProps {\n /**\n * Single component or full layout to render\n */\n content: UIComponent | UILayout\n\n /**\n * Lazy loading (default: true)\n */\n lazyLoad?: boolean\n\n /**\n * Error callback\n */\n onError?: (error: RendererError) => void\n\n /**\n * Custom CSS class\n */\n class?: string\n}\n\n/**\n * Render a single chart component in a sandboxed iframe\n */\nfunction ChartRenderer(props: {\n component: UIComponent\n onError?: (error: RendererError) => void\n}) {\n const [iframeUrl, setIframeUrl] = createSignal<string>()\n const [isLoading, setIsLoading] = createSignal(true)\n const [error, setError] = createSignal<string>()\n\n onMount(() => {\n const chartParams = props.component.params as any\n\n // Build Quickchart URL\n const chartConfig = {\n type: chartParams.type,\n data: chartParams.data,\n options: {\n ...chartParams.options,\n responsive: true,\n maintainAspectRatio: false,\n },\n }\n\n // Encode chart configuration for Quickchart API\n const configStr = encodeURIComponent(JSON.stringify(chartConfig))\n const url = `https://quickchart.io/chart?c=${configStr}&width=500&height=300&devicePixelRatio=2`\n\n // Validate domain (should always pass for quickchart.io)\n setIframeUrl(url)\n setIsLoading(false)\n })\n\n return (\n <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\">\n <Show when={isLoading()}>\n <div class=\"absolute inset-0 flex items-center justify-center\">\n <div class=\"animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600\" />\n </div>\n </Show>\n\n <Show when={error()}>\n <div class=\"absolute inset-0 flex items-center justify-center p-4\">\n <div class=\"text-center\">\n <p class=\"text-red-600 dark:text-red-400 text-sm font-medium\">Chart Error</p>\n <p class=\"text-gray-600 dark:text-gray-400 text-xs mt-1\">{error()}</p>\n </div>\n </div>\n </Show>\n\n <Show when={iframeUrl() && !error()}>\n <div class=\"w-full h-full p-4\">\n <Show when={(props.component.params as any).title}>\n <h3 class=\"text-sm font-semibold text-gray-900 dark:text-white mb-3\">\n {(props.component.params as any).title}\n </h3>\n </Show>\n <div class=\"w-full h-full\">\n <img\n src={iframeUrl()}\n alt=\"Chart visualization\"\n class=\"w-full h-auto max-h-[300px] object-contain\"\n onError={() => {\n setError('Failed to load chart')\n props.onError?.({\n type: 'render',\n message: 'Chart rendering failed',\n componentId: props.component.id,\n })\n }}\n />\n </div>\n </div>\n </Show>\n </div>\n )\n}\n\n/**\n * Render a table component\n */\nfunction TableRenderer(props: {\n component: UIComponent\n onError?: (error: RendererError) => void\n}) {\n const tableParams = props.component.params as any\n\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden\">\n <div class=\"p-4\">\n <Show when={tableParams.title}>\n <h3 class=\"text-sm font-semibold text-gray-900 dark:text-white mb-3\">\n {tableParams.title}\n </h3>\n </Show>\n\n <div class=\"overflow-x-auto\">\n <table class=\"min-w-full divide-y divide-gray-200 dark:divide-gray-700\">\n <thead class=\"bg-gray-50 dark:bg-gray-900\">\n <tr>\n <For each={tableParams.columns}>\n {(column: any) => (\n <th\n scope=\"col\"\n class=\"px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider\"\n style={column.width ? { width: column.width } : {}}\n >\n {column.label}\n </th>\n )}\n </For>\n </tr>\n </thead>\n <tbody class=\"bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700\">\n <For each={tableParams.rows.slice(0, DEFAULT_RESOURCE_LIMITS.maxTableRows)}>\n {(row: any) => (\n <tr class=\"hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors\">\n <For each={tableParams.columns}>\n {(column: any) => (\n <td class=\"px-4 py-3 text-sm text-gray-900 dark:text-gray-100 whitespace-nowrap\">\n {row[column.key] || '-'}\n </td>\n )}\n </For>\n </tr>\n )}\n </For>\n </tbody>\n </table>\n </div>\n\n <Show when={tableParams.pagination}>\n <div class=\"mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400\">\n <span>\n Showing {tableParams.pagination.currentPage * tableParams.pagination.pageSize + 1} -{' '}\n {Math.min(\n (tableParams.pagination.currentPage + 1) * tableParams.pagination.pageSize,\n tableParams.pagination.totalRows\n )}{' '}\n of {tableParams.pagination.totalRows}\n </span>\n </div>\n </Show>\n </div>\n </div>\n )\n}\n\n/**\n * Render a metric card component\n */\nfunction MetricRenderer(props: { component: UIComponent }) {\n const metricParams = props.component.params as any\n\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4\">\n <div class=\"flex flex-col h-full justify-between\">\n <div>\n <p class=\"text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide\">\n {metricParams.title}\n </p>\n <div class=\"mt-2 flex items-baseline\">\n <p class=\"text-2xl font-semibold text-gray-900 dark:text-white\">{metricParams.value}</p>\n <Show when={metricParams.unit}>\n <span class=\"ml-2 text-sm font-medium text-gray-500 dark:text-gray-400\">\n {metricParams.unit}\n </span>\n </Show>\n </div>\n </div>\n\n <Show when={metricParams.trend}>\n <div class=\"mt-3 flex items-center\">\n <span\n class={`text-sm font-medium ${metricParams.trend.direction === 'up'\n ? 'text-green-600 dark:text-green-400'\n : metricParams.trend.direction === 'down'\n ? 'text-red-600 dark:text-red-400'\n : 'text-gray-600 dark:text-gray-400'\n }`}\n >\n {metricParams.trend.direction === 'up'\n ? '�'\n : metricParams.trend.direction === 'down'\n ? '�'\n : '�'}{' '}\n {Math.abs(metricParams.trend.value)}%\n </span>\n </div>\n </Show>\n\n <Show when={metricParams.subtitle}>\n <p class=\"mt-2 text-xs text-gray-500 dark:text-gray-400\">{metricParams.subtitle}</p>\n </Show>\n </div>\n </div>\n )\n}\n\n/**\n * Render a text component (with optional markdown)\n */\nfunction TextRenderer(props: { component: UIComponent }) {\n const textParams = props.component.params as any\n\n // Convert markdown to HTML if markdown flag is true\n const htmlContent = createMemo(() => {\n if (textParams.markdown) {\n return marked.parse(textParams.content, { async: false }) as string\n }\n return textParams.content\n })\n\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4\">\n <div\n class={`prose prose-sm dark:prose-invert max-w-none ${textParams.className || ''}`}\n innerHTML={htmlContent()}\n />\n </div>\n )\n}\n\n/**\n * Render an iframe component\n */\nfunction IframeRenderer(props: { component: UIComponent }) {\n const params = props.component.params as any\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col\">\n <Show when={params.title}>\n <div class=\"px-4 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900\">\n <h3 class=\"text-sm font-semibold text-gray-900 dark:text-white\">{params.title}</h3>\n </div>\n </Show>\n <iframe\n src={params.url}\n title={params.title || 'Embedded content'}\n class=\"w-full border-0 flex-1\"\n style={{ height: params.height || '400px', 'min-height': '300px' }}\n sandbox=\"allow-scripts allow-same-origin allow-popups allow-forms\"\n loading=\"lazy\"\n />\n </div>\n )\n}\n\n/**\n * Render an image component\n */\nfunction ImageRenderer(props: { component: UIComponent }) {\n const params = props.component.params as any\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col\">\n <div class=\"flex-1 flex items-center justify-center p-4 bg-gray-50 dark:bg-gray-900 min-h-[200px]\">\n <img\n src={params.url}\n alt={params.alt || 'Image'}\n class=\"max-w-full max-h-[500px] object-contain rounded shadow-sm\"\n loading=\"lazy\"\n />\n </div>\n <Show when={params.caption}>\n <div class=\"p-3 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800\">\n <p class=\"text-sm text-gray-600 dark:text-gray-400 text-center\">{params.caption}</p>\n </div>\n </Show>\n </div>\n )\n}\n\n/**\n * Render a link component\n */\nfunction LinkRenderer(props: { component: UIComponent }) {\n const params = props.component.params as any\n return (\n <a\n href={params.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors group h-full\"\n >\n <div class=\"p-2 bg-blue-50 dark:bg-blue-900/30 rounded-full text-blue-600 dark:text-blue-400 group-hover:bg-blue-100 dark:group-hover:bg-blue-900/50 shrink-0\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"w-5 h-5\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\" />\n <path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\" />\n </svg>\n </div>\n <div class=\"flex-1 min-w-0\">\n <h4 class=\"text-sm font-medium text-gray-900 dark:text-white truncate\">\n {params.label || params.url}\n </h4>\n <Show when={params.description}>\n <p class=\"text-xs text-gray-500 dark:text-gray-400 truncate\">{params.description}</p>\n </Show>\n </div>\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"w-4 h-4 text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300 shrink-0\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n <polyline points=\"15 3 21 3 21 9\" />\n <line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\" />\n </svg>\n </a>\n )\n}\n\n/**\n * Render a single component with error boundary\n */\nfunction ComponentRenderer(props: {\n component: UIComponent\n onError?: (error: RendererError) => void\n}) {\n // Validate component before rendering\n const validation = validateComponent(props.component)\n if (!validation.valid) {\n props.onError?.({\n type: 'validation',\n message: 'Component validation failed',\n componentId: props.component.id,\n details: validation.errors,\n })\n\n return (\n <div class=\"w-full h-full bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4\">\n <p class=\"text-sm font-medium text-red-900 dark:text-red-100\">Validation Error</p>\n <p class=\"text-xs text-red-700 dark:text-red-300 mt-1\">\n {validation.errors?.[0]?.message || 'Unknown validation error'}\n </p>\n </div>\n )\n }\n\n // Render based on component type with enhanced error boundary\n return (\n <GenerativeUIErrorBoundary\n componentId={props.component.id}\n componentType={props.component.type}\n onError={props.onError}\n allowRetry={true}\n >\n <Show when={props.component.type === 'chart'}>\n <ChartRenderer component={props.component} onError={props.onError} />\n </Show>\n <Show when={props.component.type === 'table'}>\n <TableRenderer component={props.component} onError={props.onError} />\n </Show>\n <Show when={props.component.type === 'metric'}>\n <MetricRenderer component={props.component} />\n </Show>\n <Show when={props.component.type === 'text'}>\n <TextRenderer component={props.component} />\n </Show>\n <Show when={props.component.type === 'iframe'}>\n <IframeRenderer component={props.component} />\n </Show>\n <Show when={props.component.type === 'image'}>\n <ImageRenderer component={props.component} />\n </Show>\n <Show when={props.component.type === 'link'}>\n <LinkRenderer component={props.component} />\n </Show>\n </GenerativeUIErrorBoundary>\n )\n}\n\n/**\n * Main UIResourceRenderer component\n */\nexport const UIResourceRenderer: Component<UIResourceRendererProps> = (props) => {\n const layout = () => {\n // ✅ PHASE 3.3 FIX: Check if content is a UIComponent (non-composite) vs UILayout (composite)\n // UILayout has type='composite', UIComponent has type='chart'|'table'|'metric'|'text'\n if ('type' in props.content && (props.content as any).type !== 'composite') {\n return {\n id: 'single-component',\n components: [props.content as UIComponent],\n grid: {\n columns: 12,\n gap: '1rem',\n },\n } as UILayout\n }\n return props.content as UILayout\n }\n\n // Grid position to CSS Grid styles\n const getGridStyles = (component: UIComponent) => {\n // ✅ PHASE 3 FIX: Defensive check for position field\n if (!component.position) {\n console.error('[UIResourceRenderer] Component missing position field:', component)\n return {\n 'grid-column': '1 / span 12',\n 'grid-row': 'auto',\n }\n }\n\n const { colStart, colSpan, rowStart, rowSpan = 1 } = component.position\n\n return {\n 'grid-column': `${colStart} / span ${colSpan}`,\n 'grid-row': rowStart ? `${rowStart} / span ${rowSpan}` : 'auto',\n }\n }\n\n // Convert grid styles to CSS string to avoid setStyleProperty\n const gridContainerStyle = () =>\n `grid-template-columns: repeat(${layout().grid.columns}, 1fr); gap: ${layout().grid.gap}`\n\n // Convert component grid styles to CSS string\n const getGridStyleString = (component: UIComponent) => {\n // ✅ PHASE 3 FIX: Defensive check for position field\n if (!component.position) {\n console.error('[UIResourceRenderer] Component missing position field:', component)\n return 'grid-column: 1 / span 12; grid-row: auto' // Default to full width\n }\n\n const { colStart, colSpan, rowStart, rowSpan = 1 } = component.position\n return `grid-column: ${colStart} / span ${colSpan}; grid-row: ${rowStart ? `${rowStart} / span ${rowSpan}` : 'auto'}`\n }\n\n return (\n <div class={`w-full ${props.class || ''}`}>\n <div class=\"grid gap-4\" style={gridContainerStyle()}>\n <For each={layout().components}>\n {(component) => (\n <div style={getGridStyleString(component)}>\n <ComponentRenderer component={component} onError={props.onError} />\n </div>\n )}\n </For>\n </div>\n </div>\n )\n}\n"],"names":["ChartRenderer","props","iframeUrl","setIframeUrl","createSignal","isLoading","setIsLoading","error","setError","onMount","chartParams","component","params","chartConfig","type","data","options","responsive","maintainAspectRatio","configStr","encodeURIComponent","JSON","stringify","url","_$ssr","_tmpl$5","_$ssrHydrationKey","_$escape","_$createComponent","Show","when","children","_tmpl$","_tmpl$2","_tmpl$4","title","_tmpl$3","_$ssrAttribute","TableRenderer","tableParams","_tmpl$7","For","each","columns","column","_tmpl$8","_$ssrStyle","width","label","rows","slice","DEFAULT_RESOURCE_LIMITS","maxTableRows","row","_tmpl$9","_tmpl$0","key","pagination","_tmpl$6","currentPage","pageSize","Math","min","totalRows","MetricRenderer","metricParams","_tmpl$12","value","unit","_tmpl$1","trend","_tmpl$10","direction","abs","subtitle","_tmpl$11","TextRenderer","textParams","htmlContent","createMemo","markdown","marked","parse","content","async","_tmpl$13","className","IframeRenderer","_tmpl$15","_tmpl$14","_$ssrStyleProperty","height","ImageRenderer","_tmpl$17","alt","caption","_tmpl$16","LinkRenderer","_tmpl$19","description","_tmpl$18","ComponentRenderer","validation","validateComponent","valid","onError","message","componentId","id","details","errors","_tmpl$20","GenerativeUIErrorBoundary","componentType","allowRetry","UIResourceRenderer","layout","components","grid","gap","gridContainerStyle","getGridStyleString","position","console","colStart","colSpan","rowStart","rowSpan","_tmpl$21","class","_tmpl$22"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"UIResourceRenderer.js","sources":["../../../../src/components/UIResourceRenderer.tsx"],"sourcesContent":["/**\n * UI Resource Renderer Component\n * Phase 0: Foundation with iframe sandbox and composite grid support\n *\n * Security features:\n * - Sandboxed iframes for untrusted content\n * - CSP enforcement via middleware\n * - XSS prevention with DOMPurify\n * - Domain whitelist validation\n *\n * Performance:\n * - Lazy loading with Intersection Observer\n * - Render timeout enforcement\n * - Error boundaries for isolation\n */\n\nimport { Component, createSignal, onMount, Show, For, createMemo } from 'solid-js'\nimport type { UIComponent, UILayout, RendererError, ComponentType } from '../types'\nimport { validateComponent, DEFAULT_RESOURCE_LIMITS } from '../services/validation'\nimport { GenerativeUIErrorBoundary } from './GenerativeUIErrorBoundary'\nimport { marked } from 'marked'\n\n/**\n * Props for UIResourceRenderer\n */\nexport interface UIResourceRendererProps {\n /**\n * Single component or full layout to render\n */\n content: UIComponent | UILayout\n\n /**\n * Lazy loading (default: true)\n */\n lazyLoad?: boolean\n\n /**\n * Error callback\n */\n onError?: (error: RendererError) => void\n\n /**\n * Custom CSS class\n */\n class?: string\n}\n\n/**\n * Render a single chart component in a sandboxed iframe\n */\nfunction ChartRenderer(props: {\n component: UIComponent\n onError?: (error: RendererError) => void\n}) {\n const [iframeUrl, setIframeUrl] = createSignal<string>()\n const [isLoading, setIsLoading] = createSignal(true)\n const [error, setError] = createSignal<string>()\n\n onMount(() => {\n const chartParams = props.component.params as any\n\n // Build Quickchart URL\n const chartConfig = {\n type: chartParams.type,\n data: chartParams.data,\n options: {\n ...chartParams.options,\n responsive: true,\n maintainAspectRatio: false,\n },\n }\n\n // Encode chart configuration for Quickchart API\n const configStr = encodeURIComponent(JSON.stringify(chartConfig))\n const url = `https://quickchart.io/chart?c=${configStr}&width=500&height=300&devicePixelRatio=2`\n\n // Validate domain (should always pass for quickchart.io)\n setIframeUrl(url)\n setIsLoading(false)\n })\n\n return (\n <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\">\n <Show when={isLoading()}>\n <div class=\"absolute inset-0 flex items-center justify-center\">\n <div class=\"animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600\" />\n </div>\n </Show>\n\n <Show when={error()}>\n <div class=\"absolute inset-0 flex items-center justify-center p-4\">\n <div class=\"text-center\">\n <p class=\"text-red-600 dark:text-red-400 text-sm font-medium\">Chart Error</p>\n <p class=\"text-gray-600 dark:text-gray-400 text-xs mt-1\">{error()}</p>\n </div>\n </div>\n </Show>\n\n <Show when={iframeUrl() && !error()}>\n <div class=\"w-full h-full p-4\">\n <Show when={(props.component.params as any).title}>\n <h3 class=\"text-sm font-semibold text-gray-900 dark:text-white mb-3\">\n {(props.component.params as any).title}\n </h3>\n </Show>\n <div class=\"w-full h-full\">\n <img\n src={iframeUrl()}\n alt=\"Chart visualization\"\n class=\"w-full h-auto max-h-[300px] object-contain\"\n onError={() => {\n setError('Failed to load chart')\n props.onError?.({\n type: 'render',\n message: 'Chart rendering failed',\n componentId: props.component.id,\n })\n }}\n />\n </div>\n </div>\n </Show>\n </div>\n )\n}\n\n/**\n * Render a table component\n */\nfunction TableRenderer(props: {\n component: UIComponent\n onError?: (error: RendererError) => void\n}) {\n const tableParams = props.component.params as any\n\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden\">\n <div class=\"p-4\">\n <Show when={tableParams.title}>\n <h3 class=\"text-sm font-semibold text-gray-900 dark:text-white mb-3\">\n {tableParams.title}\n </h3>\n </Show>\n\n <div class=\"overflow-x-auto\">\n <table class=\"min-w-full divide-y divide-gray-200 dark:divide-gray-700\">\n <thead class=\"bg-gray-50 dark:bg-gray-900\">\n <tr>\n <For each={tableParams.columns}>\n {(column: any) => (\n <th\n scope=\"col\"\n class=\"px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider\"\n style={column.width ? { width: column.width } : {}}\n >\n {column.label}\n </th>\n )}\n </For>\n </tr>\n </thead>\n <tbody class=\"bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700\">\n <For each={tableParams.rows.slice(0, DEFAULT_RESOURCE_LIMITS.maxTableRows)}>\n {(row: any) => (\n <tr class=\"hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors\">\n <For each={tableParams.columns}>\n {(column: any) => (\n <td class=\"px-4 py-3 text-sm text-gray-900 dark:text-gray-100 whitespace-nowrap\">\n {row[column.key] || '-'}\n </td>\n )}\n </For>\n </tr>\n )}\n </For>\n </tbody>\n </table>\n </div>\n\n <Show when={tableParams.pagination}>\n <div class=\"mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400\">\n <span>\n Showing {tableParams.pagination.currentPage * tableParams.pagination.pageSize + 1} -{' '}\n {Math.min(\n (tableParams.pagination.currentPage + 1) * tableParams.pagination.pageSize,\n tableParams.pagination.totalRows\n )}{' '}\n of {tableParams.pagination.totalRows}\n </span>\n </div>\n </Show>\n </div>\n </div>\n )\n}\n\n/**\n * Render a metric card component\n */\nfunction MetricRenderer(props: { component: UIComponent }) {\n const metricParams = props.component.params as any\n\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4\">\n <div class=\"flex flex-col h-full justify-between\">\n <div>\n <p class=\"text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide\">\n {metricParams.title}\n </p>\n <div class=\"mt-2 flex items-baseline\">\n <p class=\"text-2xl font-semibold text-gray-900 dark:text-white\">{metricParams.value}</p>\n <Show when={metricParams.unit}>\n <span class=\"ml-2 text-sm font-medium text-gray-500 dark:text-gray-400\">\n {metricParams.unit}\n </span>\n </Show>\n </div>\n </div>\n\n <Show when={metricParams.trend}>\n <div class=\"mt-3 flex items-center\">\n <span\n class={`text-sm font-medium ${metricParams.trend.direction === 'up'\n ? 'text-green-600 dark:text-green-400'\n : metricParams.trend.direction === 'down'\n ? 'text-red-600 dark:text-red-400'\n : 'text-gray-600 dark:text-gray-400'\n }`}\n >\n {metricParams.trend.direction === 'up'\n ? '�'\n : metricParams.trend.direction === 'down'\n ? '�'\n : '�'}{' '}\n {Math.abs(metricParams.trend.value)}%\n </span>\n </div>\n </Show>\n\n <Show when={metricParams.subtitle}>\n <p class=\"mt-2 text-xs text-gray-500 dark:text-gray-400\">{metricParams.subtitle}</p>\n </Show>\n </div>\n </div>\n )\n}\n\n/**\n * Render a text component (with optional markdown)\n */\nfunction TextRenderer(props: { component: UIComponent }) {\n const textParams = props.component.params as any\n\n // Convert markdown to HTML if markdown flag is true\n const htmlContent = createMemo(() => {\n if (textParams.markdown) {\n return marked.parse(textParams.content, { async: false }) as string\n }\n return textParams.content\n })\n\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4\">\n <div\n class={`prose prose-sm dark:prose-invert max-w-none ${textParams.className || ''}`}\n innerHTML={htmlContent()}\n />\n </div>\n )\n}\n\n/**\n * Render an iframe component\n */\nfunction IframeRenderer(props: { component: UIComponent }) {\n const params = props.component.params as any\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col\">\n <Show when={params.title}>\n <div class=\"px-4 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900\">\n <h3 class=\"text-sm font-semibold text-gray-900 dark:text-white\">{params.title}</h3>\n </div>\n </Show>\n <iframe\n src={params.url}\n title={params.title || 'Embedded content'}\n class=\"w-full border-0 flex-1\"\n style={`height: ${params.height || '400px'}; min-height: 300px;`}\n sandbox=\"allow-scripts allow-same-origin allow-popups allow-forms\"\n loading=\"lazy\"\n />\n </div>\n )\n}\n\n/**\n * Render an image component\n */\nfunction ImageRenderer(props: { component: UIComponent }) {\n const params = props.component.params as any\n return (\n <div class=\"w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col\">\n <div class=\"flex-1 flex items-center justify-center p-4 bg-gray-50 dark:bg-gray-900 min-h-[200px]\">\n <img\n src={params.url}\n alt={params.alt || 'Image'}\n class=\"max-w-full max-h-[500px] object-contain rounded shadow-sm\"\n loading=\"lazy\"\n />\n </div>\n <Show when={params.caption}>\n <div class=\"p-3 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800\">\n <p class=\"text-sm text-gray-600 dark:text-gray-400 text-center\">{params.caption}</p>\n </div>\n </Show>\n </div>\n )\n}\n\n/**\n * Render a link component\n */\nfunction LinkRenderer(props: { component: UIComponent }) {\n const params = props.component.params as any\n return (\n <a\n href={params.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors group h-full\"\n >\n <div class=\"p-2 bg-blue-50 dark:bg-blue-900/30 rounded-full text-blue-600 dark:text-blue-400 group-hover:bg-blue-100 dark:group-hover:bg-blue-900/50 shrink-0\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"w-5 h-5\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\" />\n <path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\" />\n </svg>\n </div>\n <div class=\"flex-1 min-w-0\">\n <h4 class=\"text-sm font-medium text-gray-900 dark:text-white truncate\">\n {params.label || params.url}\n </h4>\n <Show when={params.description}>\n <p class=\"text-xs text-gray-500 dark:text-gray-400 truncate\">{params.description}</p>\n </Show>\n </div>\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"w-4 h-4 text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300 shrink-0\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n <polyline points=\"15 3 21 3 21 9\" />\n <line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\" />\n </svg>\n </a>\n )\n}\n\n/**\n * Render a single component with error boundary\n */\nfunction ComponentRenderer(props: {\n component: UIComponent\n onError?: (error: RendererError) => void\n}) {\n // Validate component before rendering\n const validation = validateComponent(props.component)\n if (!validation.valid) {\n props.onError?.({\n type: 'validation',\n message: 'Component validation failed',\n componentId: props.component.id,\n details: validation.errors,\n })\n\n return (\n <div class=\"w-full h-full bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4\">\n <p class=\"text-sm font-medium text-red-900 dark:text-red-100\">Validation Error</p>\n <p class=\"text-xs text-red-700 dark:text-red-300 mt-1\">\n {validation.errors?.[0]?.message || 'Unknown validation error'}\n </p>\n </div>\n )\n }\n\n // Render based on component type with enhanced error boundary\n return (\n <GenerativeUIErrorBoundary\n componentId={props.component.id}\n componentType={props.component.type}\n onError={props.onError}\n allowRetry={true}\n >\n <Show when={props.component.type === 'chart'}>\n <ChartRenderer component={props.component} onError={props.onError} />\n </Show>\n <Show when={props.component.type === 'table'}>\n <TableRenderer component={props.component} onError={props.onError} />\n </Show>\n <Show when={props.component.type === 'metric'}>\n <MetricRenderer component={props.component} />\n </Show>\n <Show when={props.component.type === 'text'}>\n <TextRenderer component={props.component} />\n </Show>\n <Show when={props.component.type === 'iframe'}>\n <IframeRenderer component={props.component} />\n </Show>\n <Show when={props.component.type === 'image'}>\n <ImageRenderer component={props.component} />\n </Show>\n <Show when={props.component.type === 'link'}>\n <LinkRenderer component={props.component} />\n </Show>\n </GenerativeUIErrorBoundary>\n )\n}\n\n/**\n * Main UIResourceRenderer component\n */\nexport const UIResourceRenderer: Component<UIResourceRendererProps> = (props) => {\n const layout = () => {\n // ✅ PHASE 3.3 FIX: Check if content is a UIComponent (non-composite) vs UILayout (composite)\n // UILayout has type='composite', UIComponent has type='chart'|'table'|'metric'|'text'\n if ('type' in props.content && (props.content as any).type !== 'composite') {\n return {\n id: 'single-component',\n components: [props.content as UIComponent],\n grid: {\n columns: 12,\n gap: '1rem',\n },\n } as UILayout\n }\n return props.content as UILayout\n }\n\n // Grid position to CSS Grid styles\n const getGridStyles = (component: UIComponent) => {\n // ✅ PHASE 3 FIX: Defensive check for position field\n if (!component.position) {\n console.error('[UIResourceRenderer] Component missing position field:', component)\n return {\n 'grid-column': '1 / span 12',\n 'grid-row': 'auto',\n }\n }\n\n const { colStart, colSpan, rowStart, rowSpan = 1 } = component.position\n\n return {\n 'grid-column': `${colStart} / span ${colSpan}`,\n 'grid-row': rowStart ? `${rowStart} / span ${rowSpan}` : 'auto',\n }\n }\n\n // Convert grid styles to CSS string to avoid setStyleProperty\n const gridContainerStyle = () =>\n `grid-template-columns: repeat(${layout().grid.columns}, 1fr); gap: ${layout().grid.gap}`\n\n // Convert component grid styles to CSS string\n const getGridStyleString = (component: UIComponent) => {\n // ✅ PHASE 3 FIX: Defensive check for position field\n if (!component.position) {\n console.error('[UIResourceRenderer] Component missing position field:', component)\n return 'grid-column: 1 / span 12; grid-row: auto' // Default to full width\n }\n\n const { colStart, colSpan, rowStart, rowSpan = 1 } = component.position\n return `grid-column: ${colStart} / span ${colSpan}; grid-row: ${rowStart ? `${rowStart} / span ${rowSpan}` : 'auto'}`\n }\n\n return (\n <div class={`w-full ${props.class || ''}`}>\n <div class=\"grid gap-4\" style={gridContainerStyle()}>\n <For each={layout().components}>\n {(component) => (\n <div style={getGridStyleString(component)}>\n <ComponentRenderer component={component} onError={props.onError} />\n </div>\n )}\n </For>\n </div>\n </div>\n )\n}\n"],"names":["ChartRenderer","props","iframeUrl","setIframeUrl","createSignal","isLoading","setIsLoading","error","setError","onMount","chartParams","component","params","chartConfig","type","data","options","responsive","maintainAspectRatio","configStr","encodeURIComponent","JSON","stringify","url","_$ssr","_tmpl$5","_$ssrHydrationKey","_$escape","_$createComponent","Show","when","children","_tmpl$","_tmpl$2","_tmpl$4","title","_tmpl$3","_$ssrAttribute","TableRenderer","tableParams","_tmpl$7","For","each","columns","column","_tmpl$8","_$ssrStyle","width","label","rows","slice","DEFAULT_RESOURCE_LIMITS","maxTableRows","row","_tmpl$9","_tmpl$0","key","pagination","_tmpl$6","currentPage","pageSize","Math","min","totalRows","MetricRenderer","metricParams","_tmpl$12","value","unit","_tmpl$1","trend","_tmpl$10","direction","abs","subtitle","_tmpl$11","TextRenderer","textParams","htmlContent","createMemo","markdown","marked","parse","content","async","_tmpl$13","className","IframeRenderer","_tmpl$15","_tmpl$14","height","ImageRenderer","_tmpl$17","alt","caption","_tmpl$16","LinkRenderer","_tmpl$19","description","_tmpl$18","ComponentRenderer","validation","validateComponent","valid","onError","message","componentId","id","details","errors","_tmpl$20","GenerativeUIErrorBoundary","componentType","allowRetry","UIResourceRenderer","layout","components","grid","gap","gridContainerStyle","getGridStyleString","position","console","colStart","colSpan","rowStart","rowSpan","_tmpl$21","class","_tmpl$22"],"mappings":"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"}
|
|
@@ -203,6 +203,36 @@ function validateComponent(component, limits = DEFAULT_RESOURCE_LIMITS) {
|
|
|
203
203
|
});
|
|
204
204
|
}
|
|
205
205
|
break;
|
|
206
|
+
case "iframe":
|
|
207
|
+
const iframeParams = component.params;
|
|
208
|
+
if (!iframeParams.url) {
|
|
209
|
+
errors.push({
|
|
210
|
+
path: "params",
|
|
211
|
+
message: "Iframe component must have url",
|
|
212
|
+
code: "INVALID_IFRAME"
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
case "image":
|
|
217
|
+
const imageParams = component.params;
|
|
218
|
+
if (!imageParams.url) {
|
|
219
|
+
errors.push({
|
|
220
|
+
path: "params",
|
|
221
|
+
message: "Image component must have url",
|
|
222
|
+
code: "INVALID_IMAGE"
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
226
|
+
case "link":
|
|
227
|
+
const linkParams = component.params;
|
|
228
|
+
if (!linkParams.url) {
|
|
229
|
+
errors.push({
|
|
230
|
+
path: "params",
|
|
231
|
+
message: "Link component must have url",
|
|
232
|
+
code: "INVALID_LINK"
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
206
236
|
default:
|
|
207
237
|
errors.push({
|
|
208
238
|
path: "type",
|
|
@@ -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} from '../types'\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 * Allowed iframe domains (whitelist)\n * Must match CSP frame-src directive\n */\nconst ALLOWED_IFRAME_DOMAINS = [\n 'quickchart.io',\n 'www.quickchart.io',\n 'deposium.com',\n 'deposium.vip',\n 'localhost',\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 */\nexport function validateIframeDomain(url: string): ValidationResult {\n try {\n const parsedUrl = new URL(url)\n const domain = parsedUrl.hostname\n\n const isAllowed = ALLOWED_IFRAME_DOMAINS.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 */\nexport function validateComponent(\n component: UIComponent,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\n): ValidationResult {\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 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 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 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 default:\n errors.push({\n path: 'type',\n message: `Unknown component type: ${component.type}`,\n code: 'UNKNOWN_COMPONENT_TYPE',\n })\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 */\nexport function validateLayout(\n layout: UILayout,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\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, limits)\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"],"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} from '../types'\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 * Allowed iframe domains (whitelist)\n * Must match CSP frame-src directive\n */\nconst ALLOWED_IFRAME_DOMAINS = [\n 'quickchart.io',\n 'www.quickchart.io',\n 'deposium.com',\n 'deposium.vip',\n 'localhost',\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 */\nexport function validateIframeDomain(url: string): ValidationResult {\n try {\n const parsedUrl = new URL(url)\n const domain = parsedUrl.hostname\n\n const isAllowed = ALLOWED_IFRAME_DOMAINS.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 */\nexport function validateComponent(\n component: UIComponent,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\n): ValidationResult {\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 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 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 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 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 }\n break\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 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 default:\n errors.push({\n path: 'type',\n message: `Unknown component type: ${component.type}`,\n code: 'UNKNOWN_COMPONENT_TYPE',\n })\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 */\nexport function validateLayout(\n layout: UILayout,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\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, limits)\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"],"names":[],"mappings":"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"}
|
|
@@ -201,6 +201,36 @@ function validateComponent(component, limits = DEFAULT_RESOURCE_LIMITS) {
|
|
|
201
201
|
});
|
|
202
202
|
}
|
|
203
203
|
break;
|
|
204
|
+
case "iframe":
|
|
205
|
+
const iframeParams = component.params;
|
|
206
|
+
if (!iframeParams.url) {
|
|
207
|
+
errors.push({
|
|
208
|
+
path: "params",
|
|
209
|
+
message: "Iframe component must have url",
|
|
210
|
+
code: "INVALID_IFRAME"
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
case "image":
|
|
215
|
+
const imageParams = component.params;
|
|
216
|
+
if (!imageParams.url) {
|
|
217
|
+
errors.push({
|
|
218
|
+
path: "params",
|
|
219
|
+
message: "Image component must have url",
|
|
220
|
+
code: "INVALID_IMAGE"
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
case "link":
|
|
225
|
+
const linkParams = component.params;
|
|
226
|
+
if (!linkParams.url) {
|
|
227
|
+
errors.push({
|
|
228
|
+
path: "params",
|
|
229
|
+
message: "Link component must have url",
|
|
230
|
+
code: "INVALID_LINK"
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
204
234
|
default:
|
|
205
235
|
errors.push({
|
|
206
236
|
path: "type",
|
|
@@ -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} from '../types'\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 * Allowed iframe domains (whitelist)\n * Must match CSP frame-src directive\n */\nconst ALLOWED_IFRAME_DOMAINS = [\n 'quickchart.io',\n 'www.quickchart.io',\n 'deposium.com',\n 'deposium.vip',\n 'localhost',\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 */\nexport function validateIframeDomain(url: string): ValidationResult {\n try {\n const parsedUrl = new URL(url)\n const domain = parsedUrl.hostname\n\n const isAllowed = ALLOWED_IFRAME_DOMAINS.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 */\nexport function validateComponent(\n component: UIComponent,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\n): ValidationResult {\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 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 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 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 default:\n errors.push({\n path: 'type',\n message: `Unknown component type: ${component.type}`,\n code: 'UNKNOWN_COMPONENT_TYPE',\n })\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 */\nexport function validateLayout(\n layout: UILayout,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\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, limits)\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"],"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} from '../types'\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 * Allowed iframe domains (whitelist)\n * Must match CSP frame-src directive\n */\nconst ALLOWED_IFRAME_DOMAINS = [\n 'quickchart.io',\n 'www.quickchart.io',\n 'deposium.com',\n 'deposium.vip',\n 'localhost',\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 */\nexport function validateIframeDomain(url: string): ValidationResult {\n try {\n const parsedUrl = new URL(url)\n const domain = parsedUrl.hostname\n\n const isAllowed = ALLOWED_IFRAME_DOMAINS.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 */\nexport function validateComponent(\n component: UIComponent,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\n): ValidationResult {\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 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 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 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 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 }\n break\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 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 default:\n errors.push({\n path: 'type',\n message: `Unknown component type: ${component.type}`,\n code: 'UNKNOWN_COMPONENT_TYPE',\n })\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 */\nexport function validateLayout(\n layout: UILayout,\n limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS\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, limits)\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"],"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,EACrB,MAAM,UAAU,CAAA;AAEjB;;GAEG;AACH,eAAO,MAAM,uBAAuB,EAAE,cAKrC,CAAA;AAcD;;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,CA8ClB;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;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAmClE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,WAAW,EACtB,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,EACrB,MAAM,UAAU,CAAA;AAEjB;;GAEG;AACH,eAAO,MAAM,uBAAuB,EAAE,cAKrC,CAAA;AAcD;;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,CA8ClB;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;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAmClE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,WAAW,EACtB,MAAM,GAAE,cAAwC,GAC/C,gBAAgB,CAuGlB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,QAAQ,EAChB,MAAM,GAAE,cAAwC,GAC/C,gBAAgB,CA8ClB"}
|