maplibre-gl@5 and maplibre-gl-geocoder@1 were floating ranges, and no runtime loader set `integrity`,
so a CDN could serve any bytes to every partner's visitors unnoticed (CRITIC-5).
- library/constants/cdn.js is the one registry: mapbox-gl 3.24.0, maplibre-gl 5.24.0 (the release the
@5 range already resolved to), both geocoders (1.9.4 / 5.0.0) and d3 7.9.0, each with its sha384.
Every host answers Access-Control-Allow-Origin: *, so integrity + crossorigin work cross-origin.
- ensureEngineScript / ensureEngineCSS, the geocoder loaders (library and PPLR) and the generator's
static MapLibre tags set integrity + crossorigin from it; the embed snippet's engine preload matches.
- check:cdn-pins (gates:post): no floating CDN url in shipped code, and every one is in the registry;
--online re-fetches each and compares the hash (all 9 match today).
MapLibre stays on 5.x knowingly. GHSA-jrc7-96c5-q579 (DOM.sanitize, reached through a source's
attribution string) affects <= 6.4.0, and 6.x is ESM-only (no UMD global), so the upgrade is a loader
migration of its own. The attribution strings rendered today come from our configured basemap styles
and our own sources; the note in cdn.js says who is exposed until then.
Typing into the research-map's search bar inside a composite (the Simple
Research dashboard) wiped the box instantly and never queried. Standalone, the
same map searched fine — which is the shape of the bug, not a coincidence.
mapbox-gl-geocoder unwraps exactly ONE shadow level to find the input:
var e = t.target && t.target.shadowRoot ? t.target.shadowRoot.activeElement : t.target
if (!(e ? e.value : "")) { this.fresh = !0, 9 !== t.keyCode && this.clear(t), ... }
Its handler is debounced, so it reads event.target after dispatch has ended, by
which time the target is the OUTERMOST host. One hop is therefore exactly right
for a map one custom element deep and exactly wrong for a map inside another
one. Measured, same build, same keystroke:
doxa-research-map (1 deep) target=DOXA-RESEARCH-MAP -> unwrap = the input -> _geocode("Bhu")
research-map-plus-grid (2) target=RESEARCH-MAP-PLUS-GRID -> unwrap = <doxa-research-map> -> clear()
An element with no .value reads as an empty box, so the vendor cleared what you
had just typed. Walk the activeElement chain to the bottom instead of one step
and hand the vendor an event whose target is that element; nesting deeper cannot
break it again, and at one level it resolves to the same input as before.
Two things worth recording. The receiver is passed explicitly: the Mapbox build
binds this method in its constructor and the MapLibre build does not, and an
unbound call landed on `this === undefined`, which swallowed the keystroke AND
the query ("Cannot read properties of undefined (reading
'_hideGeolocateButton')") — green-looking text with no search behind it. And
neither suspect on the card was the cause: the clear came from inside the vendor
bundle, not from _geoBeingCleared, and SearchBar is not involved.
MEASURED on the built tree, both engines' mapbox mode, typing "Bhutan":
fix off leaf ok (1 geocoding request) / composite value="" and 0 requests
fix on both keep "Bhutan", both fire the query, both list 19 results
including People Cluster, People Group + Country, Country and Place
Screenshot read, not just the log. gates:pre 7/7, gates:post 34/34.
"the element gets the tk to the element itself and does not need to only
gatekeep that to one component only — all components in a plugin can share
that tk" (Driver, 2026-09-23).
Inside ONE element that was already true: ProfileLoader provides
'mapboxToken' and eleven components inject it. The gate is the ELEMENT
BOUNDARY — every custom element mounts its own app with its own
ProfileLoader, which re-derives the token from its own profile-config, so a
composite that nests a real map hands the child nothing.
THE EXISTING FALLBACK COULD NOT FIRE. The ladder already read
window.MAP_TOKEN. MEASURED: that global IS set on the composite leaf and
the child still had no search bar — because the ladder lives in a
computed() and window.MAP_TOKEN is a plain global, not a reactive source.
The composite leaf FETCHES its token, so the computed resolved '' first,
cached, and never re-ran. useSharedMapToken makes it a ref that updates on
publish, and assignments to window.MAP_TOKEN are adopted through an
accessor, so already-generated pages keep working untouched. Storage lives
on window and changes travel as an event: each bundle is its own IIFE, so a
module-level ref is NOT shared between a composite and the bundle it nests.
THAT WAS NECESSARY AND NOT SUFFICIENT, which only measuring showed. With
the token shared and reactive the bar was STILL absent. Real cause:
GeocoderComponent's onMounted hit `typeof MapboxGeocoder === 'undefined'`
and RETURNED SILENTLY — no throw, no console error, no DOM. MapboxGeocoder
is "preloaded via index.html", true for a LEAF and false for a COMPOSITE
leaf, which generate-tree classifies as token-free while it goes on to nest
a real map. Measured, mapbox-gl-geocoder references per page: research-map
leaf 2, research-map-plus-grid leaf 0.
So the script is now fetched on demand when a host did not preload it,
reusing the exact shape the maplibre path already had (one loader, two
callers) and pinned to the same v5.0.0 the leaves preload. It resolves on
error as well as load, so a blocked CDN leaves the map usable and the bar
absent rather than hanging. This fixes every already-generated page without
regenerating any of them.
MEASURED on the built tree:
leaf mapbox search@56 legend@104
leaf maplibre search@NONE legend@58 (unchanged: maplibre stays bar-free)
composite search@56 legend@104 (was NONE)
library/ reaches every bundle, so all 22 rebuilt: 22/22, 7/7 pre, 18/18
post, and a browser sweep of five StackLayout consumers plus all seven
studio dashboards.
- The Mapbox Map instance is unwrapped (markRaw) at the profile boundary and the layers
composable works on the raw map, so Mapbox internals no longer pay Vue's get/set traps on
every frame; the geocoder control instance is a shallowRef for the same reason
- Prayer glow rings animate at 12 steps/s with paint transitions disabled, constants moved to
addLayer, and the loop pauses while the map is off-screen — the map can idle again
- New gate: internal/long-frames-probe.mjs measures long animation frames, blocking time and
DevTools rAF violations per phase (boot / idle / interaction); idle violations on the prayer
and research maps dropped from ~100 to the no-animation baseline
Standalone repo that builds embeddable JAMstack map bundles (web components),
composes them into a lazy-loaded SPA via layouts/, and optionally wraps a
Capacitor Android/iOS app — one build, choose your output/CDN. Diátaxis docs.
History reset to a single clean commit; no PII, IPs, or internal terms.