@seed-ship/mcp-ui-solid 6.13.0 → 6.14.0
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/CHANGELOG.md +27 -0
- package/dist/components/UIResourceRenderer.cjs +343 -297
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts +19 -0
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +343 -297
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ChartRenderer.quickchart.test.tsx +66 -0
- package/src/components/UIResourceRenderer.tsx +79 -7
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v6.14.0 — audit P1.7: the quickchart.io fallback must be an explicit host
|
|
3
|
+
* opt-in, never an implicit external network call.
|
|
4
|
+
*
|
|
5
|
+
* We drive the decision with `renderer: 'iframe'` (an explicit request for the
|
|
6
|
+
* external image path) — that branch is gated purely on `allowQuickchartFallback`
|
|
7
|
+
* and is independent of whether the `chart.js` peer happens to be installed in
|
|
8
|
+
* the test env. Default (no opt-in) → no quickchart URL, degrades to a local
|
|
9
|
+
* table; with opt-in → the quickchart.io image URL is produced.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, afterEach } from 'vitest'
|
|
12
|
+
import { render, cleanup, waitFor } from '@solidjs/testing-library'
|
|
13
|
+
import { UIResourceRenderer } from './UIResourceRenderer'
|
|
14
|
+
import type { UIComponent } from '../types'
|
|
15
|
+
|
|
16
|
+
afterEach(cleanup)
|
|
17
|
+
|
|
18
|
+
const chart: UIComponent = {
|
|
19
|
+
id: 'c1',
|
|
20
|
+
type: 'chart',
|
|
21
|
+
position: { colStart: 1, colSpan: 12 },
|
|
22
|
+
params: {
|
|
23
|
+
type: 'bar',
|
|
24
|
+
title: 'Sensitive revenue',
|
|
25
|
+
renderer: 'iframe', // explicit external render request
|
|
26
|
+
data: {
|
|
27
|
+
labels: ['Q1', 'Q2'],
|
|
28
|
+
datasets: [{ label: 'Revenue', data: [100, 200] }],
|
|
29
|
+
},
|
|
30
|
+
} as any,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function quickchartImg(container: HTMLElement): HTMLImageElement | null {
|
|
34
|
+
return (
|
|
35
|
+
(Array.from(container.querySelectorAll('img')).find((img) =>
|
|
36
|
+
(img.getAttribute('src') ?? '').includes('quickchart.io')
|
|
37
|
+
) as HTMLImageElement | undefined) ?? null
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('Chart quickchart fallback is opt-in (P1.7)', () => {
|
|
42
|
+
it('does NOT call quickchart.io by default — degrades instead', async () => {
|
|
43
|
+
const { container } = render(() => <UIResourceRenderer content={chart} />)
|
|
44
|
+
|
|
45
|
+
await waitFor(() => {
|
|
46
|
+
expect(container.textContent ?? '').toContain('Interactive chart unavailable')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
expect(quickchartImg(container)).toBeNull()
|
|
50
|
+
// The degraded table surfaces the underlying series data.
|
|
51
|
+
expect(container.textContent).toContain('Revenue')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('uses quickchart.io only when the host opts in', async () => {
|
|
55
|
+
const { container } = render(() => (
|
|
56
|
+
<UIResourceRenderer content={chart} allowQuickchartFallback />
|
|
57
|
+
))
|
|
58
|
+
|
|
59
|
+
await waitFor(() => {
|
|
60
|
+
expect(quickchartImg(container)).not.toBeNull()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const src = quickchartImg(container)?.getAttribute('src') ?? ''
|
|
64
|
+
expect(src).toContain('https://quickchart.io/chart?c=')
|
|
65
|
+
})
|
|
66
|
+
})
|
|
@@ -43,6 +43,8 @@ import { FormRenderer } from './FormRenderer'
|
|
|
43
43
|
import { ModalRenderer } from './ModalRenderer'
|
|
44
44
|
import { ActionGroupRenderer } from './ActionGroupRenderer'
|
|
45
45
|
import { ChartJSRenderer, isChartJSAvailable } from './ChartJSRenderer'
|
|
46
|
+
import { DegradedFallback } from './DegradedFallback'
|
|
47
|
+
import { chartToDegradedTable } from '../utils/degraded-projections'
|
|
46
48
|
import { ImageGalleryRenderer } from './ImageGalleryRenderer'
|
|
47
49
|
import { VideoRenderer } from './VideoRenderer'
|
|
48
50
|
import { CodeBlockRenderer } from './CodeBlockRenderer'
|
|
@@ -144,6 +146,26 @@ export interface UIResourceRendererProps {
|
|
|
144
146
|
*/
|
|
145
147
|
toolbarVariant?: 'hover' | 'always-visible'
|
|
146
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Allow the chart renderer to fall back to the **quickchart.io** image API
|
|
151
|
+
* when the native `chart.js` peer is unavailable (v6.14.0, audit P1.7).
|
|
152
|
+
*
|
|
153
|
+
* The quickchart fallback sends the **entire chart config** (labels + data)
|
|
154
|
+
* to an external service inside an image URL — an implicit network call that
|
|
155
|
+
* can leak potentially sensitive data and behaves differently offline. For a
|
|
156
|
+
* public, LLM/connector-driven package the safe default is **off**: when
|
|
157
|
+
* Chart.js is missing (or `renderer: 'iframe'` is requested) the chart
|
|
158
|
+
* **degrades to a local data table** and emits a `render:error` telemetry
|
|
159
|
+
* signal instead of silently calling out.
|
|
160
|
+
*
|
|
161
|
+
* This is a **host-level** trust decision, deliberately NOT a payload field
|
|
162
|
+
* (a payload could otherwise opt itself in). Set it to `true` only when the
|
|
163
|
+
* data is non-sensitive and an external image render is acceptable.
|
|
164
|
+
*
|
|
165
|
+
* @default false
|
|
166
|
+
*/
|
|
167
|
+
allowQuickchartFallback?: boolean
|
|
168
|
+
|
|
147
169
|
/**
|
|
148
170
|
* Per-instance hook fired when this renderer mounts a content key that
|
|
149
171
|
* is already mounted elsewhere in the document (v6.5.0 — closes Demande 2
|
|
@@ -177,14 +199,36 @@ function ChartRenderer(props: {
|
|
|
177
199
|
component: UIComponent
|
|
178
200
|
onError?: (error: RendererError) => void
|
|
179
201
|
toolbarVariant?: 'hover' | 'always-visible'
|
|
202
|
+
/** Host opt-in for the external quickchart.io fallback (audit P1.7). */
|
|
203
|
+
allowQuickchartFallback?: boolean
|
|
180
204
|
}) {
|
|
181
205
|
const [useNative, setUseNative] = createSignal(false)
|
|
182
206
|
const [iframeUrl, setIframeUrl] = createSignal<string>()
|
|
183
207
|
const [isLoading, setIsLoading] = createSignal(true)
|
|
184
208
|
const [error, setError] = createSignal<string>()
|
|
209
|
+
// Set when Chart.js is unavailable AND the host has not opted into the
|
|
210
|
+
// external quickchart fallback — we then degrade to a local data table
|
|
211
|
+
// rather than calling out to quickchart.io (audit P1.7).
|
|
212
|
+
const [degraded, setDegraded] = createSignal(false)
|
|
213
|
+
const telemetry = useTelemetry()
|
|
185
214
|
|
|
186
215
|
const params = () => props.component.params as any
|
|
187
216
|
const rendererPref = () => params()?.renderer || 'auto'
|
|
217
|
+
const allowQuickchart = () => props.allowQuickchartFallback === true
|
|
218
|
+
|
|
219
|
+
// Emit a clear, observable signal whenever we decline the external fallback.
|
|
220
|
+
const signalBlockedFallback = (reason: string) => {
|
|
221
|
+
setDegraded(true)
|
|
222
|
+
setIsLoading(false)
|
|
223
|
+
const message = `Chart degraded to a data table: ${reason}`
|
|
224
|
+
telemetry?.dispatch({
|
|
225
|
+
type: 'render:error',
|
|
226
|
+
errorMessage: message,
|
|
227
|
+
id: props.component.id ?? '',
|
|
228
|
+
componentType: 'chart',
|
|
229
|
+
ts: Date.now(),
|
|
230
|
+
})
|
|
231
|
+
}
|
|
188
232
|
|
|
189
233
|
// Guard: if data or datasets missing, show error instead of crashing Chart.js
|
|
190
234
|
if (!params()?.data?.datasets) {
|
|
@@ -200,9 +244,17 @@ function ChartRenderer(props: {
|
|
|
200
244
|
const pref = rendererPref()
|
|
201
245
|
|
|
202
246
|
if (pref === 'iframe') {
|
|
203
|
-
//
|
|
247
|
+
// Explicit external render requested. Honor it only when the host has
|
|
248
|
+
// opted in (audit P1.7) — otherwise degrade to a local table.
|
|
204
249
|
setUseNative(false)
|
|
205
|
-
|
|
250
|
+
if (allowQuickchart()) {
|
|
251
|
+
buildIframeUrl()
|
|
252
|
+
} else {
|
|
253
|
+
signalBlockedFallback(
|
|
254
|
+
"renderer: 'iframe' requires the external quickchart.io service; " +
|
|
255
|
+
'set allowQuickchartFallback on the host to enable it.'
|
|
256
|
+
)
|
|
257
|
+
}
|
|
206
258
|
} else if (pref === 'native') {
|
|
207
259
|
// Force native mode - will show error if Chart.js not available
|
|
208
260
|
const available = await isChartJSAvailable()
|
|
@@ -214,14 +266,19 @@ function ChartRenderer(props: {
|
|
|
214
266
|
setIsLoading(false)
|
|
215
267
|
}
|
|
216
268
|
} else {
|
|
217
|
-
// Auto mode - use native if available,
|
|
269
|
+
// Auto mode - use native if available. When Chart.js is missing, only
|
|
270
|
+
// call out to quickchart.io if the host opted in; otherwise degrade to a
|
|
271
|
+
// local data table instead of an implicit external network call (P1.7).
|
|
218
272
|
const available = await isChartJSAvailable()
|
|
219
273
|
if (available) {
|
|
220
274
|
setUseNative(true)
|
|
221
275
|
setIsLoading(false)
|
|
222
|
-
} else {
|
|
276
|
+
} else if (allowQuickchart()) {
|
|
223
277
|
setUseNative(false)
|
|
224
278
|
buildIframeUrl()
|
|
279
|
+
} else {
|
|
280
|
+
setUseNative(false)
|
|
281
|
+
signalBlockedFallback('Chart.js peer is not installed.')
|
|
225
282
|
}
|
|
226
283
|
}
|
|
227
284
|
})
|
|
@@ -256,6 +313,19 @@ function ChartRenderer(props: {
|
|
|
256
313
|
when={useNative()}
|
|
257
314
|
fallback={
|
|
258
315
|
<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">
|
|
316
|
+
{/* P1.7 — Chart.js missing and quickchart fallback not allowed:
|
|
317
|
+
show the chart data as a local table instead of an implicit
|
|
318
|
+
call to quickchart.io. */}
|
|
319
|
+
<Show when={degraded()}>
|
|
320
|
+
<div class="p-3">
|
|
321
|
+
<DegradedFallback
|
|
322
|
+
message="Interactive chart unavailable — install the chart.js peer dependency, or set allowQuickchartFallback to use the external quickchart.io renderer."
|
|
323
|
+
caption="Showing the chart data as a table."
|
|
324
|
+
{...chartToDegradedTable(params() ?? {})}
|
|
325
|
+
/>
|
|
326
|
+
</div>
|
|
327
|
+
</Show>
|
|
328
|
+
|
|
259
329
|
<Show when={isLoading()}>
|
|
260
330
|
<div class="absolute inset-0 flex items-center justify-center">
|
|
261
331
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
|
@@ -1330,6 +1400,8 @@ function ComponentRenderer(props: {
|
|
|
1330
1400
|
onError?: (error: RendererError) => void
|
|
1331
1401
|
errorMode?: ValidationErrorMode
|
|
1332
1402
|
toolbarVariant?: 'hover' | 'always-visible'
|
|
1403
|
+
/** Host opt-in for the external quickchart.io chart fallback (audit P1.7). */
|
|
1404
|
+
allowQuickchartFallback?: boolean
|
|
1333
1405
|
}) {
|
|
1334
1406
|
// Performance marks — visible in Chrome DevTools "Performance" panel under
|
|
1335
1407
|
// user timings. Always-on, SSR-safe (see utils/perf.ts).
|
|
@@ -1458,7 +1530,7 @@ function ComponentRenderer(props: {
|
|
|
1458
1530
|
allowRetry={true}
|
|
1459
1531
|
>
|
|
1460
1532
|
<Show when={props.component.type === 'chart'}>
|
|
1461
|
-
<ChartRenderer component={props.component} onError={props.onError} toolbarVariant={props.toolbarVariant} />
|
|
1533
|
+
<ChartRenderer component={props.component} onError={props.onError} toolbarVariant={props.toolbarVariant} allowQuickchartFallback={props.allowQuickchartFallback} />
|
|
1462
1534
|
</Show>
|
|
1463
1535
|
<Show when={props.component.type === 'table'}>
|
|
1464
1536
|
<TableRenderer component={props.component} onError={props.onError} toolbarVariant={props.toolbarVariant} />
|
|
@@ -1834,7 +1906,7 @@ export const UIResourceRenderer: Component<UIResourceRendererProps> = (props) =>
|
|
|
1834
1906
|
|
|
1835
1907
|
// Wrapper function for RenderContext (breaks circular dependency)
|
|
1836
1908
|
const renderComponent = (component: UIComponent, onError?: (error: RendererError) => void) => (
|
|
1837
|
-
<ComponentRenderer component={component} onError={onError} errorMode={props.errorMode} toolbarVariant={props.toolbarVariant} />
|
|
1909
|
+
<ComponentRenderer component={component} onError={onError} errorMode={props.errorMode} toolbarVariant={props.toolbarVariant} allowQuickchartFallback={props.allowQuickchartFallback} />
|
|
1838
1910
|
)
|
|
1839
1911
|
|
|
1840
1912
|
return (
|
|
@@ -1852,7 +1924,7 @@ export const UIResourceRenderer: Component<UIResourceRendererProps> = (props) =>
|
|
|
1852
1924
|
style={getGridStyleString(component)}
|
|
1853
1925
|
data-mcp-ui-component-id={getUiResourceStableKey(component)}
|
|
1854
1926
|
>
|
|
1855
|
-
<ComponentRenderer component={component} onError={props.onError} errorMode={props.errorMode} toolbarVariant={props.toolbarVariant} />
|
|
1927
|
+
<ComponentRenderer component={component} onError={props.onError} errorMode={props.errorMode} toolbarVariant={props.toolbarVariant} allowQuickchartFallback={props.allowQuickchartFallback} />
|
|
1856
1928
|
</div>
|
|
1857
1929
|
)}
|
|
1858
1930
|
</For>
|