mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-17 04:04:22 +00:00
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 <nullrouten@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
nullrouten
Claude Opus 5
parent
7aa60c0350
commit
8ce5291b7c
+1
-1
@@ -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": "",
|
||||
|
||||
@@ -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 <https://carto.com/basemaps/apikey> — 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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -85,14 +85,30 @@
|
||||
</div>
|
||||
|
||||
<script src="geofilter-draft.js"></script>
|
||||
<script src="map-tile-providers.js"></script>
|
||||
<script>
|
||||
const map = L.map('map').setView([50.5, 4.4], 8);
|
||||
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
const DEFAULT_DARK_TILES = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png';
|
||||
const tiles = L.tileLayer(DEFAULT_DARK_TILES, {
|
||||
attribution: '© OpenStreetMap © CartoDB',
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
// This page is standalone (not the SPA), so it loads the server's map config
|
||||
// itself and re-resolves the tile URL through the shared registry. Without
|
||||
// this, CARTO serves every tile stamped 'API KEY REQUIRED' — with a 200.
|
||||
fetch('/api/config/client')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (cfg) {
|
||||
window.MC_MAP_CFG = (cfg && cfg.map) || { tiles: { providers: {} } };
|
||||
if (typeof window.MC_initTileRegistry === 'function') window.MC_initTileRegistry();
|
||||
if (typeof window.MC_tileUrlById === 'function') {
|
||||
tiles.setUrl(window.MC_tileUrlById('carto-dark', DEFAULT_DARK_TILES));
|
||||
}
|
||||
})
|
||||
.catch(function () { /* keep the unkeyed default */ });
|
||||
|
||||
let points = [];
|
||||
let markers = [];
|
||||
let polygon = null;
|
||||
|
||||
@@ -192,6 +192,19 @@
|
||||
window.MC_setServerDefaultLightTileProvider = setServerDefaultLight;
|
||||
window.MC_applyTileFilter = applyTileFilter;
|
||||
|
||||
/**
|
||||
* Resolve a registry id to a concrete tile-URL string, invoking function-typed
|
||||
* `url` builders so callers never hand L.tileLayer a function (#1614). Returns
|
||||
* `fallback` when the id isn't in the active registry (e.g. provider disabled).
|
||||
* Lets non-map pages request a specific style without duplicating the URL.
|
||||
*/
|
||||
window.MC_tileUrlById = function (id, fallback) {
|
||||
var p = _hasId(id) ? REGISTRY[id] : null;
|
||||
var u = p && (p.url || p.baseUrl);
|
||||
if (!u) return fallback;
|
||||
return (typeof u === 'function') ? u() : u;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Build and attach a Leaflet layer control listing "Auto (follows theme)"
|
||||
|
||||
@@ -587,6 +587,15 @@
|
||||
if (cfg.map.tiles.lightUrl) window.TILE_LIGHT = cfg.map.tiles.lightUrl;
|
||||
}
|
||||
if (typeof window.MC_initTileRegistry === 'function') window.MC_initTileRegistry(true);
|
||||
// CARTO raster needs an API key now, so the bare TILE_DARK/TILE_LIGHT
|
||||
// fallbacks have to pick it up as well — getTileUrl() returns TILE_LIGHT
|
||||
// directly in light mode and never consults the registry. Explicit
|
||||
// darkUrl/lightUrl overrides above still win.
|
||||
if (typeof window.MC_tileUrlById === 'function') {
|
||||
var _tOv = (cfg.tiles || (cfg.map && cfg.map.tiles) || {});
|
||||
if (!_tOv.dark && !_tOv.darkUrl) window.TILE_DARK = window.MC_tileUrlById('carto-dark', window.TILE_DARK);
|
||||
if (!_tOv.light && !_tOv.lightUrl) window.TILE_LIGHT = window.MC_tileUrlById('carto-light', window.TILE_LIGHT);
|
||||
}
|
||||
if (cfg.snrThresholds) Object.assign(SNR_THRESHOLDS, cfg.snrThresholds);
|
||||
if (cfg.distThresholds) Object.assign(DIST_THRESHOLDS, cfg.distThresholds);
|
||||
if (cfg.maxHopDist != null) window.MAX_HOP_DIST = cfg.maxHopDist;
|
||||
|
||||
@@ -564,6 +564,41 @@ test('Carto key coexists with the enterprise domain option', () => {
|
||||
assert.ok(u.endsWith('?key=abc123'), 'key lost: ' + u);
|
||||
});
|
||||
|
||||
test('Carto uses ?key= and never ?api_key= (CARTO ignores api_key and still watermarks)', () => {
|
||||
const ctx = makeSandbox();
|
||||
loadProviders(ctx, { tiles: { providers: { carto: { enabled: true, key: 'abc123' } } } });
|
||||
const url = ctx.window.MC_TILE_PROVIDERS['carto-dark'].url();
|
||||
assert.ok(url.endsWith('?key=abc123'), 'should end with ?key=abc123: ' + url);
|
||||
assert.ok(url.indexOf('api_key') < 0, 'must not use the api_key param: ' + url);
|
||||
});
|
||||
|
||||
test('Carto key resolves lazily - set after load, picked up on re-init', () => {
|
||||
const ctx = makeSandbox();
|
||||
loadProviders(ctx, { tiles: { providers: { carto: { enabled: true } } } });
|
||||
assert.ok(ctx.window.MC_TILE_PROVIDERS['carto-dark'].url().indexOf('key=') < 0, 'no key before config');
|
||||
ctx.window.MC_MAP_CFG = { tiles: { providers: { carto: { enabled: true, key: 'late' } } } };
|
||||
ctx.window.MC_initTileRegistry(true);
|
||||
assert.ok(ctx.window.MC_TILE_PROVIDERS['carto-dark'].url().endsWith('?key=late'), 'key picked up after async config');
|
||||
});
|
||||
|
||||
// --- MC_tileUrlById -------------------------------------------------------
|
||||
|
||||
test('MC_tileUrlById resolves a function-typed url to a string', () => {
|
||||
const ctx = makeSandbox();
|
||||
loadProviders(ctx, { tiles: { providers: { carto: { enabled: true, key: 'k' } } } });
|
||||
const url = ctx.window.MC_tileUrlById('carto-light', 'FALLBACK');
|
||||
assert.strictEqual(typeof url, 'string', 'must return a string, not a function');
|
||||
assert.ok(url.endsWith('?key=k'), 'carries the key: ' + url);
|
||||
});
|
||||
|
||||
test('MC_tileUrlById returns the fallback for a disabled or unknown provider', () => {
|
||||
const ctx = makeSandbox();
|
||||
loadProviders(ctx, { tiles: { providers: { carto: { enabled: false } } } });
|
||||
assert.strictEqual(ctx.window.MC_tileUrlById('carto-dark', 'FALLBACK'), 'FALLBACK', 'disabled provider falls back');
|
||||
assert.strictEqual(ctx.window.MC_tileUrlById('nope', 'FALLBACK'), 'FALLBACK', 'unknown id falls back');
|
||||
});
|
||||
|
||||
|
||||
process.on('beforeExit', () => {
|
||||
console.log('');
|
||||
console.log(' ' + passed + ' passed, ' + failed + ' failed');
|
||||
|
||||
Reference in New Issue
Block a user