@seed-ship/mcp-ui-solid 6.10.0 → 6.11.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 +24 -0
- package/dist/components/MapRenderer.cjs +69 -35
- package/dist/components/MapRenderer.cjs.map +1 -1
- package/dist/components/MapRenderer.d.ts +5 -0
- package/dist/components/MapRenderer.d.ts.map +1 -1
- package/dist/components/MapRenderer.js +70 -36
- package/dist/components/MapRenderer.js.map +1 -1
- package/dist/mcp-ui-spec/dist/schemas.cjs +2 -1
- package/dist/mcp-ui-spec/dist/schemas.cjs.map +1 -1
- package/dist/mcp-ui-spec/dist/schemas.js +2 -1
- package/dist/mcp-ui-spec/dist/schemas.js.map +1 -1
- package/package.json +1 -1
- package/src/components/MapRenderer.markers.test.ts +68 -0
- package/src/components/MapRenderer.tsx +36 -8
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v6.11.0 — map hardening.
|
|
3
|
+
*
|
|
4
|
+
* Covers the marker tooltip/popup escaping wiring (audit P1.2 completion: the
|
|
5
|
+
* `bindMarkerContent` helper shipped in v6.10.0 but the marker loops still
|
|
6
|
+
* bound raw HTML until v6.11.0) and the `popupSafeText` contract reused by the
|
|
7
|
+
* PMTiles-failure path (P1.3).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from 'vitest'
|
|
11
|
+
import { bindMarkerContent, popupSafeText } from './MapRenderer'
|
|
12
|
+
|
|
13
|
+
const XSS = '<img src=x onerror=alert(1)>'
|
|
14
|
+
|
|
15
|
+
// Minimal Leaflet marker stub recording what gets bound.
|
|
16
|
+
function fakeMarker() {
|
|
17
|
+
const calls: { tooltip?: string; popup?: string } = {}
|
|
18
|
+
return {
|
|
19
|
+
calls,
|
|
20
|
+
bindTooltip(html: string) {
|
|
21
|
+
calls.tooltip = html
|
|
22
|
+
return this
|
|
23
|
+
},
|
|
24
|
+
bindPopup(html: string) {
|
|
25
|
+
calls.popup = html
|
|
26
|
+
return this
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('bindMarkerContent (audit P1.2 wiring)', () => {
|
|
32
|
+
it('escapes marker tooltip/popup by default (untrusted host)', () => {
|
|
33
|
+
const m = fakeMarker()
|
|
34
|
+
bindMarkerContent(m, { tooltip: XSS, popup: XSS }, false)
|
|
35
|
+
expect(m.calls.tooltip).not.toContain('<img')
|
|
36
|
+
expect(m.calls.tooltip).toContain('<img')
|
|
37
|
+
expect(m.calls.popup).not.toContain('<img')
|
|
38
|
+
expect(m.calls.popup).toContain('<img')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('passes raw HTML through only when the host opts in (allowHtml=true)', () => {
|
|
42
|
+
const m = fakeMarker()
|
|
43
|
+
bindMarkerContent(m, { tooltip: XSS, popup: XSS }, true)
|
|
44
|
+
expect(m.calls.tooltip).toBe(XSS)
|
|
45
|
+
expect(m.calls.popup).toBe(XSS)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('binds nothing when tooltip/popup are absent', () => {
|
|
49
|
+
const m = fakeMarker()
|
|
50
|
+
bindMarkerContent(m, {}, false)
|
|
51
|
+
expect(m.calls.tooltip).toBeUndefined()
|
|
52
|
+
expect(m.calls.popup).toBeUndefined()
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('popupSafeText', () => {
|
|
57
|
+
it('escapes by default', () => {
|
|
58
|
+
expect(popupSafeText(XSS)).toBe('<img src=x onerror=alert(1)>')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('is identity when the host trusts the payload', () => {
|
|
62
|
+
expect(popupSafeText(XSS, true)).toBe(XSS)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('returns undefined for missing values', () => {
|
|
66
|
+
expect(popupSafeText(undefined)).toBeUndefined()
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -192,7 +192,7 @@ export function popupSafeText(value: string | undefined, allowHtml = false): str
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
/** Bind a marker's tooltip + popup, escaping by default (see popupSafeText). */
|
|
195
|
-
function bindMarkerContent(
|
|
195
|
+
export function bindMarkerContent(
|
|
196
196
|
m: any,
|
|
197
197
|
marker: { tooltip?: string; popup?: string },
|
|
198
198
|
allowHtml: boolean
|
|
@@ -276,6 +276,10 @@ export const MapRenderer: Component<MapRendererProps> = (props) => {
|
|
|
276
276
|
let mapInstance: any = null;
|
|
277
277
|
const [isLeafletLoaded, setIsLeafletLoaded] = createSignal(false);
|
|
278
278
|
const [error, setError] = createSignal<string | null>(null);
|
|
279
|
+
// PMTiles is an optional overlay on top of the base map. When it fails we
|
|
280
|
+
// keep the base map but surface a visible notice (audit P1.3) instead of a
|
|
281
|
+
// silent console.warn that leaves the user with an unexplained empty layer.
|
|
282
|
+
const [pmtilesError, setPmtilesError] = createSignal<string | null>(null);
|
|
279
283
|
const isExpanded = useExpanded();
|
|
280
284
|
const telemetry = useTelemetry();
|
|
281
285
|
// Host-level trust for raw HTML in marker/popup content (audit P1.2).
|
|
@@ -390,8 +394,7 @@ export const MapRenderer: Component<MapRendererProps> = (props) => {
|
|
|
390
394
|
});
|
|
391
395
|
p?.markers?.forEach((marker) => {
|
|
392
396
|
const m = L.marker(marker.position);
|
|
393
|
-
|
|
394
|
-
if (marker.popup) m.bindPopup(marker.popup);
|
|
397
|
+
bindMarkerContent(m, marker, allowHtml());
|
|
395
398
|
clusterGroup.addLayer(m);
|
|
396
399
|
markers.push(m);
|
|
397
400
|
});
|
|
@@ -399,16 +402,14 @@ export const MapRenderer: Component<MapRendererProps> = (props) => {
|
|
|
399
402
|
} catch {
|
|
400
403
|
p?.markers?.forEach((marker) => {
|
|
401
404
|
const m = L.marker(marker.position).addTo(mapInstance);
|
|
402
|
-
|
|
403
|
-
if (marker.popup) m.bindPopup(marker.popup);
|
|
405
|
+
bindMarkerContent(m, marker, allowHtml());
|
|
404
406
|
markers.push(m);
|
|
405
407
|
});
|
|
406
408
|
}
|
|
407
409
|
} else {
|
|
408
410
|
p?.markers?.forEach((marker) => {
|
|
409
411
|
const m = L.marker(marker.position).addTo(mapInstance);
|
|
410
|
-
|
|
411
|
-
if (marker.popup) m.bindPopup(marker.popup);
|
|
412
|
+
bindMarkerContent(m, marker, allowHtml());
|
|
412
413
|
markers.push(m);
|
|
413
414
|
});
|
|
414
415
|
}
|
|
@@ -452,6 +453,7 @@ export const MapRenderer: Component<MapRendererProps> = (props) => {
|
|
|
452
453
|
}
|
|
453
454
|
|
|
454
455
|
// ─── PMTiles (v3.1.0) ────────────────────────
|
|
456
|
+
setPmtilesError(null);
|
|
455
457
|
if (p?.pmtiles) {
|
|
456
458
|
try {
|
|
457
459
|
// @ts-ignore — optional peer dependency, may not be installed
|
|
@@ -495,7 +497,23 @@ export const MapRenderer: Component<MapRendererProps> = (props) => {
|
|
|
495
497
|
|
|
496
498
|
pmLayer.addTo(mapInstance);
|
|
497
499
|
} catch (e) {
|
|
498
|
-
|
|
500
|
+
// P1.3 — do NOT fail silently. Keep the base map, but surface a
|
|
501
|
+
// visible notice and report a renderer error via telemetry. The
|
|
502
|
+
// most common cause is the optional `protomaps-leaflet` peer not
|
|
503
|
+
// being installed.
|
|
504
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
505
|
+
const message =
|
|
506
|
+
'PMTiles layer unavailable — the optional "protomaps-leaflet" ' +
|
|
507
|
+
'peer dependency failed to load or render.';
|
|
508
|
+
console.warn('[MCP-UI] ' + message, e);
|
|
509
|
+
setPmtilesError(message);
|
|
510
|
+
telemetry?.dispatch({
|
|
511
|
+
type: 'render:error',
|
|
512
|
+
errorMessage: `${message} (${detail})`,
|
|
513
|
+
id: props.component?.id ?? '',
|
|
514
|
+
componentType: 'map',
|
|
515
|
+
ts: Date.now(),
|
|
516
|
+
});
|
|
499
517
|
}
|
|
500
518
|
}
|
|
501
519
|
|
|
@@ -557,6 +575,16 @@ export const MapRenderer: Component<MapRendererProps> = (props) => {
|
|
|
557
575
|
</div>
|
|
558
576
|
</Show>
|
|
559
577
|
<Show when={!error()}>
|
|
578
|
+
{/* P1.3 — visible notice when the PMTiles overlay fails but the
|
|
579
|
+
base map is still usable. */}
|
|
580
|
+
<Show when={pmtilesError()}>
|
|
581
|
+
<div
|
|
582
|
+
role="alert"
|
|
583
|
+
class="m-2 rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-800 dark:border-amber-700/50 dark:bg-amber-900/20 dark:text-amber-200"
|
|
584
|
+
>
|
|
585
|
+
{pmtilesError()} The base map is still shown.
|
|
586
|
+
</div>
|
|
587
|
+
</Show>
|
|
560
588
|
<div
|
|
561
589
|
ref={mapContainer}
|
|
562
590
|
style={
|