From 1696162d227c9a3cf0d449f65f0310795390ecef Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 24 Oct 2026 23:37:49 +0200 Subject: [PATCH] fix(deps): maplibre-gl 6.15.0 past the attribution sanitizer advisory GHSA-jrc7-96c5-q579 covers every maplibre-gl up to 6.4.0 and has no 5.x release, so npm audit kept flagging the patched 5.24.0. Moving to 6.15.0 retires the local backport: patch-maplibre.mjs and its postinstall, predev and prebuild hooks go, along with the Dockerfile copy and its format-baseline entry. 6.x sanitizes attributions against an allow list. What the major changes for TREK: - ESM only, no default export: the engine module imports the namespace and hands that out as its default, so every caller stays as it was. - The worker is no longer inlined, and inside a bundle MapLibre cannot find it next to its own module. The engine sets it from Vite's ?worker&url build before any map, the Leaflet bridge's included. - WebGL2 only: the probe asks for webgl2 alone, so a WebGL1-only browser gets the raster basemap instead of a GPUInitializationError. - 6.9 shapes right-to-left text itself and deprecated setRTLTextPlugin; the old plugin would replace that and could only run through eval in the module worker, so MapLibre no longer registers it. Mapbox does. The lockfile moves maplibre-gl and its own dependencies only; the Leaflet bridge already allows ^6.0.0 as its peer. --- Dockerfile | 1 - client/package.json | 8 +- client/scripts/format-baseline.json | 1 - client/scripts/patch-maplibre.mjs | 34 ------ .../components/Journey/JourneyMapGL.test.tsx | 5 +- .../Map/MapViewGL.plannerWaypoints.test.tsx | 31 +++-- client/src/components/Map/MapViewGL.test.tsx | 35 +++--- client/src/components/Map/engines/maplibre.ts | 24 +++- .../Map/engines/rtlRegistration.test.ts | 37 +++--- client/src/components/Map/rtlText.ts | 18 +-- .../Settings/MapboxPreview.test.tsx | 4 +- client/src/utils/webgl.test.ts | 10 +- client/src/utils/webgl.ts | 13 +- client/tests/unit/maplibre-sanitizer.test.ts | 24 ++-- package-lock.json | 113 +++++++----------- 15 files changed, 157 insertions(+), 201 deletions(-) delete mode 100644 client/scripts/patch-maplibre.mjs diff --git a/Dockerfile b/Dockerfile index 68c47f0d2..d425e8549 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,6 @@ WORKDIR /app COPY package.json package-lock.json ./ COPY shared/package.json ./shared/ COPY client/package.json ./client/ -COPY client/scripts/patch-maplibre.mjs ./client/scripts/ RUN npm ci --workspace=client COPY --from=shared-builder /app/shared/dist ./shared/dist COPY client/ ./client/ diff --git a/client/package.json b/client/package.json index 12c0bf08e..4e38c0940 100644 --- a/client/package.json +++ b/client/package.json @@ -8,7 +8,7 @@ "type": "module", "scripts": { "dev": "vite", - "prebuild": "node scripts/patch-maplibre.mjs && node scripts/generate-icons.mjs", + "prebuild": "node scripts/generate-icons.mjs", "build": "vite build", "build:analyze": "vite build --mode analyze", "preview": "vite preview", @@ -41,9 +41,7 @@ "shots:promote": "node e2e/screenshots/promote.mjs", "e2e:report": "playwright show-report", "format": "prettier --write \"src/**/*.tsx\" \"src/**/*.css\"", - "format:check": "prettier --check \"src/**/*.tsx\" \"src/**/*.css\"", - "postinstall": "node scripts/patch-maplibre.mjs", - "predev": "node scripts/patch-maplibre.mjs" + "format:check": "prettier --check \"src/**/*.tsx\" \"src/**/*.css\"" }, "dependencies": { "@assistant-ui/react": "0.15.25", @@ -94,7 +92,7 @@ "leaflet": "^1.9.4", "lucide-react": "^0.577.0", "mapbox-gl": "^3.31.0", - "maplibre-gl": "5.24.0", + "maplibre-gl": "6.15.0", "modern-screenshot": "4.7.0", "plyr": "^3.8.4", "react": "^19.3.0", diff --git a/client/scripts/format-baseline.json b/client/scripts/format-baseline.json index 79098a567..5b54a7eeb 100644 --- a/client/scripts/format-baseline.json +++ b/client/scripts/format-baseline.json @@ -55,7 +55,6 @@ "scripts/check-gl-split.mjs", "scripts/check-page-pattern.mjs", "scripts/generate-icons.mjs", - "scripts/patch-maplibre.mjs", "src/App.test.tsx", "src/App.tsx", "src/App.versionHandover.test.tsx", diff --git a/client/scripts/patch-maplibre.mjs b/client/scripts/patch-maplibre.mjs deleted file mode 100644 index d9f99a89d..000000000 --- a/client/scripts/patch-maplibre.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import { readFileSync, writeFileSync } from 'node:fs' -import { createRequire } from 'node:module' -import { dirname, join } from 'node:path' - -const require = createRequire(import.meta.url) -const packagePath = require.resolve('maplibre-gl/package.json') -const { version } = JSON.parse(readFileSync(packagePath, 'utf8')) - -if (version !== '5.24.0') throw new Error('Recheck the MapLibre sanitizer backport before changing its version') - -// Backport maplibre-gl-js#8189 without removing WebGL1 support in version 6. -const loops = [ - ['of elem.attributes)', 'of Array.from(elem.attributes))'], - ['of e.attributes)', 'of Array.from(e.attributes))'], -] - -for (const filename of [ - 'src/util/dom.ts', - 'dist/maplibre-gl.js', - 'dist/maplibre-gl-dev.js', - 'dist/maplibre-gl-csp.js', - 'dist/maplibre-gl-csp-dev.js', -]) { - const target = join(dirname(packagePath), filename) - const code = readFileSync(target, 'utf8') - const loop = loops.find(([before, after]) => code.includes(before) || code.includes(after)) - if (!loop) throw new Error(`MapLibre sanitizer loop not found in ${filename}`) - const [before, after] = loop - if (code.split(before).length === 1 && code.split(after).length === 2) continue - if (code.split(before).length !== 2 || code.includes(after)) { - throw new Error(`Unexpected MapLibre sanitizer in ${filename}`) - } - writeFileSync(target, code.replace(before, after)) -} diff --git a/client/src/components/Journey/JourneyMapGL.test.tsx b/client/src/components/Journey/JourneyMapGL.test.tsx index c984f07bf..c7956ed60 100644 --- a/client/src/components/Journey/JourneyMapGL.test.tsx +++ b/client/src/components/Journey/JourneyMapGL.test.tsx @@ -107,7 +107,8 @@ const glModule = vi.hoisted(() => () => ({ })) vi.mock('mapbox-gl', glModule) -vi.mock('maplibre-gl', glModule) +// maplibre-gl 6 has no default export: the same fakes, as named exports. +vi.mock('maplibre-gl', () => glModule().default) vi.mock('mapbox-gl/dist/mapbox-gl.css', () => ({})) vi.mock('maplibre-gl/dist/maplibre-gl.css', () => ({})) @@ -121,7 +122,7 @@ vi.mock('../Map/mapboxSetup', () => ({ import React from 'react' import mapboxgl from 'mapbox-gl' -import maplibregl from 'maplibre-gl' +import * as maplibregl from 'maplibre-gl' import { render, screen, act } from '../../../tests/helpers/render' import { resetAllStores, seedStore } from '../../../tests/helpers/store' import { useSettingsStore } from '../../store/settingsStore' diff --git a/client/src/components/Map/MapViewGL.plannerWaypoints.test.tsx b/client/src/components/Map/MapViewGL.plannerWaypoints.test.tsx index 7eff8a397..0b183cd10 100644 --- a/client/src/components/Map/MapViewGL.plannerWaypoints.test.tsx +++ b/client/src/components/Map/MapViewGL.plannerWaypoints.test.tsx @@ -1,5 +1,5 @@ import { act } from '@testing-library/react'; -import maplibregl from 'maplibre-gl'; +import * as maplibregl from 'maplibre-gl'; import React from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { render } from '../../../tests/helpers/render'; @@ -164,22 +164,21 @@ vi.mock('mapbox-gl', () => ({ })); vi.mock('mapbox-gl/dist/mapbox-gl.css', () => ({})); +// maplibre-gl 6 has no default export, so the fakes are named exports. vi.mock('maplibre-gl', () => ({ - default: { - Map: vi.fn(function () { - return glMap; - }), - Marker: vi.fn(function (opts?: { element?: HTMLElement }) { - return glMarkers.make(opts); - }), - LngLatBounds: vi.fn(function () { - return glBounds.create(); - }), - NavigationControl: vi.fn(), - Popup: vi.fn(function () { - return glPopup; - }), - }, + Map: vi.fn(function () { + return glMap; + }), + Marker: vi.fn(function (opts?: { element?: HTMLElement }) { + return glMarkers.make(opts); + }), + LngLatBounds: vi.fn(function () { + return glBounds.create(); + }), + NavigationControl: vi.fn(), + Popup: vi.fn(function () { + return glPopup; + }), })); vi.mock('maplibre-gl/dist/maplibre-gl.css', () => ({})); diff --git a/client/src/components/Map/MapViewGL.test.tsx b/client/src/components/Map/MapViewGL.test.tsx index 6a1b4a0ac..ff0f01519 100644 --- a/client/src/components/Map/MapViewGL.test.tsx +++ b/client/src/components/Map/MapViewGL.test.tsx @@ -8,7 +8,7 @@ import { buildPlace } from '../../../tests/helpers/factories' import { server } from '../../../tests/helpers/msw/server' import { useSettingsStore } from '../../store/settingsStore' import { useAuthStore } from '../../store/authStore' -import maplibregl from 'maplibre-gl' +import * as maplibregl from 'maplibre-gl' import { DEFAULT_MAP_ZOOM } from '../../constants/mapDefaults' import { MAP_LAYER_SWITCHER_INSET } from './MapLayerSwitcher' import type { GeoPosition, TrackingMode } from '../../hooks/useGeolocation' @@ -156,22 +156,21 @@ vi.mock('mapbox-gl', () => ({ })) vi.mock('mapbox-gl/dist/mapbox-gl.css', () => ({})) +// maplibre-gl 6 has no default export, so the fakes are named exports. vi.mock('maplibre-gl', () => ({ - default: { - Map: vi.fn(function () { - return glMap - }), - Marker: vi.fn(function (opts?: { element?: HTMLElement }) { - return glMarkers.make(opts) - }), - LngLatBounds: vi.fn(function () { - return glBounds.create() - }), - NavigationControl: vi.fn(), - Popup: vi.fn(function () { - return glPopup - }), - }, + Map: vi.fn(function () { + return glMap + }), + Marker: vi.fn(function (opts?: { element?: HTMLElement }) { + return glMarkers.make(opts) + }), + LngLatBounds: vi.fn(function () { + return glBounds.create() + }), + NavigationControl: vi.fn(), + Popup: vi.fn(function () { + return glPopup + }), })) vi.mock('maplibre-gl/dist/maplibre-gl.css', () => ({})) @@ -352,7 +351,7 @@ describe('MapViewGL', () => { it('FE-COMP-MAPVIEWGL-004: renders with the MapLibre provider and no token', async () => { const mapboxgl = (await import('mapbox-gl')).default - const maplibregl = (await import('maplibre-gl')).default + const maplibregl = await import('maplibre-gl') useSettingsStore.setState({ settings: { ...useSettingsStore.getState().settings, @@ -373,7 +372,7 @@ describe('MapViewGL', () => { it('FE-COMP-MAPVIEWGL-014: MapLibre maps disable the around-center mouse rotate (#1545)', async () => { const mapboxgl = (await import('mapbox-gl')).default - const maplibregl = (await import('maplibre-gl')).default + const maplibregl = await import('maplibre-gl') useSettingsStore.setState({ settings: { ...useSettingsStore.getState().settings, diff --git a/client/src/components/Map/engines/maplibre.ts b/client/src/components/Map/engines/maplibre.ts index d67716a70..0c4a72f3a 100644 --- a/client/src/components/Map/engines/maplibre.ts +++ b/client/src/components/Map/engines/maplibre.ts @@ -1,7 +1,6 @@ -import maplibregl from 'maplibre-gl' +import * as maplibregl from 'maplibre-gl' import 'maplibre-gl/dist/maplibre-gl.css' -import rtlTextPluginUrl from '@mapbox/mapbox-gl-rtl-text/dist/mapbox-gl-rtl-text.js?url' -import { registerRtlTextPlugin } from '../rtlText' +import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url' /** * The only module in the client that pulls maplibre-gl in at runtime. Same rule as @@ -11,9 +10,22 @@ import { registerRtlTextPlugin } from '../rtlText' * Popup, LngLatBounds — which is why the components take the engine as a prop and * stay engine-agnostic. Where the two genuinely differ, the caller already guards * on the provider (projection, accessToken, aroundCenter). + * + * MapLibre 6 ships as ES modules only and has no default export any more, so the + * namespace is what goes out as this module's default. It is a frozen module + * namespace: nothing may assign to it, which is why the accessToken writes stay + * behind their mapbox-only guard. */ -// Arabic/Hebrew/Persian labels come out unjoined and reversed without this. -// MapLibre's signature is (url, lazy) and it resolves a promise — see rtlText.ts. -registerRtlTextPlugin(() => maplibregl.setRTLTextPlugin(rtlTextPluginUrl, true)) + +// MapLibre 6 finds its worker next to its own module, which inside a bundle is a +// chunk in /assets/ with no worker beside it. Vite builds the worker into its own +// file and hands back the URL; set before any Map is constructed, the +// maplibre-gl-leaflet bridge included, since it only ever reaches the engine +// together with this module. +maplibregl.setWorkerUrl(workerUrl) + +// No RTL text plugin here, unlike mapbox. MapLibre 6.9 shapes Arabic and orders +// bidirectional text itself; the old plugin would replace that implementation, and +// a v6 worker can only load the plugin's UMD build through eval. export default maplibregl diff --git a/client/src/components/Map/engines/rtlRegistration.test.ts b/client/src/components/Map/engines/rtlRegistration.test.ts index b2cfb1585..cb46e9c2f 100644 --- a/client/src/components/Map/engines/rtlRegistration.test.ts +++ b/client/src/components/Map/engines/rtlRegistration.test.ts @@ -1,29 +1,33 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; /** - * The two lines this fix actually adds. Everything else is a helper: the guard in - * rtlText.ts has its own tests, but a guard nobody calls looks exactly like a - * working fix from the outside. Deleting either registration, swapping an - * argument or dropping the deferred flag left the whole suite green. + * What each engine module does on import. Everything else is a helper: the guard + * in rtlText.ts has its own tests, but a guard nobody calls looks exactly like a + * working fix from the outside. Deleting the mapbox registration, swapping an + * argument or dropping the deferred flag left the whole suite green, and so would + * a maplibre engine that forgot its worker URL. * * The engines are mocked because importing them for real builds a worker pool * under jsdom, and the plugin URL is mocked so the 133 KB payload never loads. */ -const { mapboxSet, maplibreSet } = vi.hoisted(() => ({ +const { mapboxSet, maplibreSet, maplibreWorker } = vi.hoisted(() => ({ mapboxSet: vi.fn(), maplibreSet: vi.fn(), + maplibreWorker: vi.fn(), })); vi.mock('mapbox-gl', () => ({ default: { setRTLTextPlugin: mapboxSet } })); -vi.mock('maplibre-gl', () => ({ default: { setRTLTextPlugin: maplibreSet } })); +vi.mock('maplibre-gl', () => ({ setRTLTextPlugin: maplibreSet, setWorkerUrl: maplibreWorker })); vi.mock('mapbox-gl/dist/mapbox-gl.css', () => ({})); vi.mock('maplibre-gl/dist/maplibre-gl.css', () => ({})); +vi.mock('maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url', () => ({ default: '/assets/maplibre-worker.js' })); vi.mock('@mapbox/mapbox-gl-rtl-text/dist/mapbox-gl-rtl-text.js?url', () => ({ default: '/assets/rtl-plugin.js' })); beforeEach(() => { vi.resetModules(); mapboxSet.mockReset(); maplibreSet.mockReset(); + maplibreWorker.mockReset(); }); describe('the GL engines register the RTL text plugin (#2235)', () => { @@ -37,13 +41,12 @@ describe('the GL engines register the RTL text plugin (#2235)', () => { expect(mapboxSet).toHaveBeenCalledWith(expect.stringContaining('rtl'), null, true); }); - it('FE-MAP-RTL-002: importing the maplibre engine registers it lazily', async () => { + it('FE-MAP-RTL-002: importing the maplibre engine registers no plugin', async () => { await import('./maplibre'); - expect(maplibreSet).toHaveBeenCalledTimes(1); - // MapLibre 5 takes (url, lazy) and returns a promise, which is the reason - // the call stays with each engine instead of going through one wrapper. - expect(maplibreSet).toHaveBeenCalledWith(expect.stringContaining('rtl'), true); + // MapLibre 6.9 shapes right-to-left text itself. The plugin would replace + // that, and a v6 worker could only run its UMD build through eval. + expect(maplibreSet).not.toHaveBeenCalled(); }); it('FE-MAP-RTL-003: an engine still loads when its registration throws', async () => { @@ -54,13 +57,13 @@ describe('the GL engines register the RTL text plugin (#2235)', () => { expect(engine.default).toBeDefined(); }); - it('FE-MAP-RTL-004: a rejected registration promise does not surface as an unhandled rejection', async () => { - maplibreSet.mockReturnValue(Promise.reject(new Error('already registered'))); - + it('FE-MAP-RTL-004: importing the maplibre engine points it at the bundled worker', async () => { const engine = await import('./maplibre'); - expect(engine.default).toBeDefined(); - // Give the rejection a tick to go unhandled if nothing caught it. - await new Promise((resolve) => setTimeout(resolve, 0)); + // MapLibre 6 looks for its worker next to its own module, which in a bundle + // is a chunk with no worker beside it. Without this no map draws a tile. + expect(maplibreWorker).toHaveBeenCalledTimes(1); + expect(maplibreWorker).toHaveBeenCalledWith('/assets/maplibre-worker.js'); + expect(engine.default.setWorkerUrl).toBe(maplibreWorker); }); }); diff --git a/client/src/components/Map/rtlText.ts b/client/src/components/Map/rtlText.ts index c260772ec..340922b9a 100644 --- a/client/src/components/Map/rtlText.ts +++ b/client/src/components/Map/rtlText.ts @@ -7,16 +7,18 @@ * this because their labels are drawn server-side, which is why it only surfaced * once the default basemap became a vector style. * - * Both engines expose the fix as an optional plugin and neither loads it by - * default. Registering is global to the engine, so it happens once per engine - * module rather than at each map's construction site. + * Mapbox GL exposes the fix as an optional plugin and does not load it by + * default. Registering is global to the engine, so it happens once in the engine + * module rather than at each map's construction site. MapLibre needed the same + * plugin up to version 5; since 6.9 it shapes and reorders the text itself, and + * its engine module registers nothing. * - * The call itself stays with the caller: this is one of the places the two engines - * genuinely differ. MapLibre 5 takes `(url, lazy)` and returns a promise, Mapbox - * GL 3 takes `(url, callback, deferred)` and returns nothing — so each engine - * module passes its own invocation rather than a shared wrapper guessing an arity. + * The call itself stays with the caller: Mapbox GL 3 takes + * `(url, callback, deferred)` and returns nothing, while MapLibre took + * `(url, lazy)` and returned a promise, so the guard below accepts either rather + * than guessing an arity. * - * Deliberately no getRTLTextPluginStatus() pre-check: reading it makes MapLibre + * Deliberately no getRTLTextPluginStatus() pre-check: reading it made MapLibre * build its worker pool, which is an eager side effect for a module that is * imported for its default export, and throws outright under jsdom. Registering * twice is reported — by a throw or a rejected promise depending on the engine — diff --git a/client/src/components/Settings/MapboxPreview.test.tsx b/client/src/components/Settings/MapboxPreview.test.tsx index b028225e6..1b06aa1f1 100644 --- a/client/src/components/Settings/MapboxPreview.test.tsx +++ b/client/src/components/Settings/MapboxPreview.test.tsx @@ -32,7 +32,7 @@ const mapboxModule = vi.hoisted(() => ({ accessToken: '', Map: mapboxCtor })); vi.mock('mapbox-gl', () => ({ default: mapboxModule })); vi.mock('mapbox-gl/dist/mapbox-gl.css', () => ({})); -vi.mock('maplibre-gl', () => ({ default: { Map: maplibreCtor } })); +vi.mock('maplibre-gl', () => ({ Map: maplibreCtor })); vi.mock('maplibre-gl/dist/maplibre-gl.css', () => ({})); vi.mock('../Map/mapboxSetup', () => ({ @@ -45,7 +45,7 @@ vi.mock('../Map/mapboxSetup', () => ({ import { isStandardFamily, supportsCustom3d, addCustom3dBuildings, addTerrainAndSky } from '../Map/mapboxSetup'; import { MAPBOX_DEFAULT_STYLE, OPENFREEMAP_DEFAULT_STYLE } from '../Map/glProviders'; import mapboxgl from 'mapbox-gl'; -import maplibregl from 'maplibre-gl'; +import * as maplibregl from 'maplibre-gl'; import GlMapPreviewWithEngine from './MapboxPreview'; // The engine is a prop now, not a module import — that is what keeps mapbox-gl and diff --git a/client/src/utils/webgl.test.ts b/client/src/utils/webgl.test.ts index c69cecce6..fa8422428 100644 --- a/client/src/utils/webgl.test.ts +++ b/client/src/utils/webgl.test.ts @@ -42,15 +42,15 @@ describe('hasWebGL', () => { expect(hasWebGL()).toBe(false) }) - it('FE-UTIL-WEBGL-003: falls back to webgl1 when only webgl2 is refused', () => { - // Exactly the order maplibre-gl asks in. A stricter probe would black out - // maps that would in fact have drawn. + it('FE-UTIL-WEBGL-003: says no when only webgl1 is on offer', () => { + // MapLibre 6 asks for webgl2 and nothing else. A probe that settled for + // webgl1 would wave the map through to a GPUInitializationError. const { ctx } = fakeContext() const getContext = vi.fn((type: string) => (type === 'webgl2' ? null : ctx)) HTMLCanvasElement.prototype.getContext = getContext as unknown as typeof realGetContext - expect(hasWebGL()).toBe(true) - expect(getContext.mock.calls.map((c) => c[0])).toEqual(['webgl2', 'webgl']) + expect(hasWebGL()).toBe(false) + expect(getContext.mock.calls.map((c) => c[0])).toEqual(['webgl2']) }) it('FE-UTIL-WEBGL-004: gives the probe context straight back', () => { diff --git a/client/src/utils/webgl.ts b/client/src/utils/webgl.ts index 92a0be3b1..5c1e28362 100644 --- a/client/src/utils/webgl.ts +++ b/client/src/utils/webgl.ts @@ -1,5 +1,5 @@ /** - * Whether this browser will hand MapLibre a WebGL context. + * Whether this browser will hand MapLibre a WebGL2 context. * * The default basemap has been a vector style since 4.1.0, and maplibre-gl builds * its GL context inside the Map constructor: no context, no map, and the throw @@ -45,12 +45,13 @@ function probe(): boolean { canvas.width = 1 canvas.height = 1 - // Exactly the order maplibre-gl asks in (Map._setupPainter): webgl2 first, - // webgl1 second. A stricter probe would black out maps that would in fact - // have drawn; a looser one would let the throw through anyway. - const gl = (canvas.getContext('webgl2') ?? canvas.getContext('webgl')) as WebGLRenderingContext | null + // Exactly what maplibre-gl asks for (Map._setupPainter): WebGL2 and nothing + // else, since MapLibre 6 dropped WebGL1. A browser that only offers WebGL1 + // passes a looser probe and then throws GPUInitializationError out of the Map + // constructor, which is the crash this probe exists to prevent. + const gl = canvas.getContext('webgl2') if (!gl) { - console.warn('[basemap] this browser refuses a WebGL context, vector basemaps are unavailable') + console.warn('[basemap] this browser refuses a WebGL2 context, vector basemaps are unavailable') return false } diff --git a/client/tests/unit/maplibre-sanitizer.test.ts b/client/tests/unit/maplibre-sanitizer.test.ts index 2705d05df..0a9d51566 100644 --- a/client/tests/unit/maplibre-sanitizer.test.ts +++ b/client/tests/unit/maplibre-sanitizer.test.ts @@ -1,8 +1,14 @@ import { describe, expect, it } from 'vitest' -import maplibregl from 'maplibre-gl' +import { AttributionControl, type Map } from 'maplibre-gl' +/** + * GHSA-jrc7-96c5-q579: MapLibre's attribution sanitizer let a second dangerous + * attribute through after removing the first, so a tile source's attribution + * could run script. TREK carried a patch for this on 5.x; 6.x replaced the + * sanitizer with an allow list, and these cases hold the upgrade to it. + */ function attribution(html: string, fromSource = false) { - const control = new maplibregl.AttributionControl({ + const control = new AttributionControl({ compact: false, customAttribution: fromSource ? undefined : html, }) @@ -16,17 +22,17 @@ function attribution(html: string, fromSource = false) { on: () => {}, off: () => {}, getCanvasContainer: () => document.createElement('div'), - } as unknown as maplibregl.Map + } as unknown as Map return control.onAdd(map) } -describe('MapLibre attribution sanitizer backport', () => { - it.each([false, true])('removes adjacent event handlers (source: %s)', fromSource => { +describe('MapLibre attribution sanitizer', () => { + it.each([false, true])('leaves no event handler behind (source: %s)', fromSource => { const container = attribution('
Credit
', fromSource) - const details = container.querySelector('.maplibregl-ctrl-attrib-inner details')! - expect(details.hasAttribute('onload')).toBe(false) - expect(details.hasAttribute('ontoggle')).toBe(false) - expect(details.textContent).toBe('Credit') + const inner = container.querySelector('.maplibregl-ctrl-attrib-inner')! + //
is not on the allow list, so it goes with its subtree. + expect(inner.querySelector('details')).toBeNull() + expect(inner.querySelectorAll('[onload], [ontoggle]')).toHaveLength(0) }) it('removes consecutive dangerous URLs and handlers in nested markup', () => { diff --git a/package-lock.json b/package-lock.json index 43d5e139a..182299b1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,6 @@ "client": { "name": "@trek/client", "version": "4.3.4", - "hasInstallScript": true, "dependencies": { "@assistant-ui/react": "0.15.25", "@fontsource/abril-fatface": "^5.3.0", @@ -80,7 +79,7 @@ "leaflet": "^1.9.4", "lucide-react": "^0.577.0", "mapbox-gl": "^3.31.0", - "maplibre-gl": "5.24.0", + "maplibre-gl": "6.15.0", "modern-screenshot": "4.7.0", "plyr": "^3.8.4", "react": "^19.3.0", @@ -4971,44 +4970,35 @@ "license": "ISC" }, "node_modules/@mapbox/tiny-sdf": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz", - "integrity": "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.3.0.tgz", + "integrity": "sha512-gBvcNDBBB66bgQYvnaJ6Jk+VCaMmVq5+IZR2kWPisT88Z8s0kBQt6hfVi8QgRW8Vpg0UWZzBA4ZSyzHHC+VeRA==", "license": "BSD-2-Clause" }, "node_modules/@mapbox/unitbezier": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", - "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-1.0.0.tgz", + "integrity": "sha512-fqd515fjBmANKGGsQ286E2Wvj/XvDFpGzwJxq4CI6jMQue6Oy04uCKp+JWKF00xRTmk6cEu1jPJ9p3xqH8YWqQ==", "license": "BSD-2-Clause" }, "node_modules/@mapbox/vector-tile": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.5.tgz", - "integrity": "sha512-pXj8m7KTsqZt+1jsE0xIpGvqTSbblfkuEJL/NJmNePMtEwxO8V3XMDo9WMSfDeqHvCtBI9Lmt4mGcGR10zecmw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-3.0.0.tgz", + "integrity": "sha512-Qf10S1uIHMk20ri/IVBnpS+esUEkVaR5Hftmz88jTInrpmWgPGJfPe3LVjjlE77trLx8tH6qjTG7uWH9hIq/0Q==", "license": "BSD-3-Clause", "dependencies": { "@mapbox/point-geometry": "~1.1.0", "@types/geojson": "^7946.0.16", - "pbf": "^4.0.2" - } - }, - "node_modules/@mapbox/whoots-js": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", - "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", - "license": "ISC", - "engines": { - "node": ">=6.0.0" + "pbf": "^5.0.0" } }, "node_modules/@maplibre/geojson-vt": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.1.0.tgz", - "integrity": "sha512-2eIY4gZxeKIVOZVNkAMb+5NgXhgsMQpOveTQAvnp53LYqHGJZDidk7Ew0Tged9PThidpbS+NFTh0g4zivhPDzQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.3.0.tgz", + "integrity": "sha512-3Zw2jsU9pIJG3G2CQFbLjWj2MlAHodhD7cSY+15iywUuLmmkgzcKRnueVRZ4AcC/5wOtfhidcJAAzBpoBNs3Xg==", "license": "ISC", "dependencies": { - "kdbush": "^4.0.2" + "kdbush": "^4.1.0" } }, "node_modules/@maplibre/maplibre-gl-leaflet": { @@ -5023,12 +5013,12 @@ } }, "node_modules/@maplibre/maplibre-gl-style-spec": { - "version": "24.10.0", - "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.10.0.tgz", - "integrity": "sha512-lichxSiagMEBBrqHF0trtMQH9RKh+9jUlIJl0qW0QHvt2H/tbvUWdE+ZzI2Jd0/pT7j/iavLonlPu7EQ/ixTOw==", + "version": "26.4.4", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-26.4.4.tgz", + "integrity": "sha512-hG4RZR4PBd/+6A3ODdZ3AnQRF106ZQk1y3bhwDg8bH/A4Oz2tnRUJVVWs9BZa6gXZ1d3UKe89C+Mm+olGBELvA==", "license": "ISC", "dependencies": { - "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.3", "@mapbox/unitbezier": "^1.0.0", "json-stringify-pretty-compact": "^4.0.0", "minimist": "^1.2.8", @@ -5041,16 +5031,10 @@ "gl-style-validate": "dist/gl-style-validate.mjs" } }, - "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/@mapbox/unitbezier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-1.0.0.tgz", - "integrity": "sha512-fqd515fjBmANKGGsQ286E2Wvj/XvDFpGzwJxq4CI6jMQue6Oy04uCKp+JWKF00xRTmk6cEu1jPJ9p3xqH8YWqQ==", - "license": "BSD-2-Clause" - }, "node_modules/@maplibre/mlt": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.12.tgz", - "integrity": "sha512-ZeK5w2TTeHOajcLaEQs1KZXw2V9wIKo1PmThlxlsHoXsQsYlBqLJzPOd6tJHRtGTChUY3DPPmjXRArYVvAbmZw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.3.0.tgz", + "integrity": "sha512-7M2O7ABM80Oi8XPkfkrjKB8mOBIxGoVgkA52Mu4RMWoBvvMdagQ522+WxVYujZsMpqIr3BpzrTbXrm4k8lCzCQ==", "license": "(MIT OR Apache-2.0)", "dependencies": { "@mapbox/point-geometry": "^1.1.0" @@ -5067,18 +5051,6 @@ "pbf": "^5.1.0" } }, - "node_modules/@maplibre/vt-pbf/node_modules/pbf": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/pbf/-/pbf-5.1.0.tgz", - "integrity": "sha512-Wv0yo0+uZepnoNEKsquhar1F18LogB8oeEikIhUXG16udbiXG7JecHGySwoo6kuMgjmbQYzdrTZlO+/K9t8eZg==", - "license": "BSD-3-Clause", - "dependencies": { - "resolve-protobuf-schema": "^2.1.0" - }, - "bin": { - "pbf": "bin/pbf" - } - }, "node_modules/@microsoft/tsdoc": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", @@ -13603,9 +13575,9 @@ } }, "node_modules/earcut": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", - "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.4.tgz", + "integrity": "sha512-FHc6yNpnTORB0tM5kmZ03iD2mDuNakDE2lvf4Hw1XkfxtEHIGVi01B/GqUSwx+NcC1WGjidAfpTXzeALtPtfpg==", "license": "ISC" }, "node_modules/ecdsa-sig-formatter": { @@ -17490,27 +17462,26 @@ ] }, "node_modules/maplibre-gl": { - "version": "5.24.0", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.24.0.tgz", - "integrity": "sha512-ALyFxgtd5R+65UqZ/++lOqwWcC0SNho9c27fYSyLmG7AfnAul2o46F05aDJGPbFU57wos9dgcIySHs0Xe6ia3A==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-6.15.0.tgz", + "integrity": "sha512-kxFetbvUHDOVaAP1CYb226Mdk2QhzIPddqM0xJTFD35wu78Mpd6NM7+H68ALjc6Oa0kAOX5EAdMKyvD2u3qfcg==", "license": "BSD-3-Clause", "dependencies": { - "@mapbox/jsonlint-lines-primitives": "^2.0.2", "@mapbox/point-geometry": "^1.1.0", - "@mapbox/tiny-sdf": "^2.1.0", - "@mapbox/unitbezier": "^0.0.1", - "@mapbox/vector-tile": "^2.0.4", - "@mapbox/whoots-js": "^3.1.0", - "@maplibre/geojson-vt": "^6.1.0", - "@maplibre/maplibre-gl-style-spec": "^24.8.1", - "@maplibre/mlt": "^1.1.8", - "@maplibre/vt-pbf": "^4.3.0", + "@mapbox/tiny-sdf": "^2.3.0", + "@mapbox/unitbezier": "^1.0.0", + "@mapbox/vector-tile": "^3.0.0", + "@maplibre/geojson-vt": "^6.3.0", + "@maplibre/maplibre-gl-style-spec": "^26.4.4", + "@maplibre/mlt": "^1.3.0", + "@maplibre/vt-pbf": "^4.3.2", "@types/geojson": "^7946.0.16", - "earcut": "^3.0.2", + "bidi-js": "^1.1.0", + "earcut": "^3.2.4", "gl-matrix": "^3.4.4", - "kdbush": "^4.0.2", + "kdbush": "^4.1.0", "murmurhash-js": "^1.0.0", - "pbf": "^4.0.1", + "pbf": "^5.1.2", "potpack": "^2.1.0", "quickselect": "^3.0.0", "tinyqueue": "^3.0.0" @@ -19398,9 +19369,9 @@ "license": "MIT" }, "node_modules/pbf": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.2.tgz", - "integrity": "sha512-J0ajxARhZfpUEebxYs1vhMGMuLSXtBe1e+fFPDrf2uA2hgo+UshKfNUWOz92HJNz6/NFEXseQPddnHkTreWRqg==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-5.1.2.tgz", + "integrity": "sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w==", "license": "BSD-3-Clause", "dependencies": { "resolve-protobuf-schema": "^2.1.0"