Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ The browser build proxies the sidecar at `/sidecar` (same-origin, no CORS); conf
- `MAX_VECTOR_BYTES` (`packages/plugins/src/plugins/remote-file-formats.ts`) mirrors `MAX_REMOTE_FILE_BYTES`, an **internal, unexported** constant in `maplibre-gl-vector` (2 GiB — DuckDB-WASM holds remote file sizes in 32 bits). It cannot be imported, so whenever `maplibre-gl-vector` is bumped (in `packages/plugins/package.json`) — including Dependabot PRs — re-check `src/lib/utils/remote.ts` in that package and update the mirror if it moved. If it drifts, the remote-browse panels (Source Cooperative, Hugging Face) silently block GeoParquet the engine could now open, or offer an Add that is certain to fail. Updating the constant is enough: the limit the user is shown is rendered from it, not written into the copy. `remote-file-formats.ts` is the **single** home for this and the other format/reader/size rules those panels share — a per-panel copy would miss this check, so add new browse panels against that module rather than duplicating it (`source-coop-api.ts` re-exports it under its own names for compatibility).
- `MAP_PANEL_SELECTOR` (`apps/geolibre-desktop/src/components/layout/RecordVideoDialog.tsx`) mirrors the **rendered** control class names from `maplibre-gl-components` — `maplibre-gl-html-control`, `maplibre-gl-legend`, `maplibre-gl-colorbar` — so the Record Video "Include map panels" option can rasterize those on-map overlays into the recording. These are the display elements, deliberately **not** the `*-gui-control` authoring editors. The classes are internal and unexported, so whenever `maplibre-gl-components` is bumped (in `packages/plugins/package.json`) — including Dependabot PRs — re-check them against the rendered controls and update the selector if they moved. If a class drifts, the option silently stops burning that panel into the video (or the checkbox never appears) with no build error.
- `GLOBE_CONTROL_TOGGLE_SELECTOR` (`packages/map/src/globe-control-toggle.ts`) mirrors the class names MapLibre's own `GlobeControl` puts on its toggle button — `maplibregl-ctrl-globe` and `maplibregl-ctrl-globe-enabled`, swapped on every projection change. `MapCanvas` persists a projection change from a **click** on that button rather than from the `projectiontransition` event, because style initialization and project reconciliation emit that event too and a stale one overwrites the projection of a project that has just loaded. The classes are internal and unexported, so whenever `maplibre-gl` is bumped (including Dependabot PRs) run the frontend suite — `tests/globe-control-toggle.test.ts` builds a real `GlobeControl` and fails if the mirror stops matching. Without that check a renamed class silently stops persisting the user's projection, with no build error.
- `GeoLibreCogRenderEngine` (`packages/plugins/src/types.ts`) mirrors the `RenderEngine` union `maplibre-gl-raster` exports (`maplibre-gl-raster` | `cog-tiler-wasm` | `titiler`). It is hand-written rather than imported because `types.ts` is the public plugin-API surface and importing there would make that package's types a hard dependency of every external plugin. Unlike the mirrors above this one is checked by the **compiler**, not a test: `CogRenderEngineMirrorIsExact` in `packages/plugins/src/plugins/maplibre-raster.ts` asserts both directions of assignability against the real imported type, so a renamed or dropped engine identifier fails `npm run typecheck`. Nothing extra to do on a `maplibre-gl-raster` bump beyond letting the build run; without it a stale identifier would reach `control.setEngine()` as a string the control no longer recognizes, silently leaving the raster unrendered.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scope note (low confidence / non-blocking): This GeoLibreCogRenderEngine mirror-documentation line, and the corresponding CogRenderEngineMirrorIsExact type-level assertion added in packages/plugins/src/plugins/maplibre-raster.ts, don't appear related to this PR's stated purpose (dropping the Chrome extension's broad host permissions). The addition itself looks correct and harmless, but if it landed here by accident (e.g. picked up from a rebase or stray commit) it may be worth splitting into its own PR so the Chrome-extension change stays easy to review and revert independently.

- `propertySpecFor` (`packages/core/src/expressions.ts`) fabricates the **unexported** `StylePropertySpecification` shape that `@maplibre/maplibre-gl-style-spec`'s `createExpression` uses for expected-result-type enforcement (the Expression Builder's filter → boolean / color checks). The cast hides any contract change from the compiler, so whenever `@maplibre/maplibre-gl-style-spec` is bumped (including Dependabot PRs) run the frontend suite — the "enforces an expected result type" test in `tests/expressions.test.ts` fails if the shape stops being honored.
- `DISTANCE_SEGMENTS` / `NON_DISTANCE_NAMES` (`apps/geolibre-desktop/src/lib/whitebox-distance-params.ts`) decide, by parameter *name*, which Whitebox parameters are ground distances and so get the Processing dialog's metric unit picker (GeoLibre#1540). The segments are generic (`tolerance`, `radius`, `length`, `resolution`), so a tool can carry a matching name that is not a length — `corridor_tolerance` is a 0-1 fraction. Those are safe today only because the picker is confined to tools whose every dataset input is a vector layer, and the colliding names happen to sit on imagery/LiDAR tools; that is a coincidence, not a guarantee. So whenever `geolibre-wasm` is bumped (in `packages/processing/package.json`) — including Dependabot PRs — scan the new catalog for a `double` matching the rule whose description reads as a fraction, ratio, angle or weight, and add it to `NON_DISTANCE_NAMES`. If one is missed, that tool's field offers metres and silently converts a dimensionless number as if it were a distance, with no build error.
- UI strings are translatable via **react-i18next**; catalogs live in `apps/geolibre-desktop/src/i18n/locales/*.json` (`en.json` is the source of truth, typed by `i18next.d.ts`). Use `t()` for new user-facing strings; a `?locale`/`?lang` query param sets the embed language. The UI mirrors for right-to-left locales (Arabic), so style new components with Tailwind's logical utilities (`ms-`/`me-`/`ps-`/`pe-`/`text-start`/`border-s`/`start-`…), not the physical `ml-`/`left-` forms. See `docs/i18n.md`.
Expand Down
12 changes: 9 additions & 3 deletions apps/geolibre-desktop/src/lib/data-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,18 @@ export function serviceUrlParameter(search: string): ServiceUrlParameter | null
parsed && kind && TILE_TEMPLATE_KINDS.has(kind)
? parsed.replace(/%7B/gi, "{").replace(/%7D/gi, "}")
: parsed;
if (!kind || !SERVICE_KINDS.has(kind) || !url) return null;
const styleUrl = httpUrl(params.get("serviceStyle"));
if (!kind || !SERVICE_KINDS.has(kind)) return null;
// A vector tileset whose style names its tiles inline is addable from that
// style alone, since the source layers and the tile template both come out of
// the same document. So a link carrying only a style is complete, and the
// dialog opens with an empty tileset field rather than not opening at all.
if (!url && !(kind === "ogc-vector-tiles" && styleUrl)) return null;
return {
kind,
url,
url: url ?? "",
layer: params.get("serviceLayer")?.trim() || null,
styleUrl: httpUrl(params.get("serviceStyle")),
styleUrl,
};
}
export interface RemoteGeoJsonLayer {
Expand Down
2 changes: 1 addition & 1 deletion docs/user-guide/chrome-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ The extension recognizes GeoJSON and spatial JSON, GeoParquet and Parquet, PMTil

GeoLibre fetches selected links directly, so the source server must allow cross-origin requests (CORS). Complete HTTP(S) URLs, including signed query parameters, are forwarded to GeoLibre. Cookies and other browser-session credentials are not forwarded, so cookie-bound or session-authenticated links might fail. Temporary `blob:` URLs cannot be transferred.

The extension requests access only to the active tab when you open it. It does not store browsing history, send analytics, or run continuously in the background.
The extension requests access only to the active tab, and only from the moment you click its icon. It holds no standing permission to any website, stores nothing, sends no analytics, and runs nothing in the background. Map services are recognized by reading back the addresses of the requests the page has already made, which the page records for itself, rather than by watching your browsing.
22 changes: 12 additions & 10 deletions extensions/geolibre-chrome/PRIVACY.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
# Privacy policy — Open data in GeoLibre

Last updated: August 16, 2026
Last updated: August 18, 2026

Open data in GeoLibre does not independently collect, retain, or sell personal
information, page contents, or usage analytics. It does forward the complete
dataset, service, and style URLs that the user explicitly selects,
as described below.

The extension uses Chrome's `activeTab` permission to inspect links and
structured metadata on the current page only after the user clicks the
extension's toolbar icon. The extension also observes completed HTTP(S)
requests locally to identify geospatial services used by interactive maps. It
does not inspect response bodies. Detected service URLs are held in Chrome's
in-memory session storage and removed when their tab closes.
The extension uses Chrome's `activeTab` permission to inspect the current page
only after the user clicks the extension's toolbar icon. In that moment it reads
the page's links and structured metadata, and reads back the addresses of the
requests the page has already made, from the Resource Timing record each
document keeps of its own loading, to identify geospatial services used by
interactive maps. It does not inspect response bodies, and it does not observe
the network or run in the background at any other time. Nothing is stored: the
list exists only while the popup is open and is discarded when it closes.

When the user chooses **Open in GeoLibre**, the complete selected HTTP(S)
dataset and style URLs are placed in the query string of a new
Expand All @@ -25,8 +27,7 @@ The navigation request exposes its URL and the user's IP address to GeoLibre's
web-hosting infrastructure, where standard service logs may retain them. The
navigation may also appear in browser history. See the current
[GeoLibre privacy policy](https://geolibre.app/privacy/) for the service's data
practices. The extension itself does not persist URLs beyond the browser tab's
session.
practices. The extension itself never persists a URL.

The extension does not fetch or upload the datasets. GeoLibre requests them
directly from their original servers, subject to those servers' privacy
Expand All @@ -35,7 +36,8 @@ from the source page are not forwarded, although credentials embedded directly
in a selected URL are part of the URL and are forwarded.

The extension uses no remote code, advertising, analytics, tracking pixels,
cookies, accounts, or persistent extension storage.
cookies, accounts, or extension storage of any kind. It holds no host
permissions and no permission to observe browsing.

Questions may be submitted through the GeoLibre repository:
<https://github.com/opengeos/GeoLibre/issues>.
73 changes: 55 additions & 18 deletions extensions/geolibre-chrome/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Open data in GeoLibre

A Manifest V3 Chrome extension that finds supported geospatial dataset links on
the current page, observes geospatial service requests made by interactive maps,
and opens selected data in GeoLibre.
the current page, reads back the geospatial service requests its interactive
maps have already made, and opens selected data in GeoLibre.

## Install

Expand All @@ -17,10 +17,10 @@ The published extension is on the Chrome Web Store:
3. Choose **Load unpacked**.
4. Select this `extensions/geolibre-chrome` directory.

The extension scans document links after you click its toolbar icon. It also
observes completed HTTP(S) requests locally so it can recognize services used
by interactive web maps. Detected service URLs remain only in session storage
for the lifetime of their tab.
Everything happens after you click the toolbar icon: the extension scans the
document's links, and reads each frame's Resource Timing buffer to recognize the
services its maps requested. It holds no permission beyond `activeTab` and
`scripting`, runs no background service worker, and stores nothing.

## Package for the Chrome Web Store

Expand Down Expand Up @@ -49,10 +49,46 @@ virtualized, canonicalizes links to `data.source.coop`, and removes duplicate
page/download links. The popup can filter discovered files by vector or raster
type without changing the current selection.

The request watcher recognizes WMS, WMTS, WFS, OGC API Features, ArcGIS Feature
Services, XYZ/TMS image tiles, and PBF/MVT vector tiles. Tile requests are
collapsed into reusable `{z}/{x}/{y}` templates, and repeated requests from the
same service appear once.
## Services

A map fetches its tiles and service documents from JavaScript, so they are never
links in the page. What the extension reads instead is `performance
.getEntriesByType("resource")`, the record of its own requests that every
document keeps, collected from the top frame and each frame below it. Recognized
are WMS, WMTS, WFS, OGC API Features, ArcGIS Feature Services, XYZ/TMS image
tiles, and PBF/MVT vector tiles. Tile requests are collapsed into reusable
`{z}/{x}/{y}` templates, and repeated requests from the same service appear once.

Two consequences of reading the buffer rather than watching the network:

- **Worker requests are invisible.** MapLibre and similar renderers fetch vector
tiles from a web worker, which records them in the worker's own timeline, not
the document's. Such a tileset is recovered from the metadata the main thread
*did* fetch: its TileJSON, or failing that the style document, which GeoLibre
can resolve a layer from on its own. A style is only offered in its own right
when no tileset from its origin was found.

A TileJSON is recognized by name (`tile.json`, `tiles.json`, `tilejson.json`),
which is narrower than every way a server can name one: tileserver-gl serves
`/data/<id>.json`, and that tileset is reached through its style rather than
its metadata. Nothing reads the response, so a TileJSON describing *raster*
tiles is indistinguishable from a vector one and is offered as a vector
tileset. Such a false positive cannot become a layer: Add Data resolves the
document on submit and refuses it when no source layers come out. Reading the
body here would mean fetching a cross-origin URL, which needs the host
permissions this design exists to avoid.

A style stands as a candidate of its own only when its path names it one:
`…/style.json`, `…/styles.json`, or an ArcGIS `…/resources/styles/<name>.json`.
The looser `…/styles/<name>.json` is an ordinary theme or configuration route
as well, so a style matched that way is still trusted to explain a tileset
found at its origin, but never offered on its own, where a page's theme file
would appear as a layer.
- **The buffer is finite.** It holds 250 entries per document by default and
stops recording once full. A map's own early requests are normally well inside
that, but a very busy page can lose a service added late. Raising the limit
needs a `document_start` script, which needs the broad host permissions this
design exists to avoid, so the cap is accepted.

A service endpoint on its own is rarely enough to add a layer, so each result
also carries what the page asked that service *for*: the WMS `LAYERS` value, the
Expand All @@ -70,7 +106,7 @@ After loading or reloading the unpacked extension, open one of these websites in
a new tab, let its map finish drawing, then open the extension and confirm it
lists the expected service. Selecting the result opens the matching GeoLibre Add
Data dialog with the service URL filled in. Each row below is a live third-party
map, so what it detects is what a real page hands the request watcher.
map, so what it detects is what a real page actually requests.

| Service | Website | Detected service | Layer carried over |
| --- | --- | --- | --- |
Expand All @@ -80,24 +116,25 @@ map, so what it detects is what a real page hands the request watcher.
| OGC API Features | [pygeoapi lakes collection](https://demo.pygeoapi.io/master/collections/lakes/items?f=html) | `https://demo.pygeoapi.io/master/collections/lakes/items` | — |
| ArcGIS Feature Service | [OpenLayers "Vector ESRI" example](https://openlayers.org/en/latest/examples/vector-esri.html) | The `…/FeatureServer/0` layer URL | `0` |
| XYZ raster tiles | [openstreetmap.org](https://www.openstreetmap.org/) | `https://tile.openstreetmap.org/{z}/{x}/{y}.png` | — |
| Vector tiles, in an `iframe` | [MapLibre "Display a map" example](https://maplibre.org/maplibre-gl-js/docs/examples/display-a-map/) | `https://demotiles.maplibre.org/tiles/{z}/{x}/{y}.pbf` | style `…/style.json` |
| Vector tiles, in an `iframe` | [MapLibre "Display a map" example](https://maplibre.org/maplibre-gl-js/docs/examples/display-a-map/) | `https://demotiles.maplibre.org/tiles/tiles.json` | style `…/style.json` |

Each row above adds a layer that draws, with no further typing: that is the bar
for this table. A row that opens the dialog but leaves a required field empty is
a bug, not an expected extra step.

The MapLibre row is worth keeping in the set: the map runs inside an `iframe`, so
it covers services a page reaches only through an embedded frame. It also carries
a style whose glyph ranges are served as `.pbf`; those are fonts, not a tileset,
and must not be offered.
it covers services a page reaches only through an embedded frame, and it renders
through a worker, so it covers the tileset recovered from its TileJSON rather
than from a tile request. It also carries a style whose glyph ranges are served
as `.pbf`; those are fonts, not a tileset, and must not be offered.

Opening a service URL directly is detected too — the response is the page, so
[a WMS GetCapabilities document](https://ows.terrestris.de/osm/service?SERVICE=WMS&REQUEST=GetCapabilities)
lists `https://ows.terrestris.de/osm/service`.

Navigating the same tab elsewhere replaces the list, so a page with no services
(`https://example.com`) must come up empty rather than inheriting the page before
it.
A document's buffer is its own and is discarded when the tab navigates, so a page
with no services (`https://example.com`) comes up empty rather than inheriting
the page before it.

After selecting a result, add the layer in GeoLibre and verify that the browser
console does not report a CORS error while fetching the service.
Expand Down
Loading
Loading