@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.
@@ -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('&lt;img')
37
+ expect(m.calls.popup).not.toContain('<img')
38
+ expect(m.calls.popup).toContain('&lt;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('&lt;img src=x onerror=alert(1)&gt;')
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
- if (marker.tooltip) m.bindTooltip(marker.tooltip);
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
- if (marker.tooltip) m.bindTooltip(marker.tooltip);
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
- if (marker.tooltip) m.bindTooltip(marker.tooltip);
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
- console.warn('[MCP-UI] Failed to load protomaps-leaflet for PMTiles:', e);
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={