@seed-ship/mcp-ui-solid 1.0.29 → 1.0.31

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.
Files changed (85) hide show
  1. package/dist/components/GenerativeUIErrorBoundary.cjs.map +1 -0
  2. package/dist/components/GenerativeUIErrorBoundary.js.map +1 -0
  3. package/dist/components/StreamingUIRenderer.cjs.map +1 -0
  4. package/dist/components/StreamingUIRenderer.js.map +1 -0
  5. package/dist/{mcp-ui-solid/src/components → components}/UIResourceRenderer.cjs +102 -97
  6. package/dist/components/UIResourceRenderer.cjs.map +1 -0
  7. package/dist/components/UIResourceRenderer.d.ts +0 -11
  8. package/dist/components/UIResourceRenderer.d.ts.map +1 -1
  9. package/dist/{mcp-ui-solid/src/components → components}/UIResourceRenderer.js +102 -97
  10. package/dist/components/UIResourceRenderer.js.map +1 -0
  11. package/dist/components.cjs +3 -3
  12. package/dist/components.d.ts +12 -0
  13. package/dist/components.js +3 -3
  14. package/dist/hooks/useStreamingUI.cjs.map +1 -0
  15. package/dist/hooks/useStreamingUI.js.map +1 -0
  16. package/dist/hooks.cjs +1 -1
  17. package/dist/hooks.d.ts +8 -0
  18. package/dist/hooks.js +1 -1
  19. package/dist/index.cjs +6 -6
  20. package/dist/index.js +6 -6
  21. package/dist/node_modules/.pnpm/dompurify@3.3.0/node_modules/dompurify/dist/purify.es.cjs +1006 -0
  22. package/dist/node_modules/.pnpm/dompurify@3.3.0/node_modules/dompurify/dist/purify.es.cjs.map +1 -0
  23. package/dist/node_modules/.pnpm/dompurify@3.3.0/node_modules/dompurify/dist/purify.es.js +1007 -0
  24. package/dist/node_modules/.pnpm/dompurify@3.3.0/node_modules/dompurify/dist/purify.es.js.map +1 -0
  25. package/dist/services/component-registry.cjs.map +1 -0
  26. package/dist/services/component-registry.js.map +1 -0
  27. package/dist/services/validation.cjs.map +1 -0
  28. package/dist/services/validation.js.map +1 -0
  29. package/dist/types.d.ts +265 -0
  30. package/dist/utils/logger.cjs.map +1 -0
  31. package/dist/utils/logger.js.map +1 -0
  32. package/dist/validation.cjs +1 -1
  33. package/dist/validation.js +1 -1
  34. package/package.json +20 -23
  35. package/src/components/ActionRenderer.tsx +33 -0
  36. package/src/components/ArtifactRenderer.tsx +54 -0
  37. package/src/components/CarouselRenderer.tsx +77 -0
  38. package/src/components/FooterRenderer.tsx +66 -0
  39. package/src/components/GenerativeUIErrorBoundary.tsx +259 -0
  40. package/src/components/StreamingUIRenderer.tsx +327 -0
  41. package/src/components/UIResourceRenderer.tsx +573 -0
  42. package/src/components/index.ts +14 -0
  43. package/src/hooks/index.ts +14 -0
  44. package/src/hooks/useStreamingUI.ts +447 -0
  45. package/src/index.test.ts +36 -0
  46. package/src/index.ts +70 -0
  47. package/src/services/component-registry.ts +378 -0
  48. package/src/services/index.ts +9 -0
  49. package/src/services/validation.ts +472 -0
  50. package/src/types/index.ts +320 -0
  51. package/src/types-export.ts +31 -0
  52. package/src/utils/logger.ts +74 -0
  53. package/src/validation.ts +38 -0
  54. package/src/vite-env.d.ts +11 -0
  55. package/tsconfig.json +20 -0
  56. package/tsconfig.tsbuildinfo +1 -0
  57. package/vite.config.ts +52 -0
  58. package/vite.config.ts.timestamp-1763266929437-a71eed80b91318.mjs +45 -0
  59. package/vitest.config.ts +10 -0
  60. package/dist/mcp-ui-solid/src/components/GenerativeUIErrorBoundary.cjs.map +0 -1
  61. package/dist/mcp-ui-solid/src/components/GenerativeUIErrorBoundary.js.map +0 -1
  62. package/dist/mcp-ui-solid/src/components/StreamingUIRenderer.cjs.map +0 -1
  63. package/dist/mcp-ui-solid/src/components/StreamingUIRenderer.js.map +0 -1
  64. package/dist/mcp-ui-solid/src/components/UIResourceRenderer.cjs.map +0 -1
  65. package/dist/mcp-ui-solid/src/components/UIResourceRenderer.js.map +0 -1
  66. package/dist/mcp-ui-solid/src/hooks/useStreamingUI.cjs.map +0 -1
  67. package/dist/mcp-ui-solid/src/hooks/useStreamingUI.js.map +0 -1
  68. package/dist/mcp-ui-solid/src/services/component-registry.cjs.map +0 -1
  69. package/dist/mcp-ui-solid/src/services/component-registry.js.map +0 -1
  70. package/dist/mcp-ui-solid/src/services/validation.cjs.map +0 -1
  71. package/dist/mcp-ui-solid/src/services/validation.js.map +0 -1
  72. package/dist/mcp-ui-solid/src/utils/logger.cjs.map +0 -1
  73. package/dist/mcp-ui-solid/src/utils/logger.js.map +0 -1
  74. /package/dist/{mcp-ui-solid/src/components → components}/GenerativeUIErrorBoundary.cjs +0 -0
  75. /package/dist/{mcp-ui-solid/src/components → components}/GenerativeUIErrorBoundary.js +0 -0
  76. /package/dist/{mcp-ui-solid/src/components → components}/StreamingUIRenderer.cjs +0 -0
  77. /package/dist/{mcp-ui-solid/src/components → components}/StreamingUIRenderer.js +0 -0
  78. /package/dist/{mcp-ui-solid/src/hooks → hooks}/useStreamingUI.cjs +0 -0
  79. /package/dist/{mcp-ui-solid/src/hooks → hooks}/useStreamingUI.js +0 -0
  80. /package/dist/{mcp-ui-solid/src/services → services}/component-registry.cjs +0 -0
  81. /package/dist/{mcp-ui-solid/src/services → services}/component-registry.js +0 -0
  82. /package/dist/{mcp-ui-solid/src/services → services}/validation.cjs +0 -0
  83. /package/dist/{mcp-ui-solid/src/services → services}/validation.js +0 -0
  84. /package/dist/{mcp-ui-solid/src/utils → utils}/logger.cjs +0 -0
  85. /package/dist/{mcp-ui-solid/src/utils → utils}/logger.js +0 -0
@@ -0,0 +1,472 @@
1
+ /**
2
+ * Component Validation Service
3
+ * Phase 0: Resource Limits & Schema Validation
4
+ *
5
+ * Validates LLM-generated components against:
6
+ * - JSON schema
7
+ * - Resource limits (data points, payload size, grid bounds)
8
+ * - Security constraints (domain whitelist, XSS prevention)
9
+ */
10
+
11
+ import type {
12
+ UIComponent,
13
+ UILayout,
14
+ ValidationResult,
15
+ ResourceLimits,
16
+ ChartComponentParams,
17
+ TableComponentParams,
18
+ } from '../types'
19
+
20
+ /**
21
+ * Default resource limits (configurable via env)
22
+ */
23
+ export const DEFAULT_RESOURCE_LIMITS: ResourceLimits = {
24
+ maxDataPoints: 1000,
25
+ maxTableRows: 100,
26
+ maxPayloadSize: 50 * 1024, // 50KB
27
+ renderTimeout: 5000, // 5 seconds
28
+ }
29
+
30
+ /**
31
+ * Allowed iframe domains (whitelist)
32
+ * Must match CSP frame-src directive
33
+ */
34
+ const ALLOWED_IFRAME_DOMAINS = [
35
+ 'quickchart.io',
36
+ 'www.quickchart.io',
37
+ 'deposium.com',
38
+ 'deposium.vip',
39
+ 'localhost',
40
+ ]
41
+
42
+ /**
43
+ * Validate grid position bounds (1-12 columns)
44
+ */
45
+ export function validateGridPosition(position: UIComponent['position']): ValidationResult {
46
+ const errors: ValidationResult['errors'] = []
47
+
48
+ // ✅ PHASE 3 FIX: Defensive check for undefined position
49
+ if (!position) {
50
+ return {
51
+ valid: false,
52
+ errors: [
53
+ {
54
+ path: 'position',
55
+ message: 'Position is required',
56
+ code: 'MISSING_POSITION',
57
+ },
58
+ ],
59
+ }
60
+ }
61
+
62
+ if (position.colStart < 1 || position.colStart > 12) {
63
+ errors.push({
64
+ path: 'position.colStart',
65
+ message: 'Column start must be between 1 and 12',
66
+ code: 'INVALID_GRID_COL_START',
67
+ })
68
+ }
69
+
70
+ if (position.colSpan < 1 || position.colSpan > 12) {
71
+ errors.push({
72
+ path: 'position.colSpan',
73
+ message: 'Column span must be between 1 and 12',
74
+ code: 'INVALID_GRID_COL_SPAN',
75
+ })
76
+ }
77
+
78
+ if (position.colStart + position.colSpan - 1 > 12) {
79
+ errors.push({
80
+ path: 'position',
81
+ message: 'Column start + span exceeds grid width (12)',
82
+ code: 'GRID_OVERFLOW',
83
+ })
84
+ }
85
+
86
+ if (position.rowStart !== undefined && position.rowStart < 1) {
87
+ errors.push({
88
+ path: 'position.rowStart',
89
+ message: 'Row start must be >= 1',
90
+ code: 'INVALID_GRID_ROW_START',
91
+ })
92
+ }
93
+
94
+ if (position.rowSpan !== undefined && position.rowSpan < 1) {
95
+ errors.push({
96
+ path: 'position.rowSpan',
97
+ message: 'Row span must be >= 1',
98
+ code: 'INVALID_GRID_ROW_SPAN',
99
+ })
100
+ }
101
+
102
+ return {
103
+ valid: errors.length === 0,
104
+ errors: errors.length > 0 ? errors : undefined,
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Validate chart component against resource limits
110
+ */
111
+ export function validateChartComponent(
112
+ params: ChartComponentParams,
113
+ limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS
114
+ ): ValidationResult {
115
+ const errors: ValidationResult['errors'] = []
116
+
117
+ // Validate data points count
118
+ const totalDataPoints = params.data.datasets.reduce(
119
+ (sum, dataset) => sum + dataset.data.length,
120
+ 0
121
+ )
122
+
123
+ if (totalDataPoints > limits.maxDataPoints) {
124
+ errors.push({
125
+ path: 'params.data',
126
+ message: `Chart exceeds max data points: ${totalDataPoints} > ${limits.maxDataPoints}`,
127
+ code: 'RESOURCE_LIMIT_EXCEEDED',
128
+ })
129
+ }
130
+
131
+ // Validate labels match dataset length
132
+ const expectedLength = params.data.labels.length
133
+ for (const [index, dataset] of params.data.datasets.entries()) {
134
+ if (dataset.data.length !== expectedLength) {
135
+ errors.push({
136
+ path: `params.data.datasets[${index}]`,
137
+ message: `Dataset length mismatch: expected ${expectedLength}, got ${dataset.data.length}`,
138
+ code: 'DATA_LENGTH_MISMATCH',
139
+ })
140
+ }
141
+ }
142
+
143
+ // Validate numeric data
144
+ for (const [index, dataset] of params.data.datasets.entries()) {
145
+ for (const [dataIndex, value] of dataset.data.entries()) {
146
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
147
+ errors.push({
148
+ path: `params.data.datasets[${index}].data[${dataIndex}]`,
149
+ message: `Invalid data value: ${value} (must be finite number)`,
150
+ code: 'INVALID_DATA_TYPE',
151
+ })
152
+ }
153
+ }
154
+ }
155
+
156
+ return {
157
+ valid: errors.length === 0,
158
+ errors: errors.length > 0 ? errors : undefined,
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Validate table component against resource limits
164
+ */
165
+ export function validateTableComponent(
166
+ params: TableComponentParams,
167
+ limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS
168
+ ): ValidationResult {
169
+ const errors: ValidationResult['errors'] = []
170
+
171
+ // Validate row count
172
+ if (params.rows.length > limits.maxTableRows) {
173
+ errors.push({
174
+ path: 'params.rows',
175
+ message: `Table exceeds max rows: ${params.rows.length} > ${limits.maxTableRows}`,
176
+ code: 'RESOURCE_LIMIT_EXCEEDED',
177
+ })
178
+ }
179
+
180
+ // Validate columns
181
+ if (params.columns.length === 0) {
182
+ errors.push({
183
+ path: 'params.columns',
184
+ message: 'Table must have at least one column',
185
+ code: 'EMPTY_COLUMNS',
186
+ })
187
+ }
188
+
189
+ // Validate column keys are unique
190
+ const columnKeys = new Set<string>()
191
+ for (const [index, column] of params.columns.entries()) {
192
+ if (columnKeys.has(column.key)) {
193
+ errors.push({
194
+ path: `params.columns[${index}]`,
195
+ message: `Duplicate column key: ${column.key}`,
196
+ code: 'DUPLICATE_COLUMN_KEY',
197
+ })
198
+ }
199
+ columnKeys.add(column.key)
200
+ }
201
+
202
+ // Validate rows have valid data for defined columns
203
+ for (const [rowIndex, row] of params.rows.entries()) {
204
+ for (const column of params.columns) {
205
+ if (!(column.key in row)) {
206
+ errors.push({
207
+ path: `params.rows[${rowIndex}]`,
208
+ message: `Missing column key: ${column.key}`,
209
+ code: 'MISSING_COLUMN_DATA',
210
+ })
211
+ }
212
+ }
213
+ }
214
+
215
+ return {
216
+ valid: errors.length === 0,
217
+ errors: errors.length > 0 ? errors : undefined,
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Validate payload size
223
+ */
224
+ export function validatePayloadSize(
225
+ component: UIComponent,
226
+ limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS
227
+ ): ValidationResult {
228
+ const payloadSize = JSON.stringify(component).length
229
+
230
+ if (payloadSize > limits.maxPayloadSize) {
231
+ return {
232
+ valid: false,
233
+ errors: [
234
+ {
235
+ path: 'component',
236
+ message: `Payload size exceeds limit: ${payloadSize} > ${limits.maxPayloadSize} bytes`,
237
+ code: 'PAYLOAD_TOO_LARGE',
238
+ },
239
+ ],
240
+ }
241
+ }
242
+
243
+ return { valid: true }
244
+ }
245
+
246
+ /**
247
+ * Sanitize string to prevent XSS
248
+ * Basic implementation - DOMPurify used at render time
249
+ */
250
+ export function sanitizeString(input: string): string {
251
+ return input
252
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
253
+ .replace(/on\w+="[^"]*"/gi, '')
254
+ .replace(/javascript:/gi, '')
255
+ }
256
+
257
+ /**
258
+ * Validate iframe domain against whitelist
259
+ */
260
+ export function validateIframeDomain(url: string): ValidationResult {
261
+ try {
262
+ const parsedUrl = new URL(url)
263
+ const domain = parsedUrl.hostname
264
+
265
+ const isAllowed = ALLOWED_IFRAME_DOMAINS.some(
266
+ (allowed) => domain === allowed || domain.endsWith(`.${allowed}`) || allowed === 'localhost'
267
+ )
268
+
269
+ if (!isAllowed) {
270
+ return {
271
+ valid: false,
272
+ errors: [
273
+ {
274
+ path: 'url',
275
+ message: `Domain not whitelisted: ${domain}`,
276
+ code: 'DOMAIN_NOT_WHITELISTED',
277
+ },
278
+ ],
279
+ }
280
+ }
281
+
282
+ return { valid: true }
283
+ } catch (error) {
284
+ return {
285
+ valid: false,
286
+ errors: [
287
+ {
288
+ path: 'url',
289
+ message: 'Invalid URL format',
290
+ code: 'INVALID_URL',
291
+ },
292
+ ],
293
+ }
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Validate entire component
299
+ */
300
+ export function validateComponent(
301
+ component: UIComponent,
302
+ limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS
303
+ ): ValidationResult {
304
+ const errors: ValidationResult['errors'] = []
305
+
306
+ // Validate grid position
307
+ const gridResult = validateGridPosition(component.position)
308
+ if (!gridResult.valid) {
309
+ errors.push(...(gridResult.errors || []))
310
+ }
311
+
312
+ // Validate payload size
313
+ const sizeResult = validatePayloadSize(component, limits)
314
+ if (!sizeResult.valid) {
315
+ errors.push(...(sizeResult.errors || []))
316
+ }
317
+
318
+ // Type-specific validation
319
+ switch (component.type) {
320
+ case 'chart':
321
+ const chartResult = validateChartComponent(component.params as ChartComponentParams, limits)
322
+ if (!chartResult.valid) {
323
+ errors.push(...(chartResult.errors || []))
324
+ }
325
+ break
326
+
327
+ case 'table':
328
+ const tableResult = validateTableComponent(component.params as TableComponentParams, limits)
329
+ if (!tableResult.valid) {
330
+ errors.push(...(tableResult.errors || []))
331
+ }
332
+ break
333
+
334
+ case 'metric':
335
+ // Basic validation for metrics
336
+ const metricParams = component.params as any
337
+ if (!metricParams.title || !metricParams.value) {
338
+ errors.push({
339
+ path: 'params',
340
+ message: 'Metric must have title and value',
341
+ code: 'INVALID_METRIC',
342
+ })
343
+ }
344
+ break
345
+
346
+ case 'text':
347
+ // Basic validation for text
348
+ const textParams = component.params as any
349
+ if (!textParams.content) {
350
+ errors.push({
351
+ path: 'params',
352
+ message: 'Text component must have content',
353
+ code: 'INVALID_TEXT',
354
+ })
355
+ }
356
+ break
357
+
358
+ case 'iframe':
359
+ // Basic validation for iframe
360
+ const iframeParams = component.params as any
361
+ if (!iframeParams.url) {
362
+ errors.push({
363
+ path: 'params',
364
+ message: 'Iframe component must have url',
365
+ code: 'INVALID_IFRAME',
366
+ })
367
+ }
368
+ break
369
+
370
+ case 'image':
371
+ // Basic validation for image
372
+ const imageParams = component.params as any
373
+ if (!imageParams.url) {
374
+ errors.push({
375
+ path: 'params',
376
+ message: 'Image component must have url',
377
+ code: 'INVALID_IMAGE',
378
+ })
379
+ }
380
+ break
381
+
382
+ case 'link':
383
+ // Basic validation for link
384
+ const linkParams = component.params as any
385
+ if (!linkParams.url) {
386
+ errors.push({
387
+ path: 'params',
388
+ message: 'Link component must have url',
389
+ code: 'INVALID_LINK',
390
+ })
391
+ }
392
+ break
393
+
394
+ case 'action':
395
+ // Basic validation for action
396
+ const actionParams = component.params as any
397
+ if (!actionParams.label) {
398
+ errors.push({
399
+ path: 'params',
400
+ message: 'Action component must have label',
401
+ code: 'INVALID_ACTION',
402
+ })
403
+ }
404
+ break
405
+
406
+ default:
407
+ errors.push({
408
+ path: 'type',
409
+ message: `Unknown component type: ${component.type}`,
410
+ code: 'UNKNOWN_COMPONENT_TYPE',
411
+ })
412
+ }
413
+
414
+ return {
415
+ valid: errors.length === 0,
416
+ errors: errors.length > 0 ? errors : undefined,
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Validate entire layout
422
+ */
423
+ export function validateLayout(
424
+ layout: UILayout,
425
+ limits: ResourceLimits = DEFAULT_RESOURCE_LIMITS
426
+ ): ValidationResult {
427
+ const errors: ValidationResult['errors'] = []
428
+
429
+ // Validate component count
430
+ if (layout.components.length === 0) {
431
+ errors.push({
432
+ path: 'components',
433
+ message: 'Layout must have at least one component',
434
+ code: 'EMPTY_LAYOUT',
435
+ })
436
+ }
437
+
438
+ if (layout.components.length > 12) {
439
+ errors.push({
440
+ path: 'components',
441
+ message: `Layout exceeds max components: ${layout.components.length} > 12`,
442
+ code: 'TOO_MANY_COMPONENTS',
443
+ })
444
+ }
445
+
446
+ // Validate each component
447
+ for (const [index, component] of layout.components.entries()) {
448
+ const result = validateComponent(component, limits)
449
+ if (!result.valid) {
450
+ errors.push(
451
+ ...(result.errors?.map((error) => ({
452
+ ...error,
453
+ path: `components[${index}].${error.path}`,
454
+ })) || [])
455
+ )
456
+ }
457
+ }
458
+
459
+ // Validate grid configuration
460
+ if (layout.grid.columns !== 12) {
461
+ errors.push({
462
+ path: 'grid.columns',
463
+ message: 'Grid must have 12 columns (Bootstrap-like)',
464
+ code: 'INVALID_GRID_COLUMNS',
465
+ })
466
+ }
467
+
468
+ return {
469
+ valid: errors.length === 0,
470
+ errors: errors.length > 0 ? errors : undefined,
471
+ }
472
+ }