From 8ce5291b7cfa2a041d68bdce51792e75ec87940a Mon Sep 17 00:00:00 2001 From: efiten Date: Tue, 1 Sep 2026 11:50:45 +0200 Subject: [PATCH] fix(map): apply the CARTO key on every map surface (rebase of #1916 onto #1919) (#1926) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continues #1916 by @nullrouten0. The commit is theirs, authorship unchanged; I rebased it onto master and resolved the fallout from #1919. Opening it here rather than force-pushing to someone else's branch. ## Why this is needed after #1919 #1919 shipped the Carto key, but only for the five `BASE_STYLES` entries in `map-tile-providers.js`. Four map surfaces still request unkeyed tiles and get them back stamped `API KEY REQUIRED` at HTTP 200: - `public/roles.js` — `getTileUrl()` returns the bare `TILE_LIGHT` constant in light mode and never consults the registry (the dark branch has consulted it since #1461). Affects `analytics.js:2200` and `nodes.js:94`, so the analytics map and the node-detail map stay watermarked in light theme even with a key configured. - `public/customize-v2.js:1838` and `:2047` — the two geo-filter maps in the customizer. - `public/geofilter-builder.html` — standalone page, outside the SPA, so it fetches `/api/config/client` itself. @nullrouten0 had already found and fixed all four, plus written the docs, before #1919 was merged. Their diagnosis of the light-mode branch is in the PR verbatim. ## What I changed while rebasing 1. **`carto.token` → `carto.key`.** #1919 shipped `key` and operators already have it in their configs; renaming now would break them silently. All of #1916's code, tests and docs follow suit. 2. **Dropped the duplicate key getter.** `_getCartoKeyParam` did the same job as master's `_getCartoKey`; kept master's. 3. **Merged the two test suites.** Master's five cases plus four of #1916's that master does not cover: the `api_key`-is-ignored assertion, lazy resolution after async config, and both `MC_tileUrlById` cases. 42 passed, 0 failed. 4. **Corrected one factual claim in the docs.** #1916 stated CARTO offers no referrer or origin restriction. CARTO does ask for a domain when the key is issued. Whether that is enforced per request is not something I verified, so the note now says exactly that rather than asserting either way. 5. Merged the two config comments, keeping the operationally useful part: unkeyed tiles return **200** with a watermark, so nothing errors and no healthcheck fires. Verify by looking at a tile, not at a status code. ## Verification Rebased onto `7aa60c03`. No regressions: | | base 7aa60c03 | this branch | |---|---|---| | `test-issue-1420-tile-providers.js` | 38 passed, 0 failed | **42 passed, 0 failed** | | `test-issue-1614-tile-url-function.js` | 3 passed, 0 failed | 3 passed, 0 failed | | `test-issue-1470-node-tile-helper.js` | 3 passed, 3 failed | 3 passed, 3 failed (pre-existing) | | `test-frontend-helpers.js` | 625 passed, 2 failed | 625 passed, 2 failed (pre-existing) | Also verified end to end on a live instance running this change with a real key: the tile that came back carried the `API KEY REQUIRED` watermark before and came back clean after. @nullrouten0 — this is your work and I would rather you had the credit and the merge. Say the word and I will close this and hand the rebase back to you, or push it to your branch instead if you prefer #1916 to stay the vehicle. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Wzwr3eXseyNM7Xj598djjE Co-authored-by: nullrouten Co-authored-by: Claude Opus 5 (1M context) --- config.example.json | 2 +- docs/deployment.md | 38 +++++++++++++++++++++++++++++++ public/customize-v2.js | 8 +++++-- public/geofilter-builder.html | 18 ++++++++++++++- public/map-tile-providers.js | 13 +++++++++++ public/roles.js | 9 ++++++++ test-issue-1420-tile-providers.js | 35 ++++++++++++++++++++++++++++ 7 files changed, 119 insertions(+), 4 deletions(-) diff --git a/config.example.json b/config.example.json index c5de20a0..1a094dbe 100644 --- a/config.example.json +++ b/config.example.json @@ -78,7 +78,7 @@ "darkDefault": "carto-dark", "lightDefault": "carto-light", "providers": { - "_comment_carto": "Carto is the default provider. Since 2026-08 Carto requires an API key - without one the tiles come back stamped 'API KEY REQUIRED'. Get a free key at https://carto.com/basemaps/apikey and put it in 'key'. WARNING: the key is sent to the browser; restrict it by domain in the Carto dashboard. Optional: specify 'domain' for Carto enterprise (e.g. 'mycompany' for 'https://{s}.mycompany.cartocdn.com').", + "_comment_carto": "Carto is the default provider. Since 2026-08 CARTO requires an API key on its raster basemaps: without one every tile is served stamped 'API KEY REQUIRED' with a 200 status, so nothing errors and no healthcheck fires - verify by looking at a tile, not at a status code. Get a free key (no account, no card, 5M tile requests/month) at https://carto.com/basemaps/apikey and put it in 'key'. The query parameter is 'key'; 'api_key' is silently ignored and still serves the watermarked tile. WARNING: the key is sent to the browser; restrict it by domain in the CARTO dashboard. Optional: specify 'domain' for Carto enterprise (e.g. 'mycompany' for 'https://{s}.mycompany.cartocdn.com').", "carto": { "enabled": true, "key": "", diff --git a/docs/deployment.md b/docs/deployment.md index a7c15a0c..211ad025 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -188,6 +188,44 @@ See `config.example.json` in the repository for all available options including: Map tile providers are enabled and configured via the `config.json` file. You can provide your custom API credentials (e.g. `osm_url`, `stamen_api_key`, `mapbox_api_key`) to activate external tile services. Once configured on the server, users can select their preferred tile provider from the Customizer UI on the client, and their choice will be persisted automatically. +#### CARTO now requires an API key + +CARTO's raster basemaps (`carto-dark`, `carto-light`, `carto-voyager`, +`carto-voyager-dark`, `positron-dark`) require an API key. Without one, tiles +are still returned with a **200 status** but every one of them is stamped +`API KEY REQUIRED` — so nothing errors, no healthcheck fires, and the map just +quietly looks broken. + +Get a free key at — no CARTO account, no +credit card, emailed back immediately, 5 million tile requests per calendar +month across raster and vector. Then set it as `key`: + +```json +"map": { + "tiles": { + "providers": { + "carto": { "enabled": true, "key": "YOUR_CARTO_KEY" } + } + } +} +``` + +Notes: + +- The query parameter is `key`. **`api_key` is silently ignored** and still + serves the watermarked tile. +- The key is sent to the browser, so treat it as public. CARTO asks for a domain + when the key is issued, but whether that restriction is enforced on every + request has not been verified here. Assume anyone can read the key from + devtools and spend your quota. The 5M/month fair-use ceiling makes that + low-impact for most deployments, but it is worth knowing. +- A wrong or expired key fails the same silent way as no key at all. Verify by + **looking at a tile**, not by checking for a 200. +- CARTO has said it is considering freezing data updates to the raster + basemaps. If you would rather not depend on them, set + `"carto": { "enabled": false }` and use another provider — `esri` needs no + key at all, and `osm`/`stamen` accept their own tokens. + --- ## MQTT Setup diff --git a/public/customize-v2.js b/public/customize-v2.js index d1d844cf..71c73853 100644 --- a/public/customize-v2.js +++ b/public/customize-v2.js @@ -1835,7 +1835,9 @@ var modalClosingLine = null; _gfModalMap = L.map(mapDiv, { zoomControl: true }); - L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { + L.tileLayer(window.MC_tileUrlById + ? window.MC_tileUrlById('carto-light', 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png') + : 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap © CartoDB', maxZoom: 19 }).addTo(_gfModalMap); @@ -2044,7 +2046,9 @@ if (!mapEl || typeof L === 'undefined') return; _gfMap = L.map(mapEl, { zoomControl: false, dragging: false, scrollWheelZoom: false, doubleClickZoom: false, touchZoom: false }); - L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { + L.tileLayer(window.MC_tileUrlById + ? window.MC_tileUrlById('carto-light', 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png') + : 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap © CartoDB', maxZoom: 19 }).addTo(_gfMap); diff --git a/public/geofilter-builder.html b/public/geofilter-builder.html index 2a55ca84..3ea8e54f 100644 --- a/public/geofilter-builder.html +++ b/public/geofilter-builder.html @@ -85,14 +85,30 @@ +