Carto base maps: send the CARTO API key

Since about 28 August 2026 CARTO stamps an "API KEY REQUIRED" watermark into
every basemaps.cartocdn.com tile requested without a key. The request still
returns HTTP 200, so nothing fails — Carto Light and Carto Dark simply started
drawing the watermark over the map.

Both layers now build their tile URL through cartoTileUrl(), which appends
?key=. The parameter name matters: api_key= is silently ignored and returns the
same watermarked tile as no key at all, so a wrong guess looks right until the
map is looked at. Verified by checksum that the URLs the map generates return
the clean tiles for both styles.

The key comes from VITE_CARTO_API_KEY in .env.local, which is git-ignored, so it
is not in the source and rotating it is a configuration change; .env.example
documents the variable. If it is missing, a console warning says so rather than
leaving only a watermark to explain it.

The key is inlined into the built bundle in dist/ and sent with every tile
request, so it is visible to anyone using the application. That is expected for
a tile key; the exposure is quota, not access.

Cached watermarked tiles are not a concern: the Service Worker keys tiles by
full URL including the query string, so keyed URLs are fetched fresh and the old
entries age out. No Service Worker bump — HTML is network-first, so the new
bundle is picked up without one, and a bump would delete every offline tile
cache (flagged separately).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ekke 2026-09-17 14:39:26 +02:00
parent 88f55ade46
commit 486480eda6
8 changed files with 101 additions and 63 deletions

8
.env.example Normal file
View File

@ -0,0 +1,8 @@
# Copy to .env.local (git-ignored) and fill in. Vite reads VITE_-prefixed
# variables at build time and inlines them into the bundle.
# CARTO basemaps (Carto Light, Carto Dark). Since late August 2026 CARTO stamps
# an "API KEY REQUIRED" watermark into every tile requested without a key — the
# request still succeeds, so a missing key shows up only as the watermark.
# Free key: https://carto.com/basemaps/apikey
VITE_CARTO_API_KEY=

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
dist/assets/index-BzlrAJOS.js.map vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dist/index.html vendored
View File

@ -1997,7 +1997,7 @@
}
</style>
<script type="module" crossorigin src="/assets/index-D5My3hAw.js"></script>
<script type="module" crossorigin src="/assets/index-BzlrAJOS.js"></script>
<link rel="modulepreload" crossorigin href="/assets/openlayers-D5-VSKzq.js">
<link rel="modulepreload" crossorigin href="/assets/pako-Xa-UToif.js">
<link rel="modulepreload" crossorigin href="/assets/geotiff-B9aROX49.js">

View File

@ -105,6 +105,29 @@ import '../styles/layerswitcher.css';
* and every button is given an icon from here instead, which makes this the single
* place the toolbar's iconography is defined.
*/
/**
* CARTO basemap key, used by the Carto Light and Carto Dark base maps.
*
* Since late August 2026 CARTO stamps an "API KEY REQUIRED" watermark into
* every tile requested without a key. The request still returns HTTP 200, so a
* missing or mistyped key never shows up as an error — only as the watermark on
* the map. The parameter is `key`; `api_key` is silently ignored and produces
* the same watermarked tiles.
*
* Supplied through VITE_CARTO_API_KEY in .env.local (git-ignored, see
* .env.example), so rotating it is a configuration change. It is inlined into
* the built bundle and sent in every tile URL, so it is visible to anyone using
* the application — normal for a tile key, which is why it is kept separate from
* credentials that must stay private.
*/
const CARTO_API_KEY = import.meta.env.VITE_CARTO_API_KEY || '';
/** Tile URL template for a CARTO raster style, with the key when there is one. */
function cartoTileUrl(style) {
const url = `https://{a-c}.basemaps.cartocdn.com/${style}/{z}/{x}/{y}.png`;
return CARTO_API_KEY ? `${url}?key=${encodeURIComponent(CARTO_API_KEY)}` : url;
}
/**
* Resting offset of the info / analysis popup above its anchor. Dragging the
* popup by its title bar moves it from here; reopening returns it here.
@ -3080,13 +3103,20 @@ export class MapView {
});
topoLayer.set('basemapKey', 'topo');
if (!CARTO_API_KEY) {
console.warn(
'[MapView] VITE_CARTO_API_KEY is not set — Carto Light and Carto Dark tiles '
+ 'will carry an "API KEY REQUIRED" watermark. Add it to .env.local and rebuild.'
);
}
const cartoLightLayer = new TileLayer({
title: 'Carto Light',
type: 'base',
zIndex: -100,
visible: defaultBasemap === 'carto-light',
source: new XYZ({
url: 'https://{a-c}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png',
url: cartoTileUrl('light_all'),
attributions: '© CARTO',
maxZoom: 19,
crossOrigin: 'anonymous',
@ -3100,7 +3130,7 @@ export class MapView {
zIndex: -100,
visible: defaultBasemap === 'carto-dark',
source: new XYZ({
url: 'https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png',
url: cartoTileUrl('dark_all'),
attributions: '© CARTO',
maxZoom: 19,
crossOrigin: 'anonymous',