fix: authenticate the CARTO basemap with the suite key - #10
Conversation
CARTO began watermarking unauthenticated basemaps.cartocdn.com raster
tiles with "API KEY REQUIRED" on 2026-08-26, so the national picker on
the splash renders defaced on first load — the only map in this app, and
the first thing every visitor sees. The tile request still returns HTTP
200 with a valid PNG, so nothing errors or logs; full suite diagnosis in
NEON-Driver-Cascade docs/SUITE-BASEMAP-INCIDENT-2026-08.md.
Adds an add_suite_basemap() helper to global.R and routes both tile call
sites through it. With CARTO_BASEMAP_KEY set (a Connect Cloud content
variable) it serves the exact same Positron tiles as before, keyed, via
addTiles() — addProviderTiles() cannot carry the key because the bundled
CartoDB template has no {apikey} placeholder. The key is a public
rate-limited identifier, not a credential: it rides in the client-side
tile URL by design, and the env var exists to keep it out of git and make
rotation a Connect setting rather than a release.
Note this app used the object form leaflet::providers$CartoDB.Positron
rather than a quoted provider name, so a search for the quoted string
misses it entirely; the suite-wide sweep is
grep -rn --include='*.R' -E 'CartoDB[."$]|cartocdn'.
Without the key it falls back to Esri's keyless grey canvas so the map
degrades to a clean basemap rather than a defaced one, capped at that
canvas's real maxNativeZoom of 16.
SOURCE ONLY — NOT YET MERGEABLE. Editing global.R and server.R
invalidates two generated authority files: manifest.json and
release/production-identity.json, whose runtime_payload_sha256 hashes
global.R, ui.R and server.R. AGENTS.md forbids hand-editing or casually
regenerating them, this repo has no CI to produce them, and no R runtime
was available in the patch environment. They must be regenerated in the
clean validator before this merges.
CI ran after all, and it failed exactly where the PR description predictedFailing check: identity_paths=(manifest.json release/production-identity.json docs/release.json)
git diff --exit-code -- "${identity_paths[@]}"I'd said this repo has no CI because it has no Two things the run positively establishes
To unblock, on this branchRscript --vanilla scripts/build_cross_site.R
Rscript --vanilla scripts/build_search_index.R
INV_MANIFEST_PHASE=prestamp Rscript --vanilla scripts/write_manifest.R
INV_RELEASE_IDENTITY_MODE=write INV_WRITE_PAGES_RELEASE=1 Rscript --vanilla scripts/write_release_identity.R
INV_MANIFEST_PHASE=final Rscript --vanilla scripts/write_manifest.R
INV_RELEASE_IDENTITY_MODE=verify INV_WRITE_PAGES_RELEASE=1 Rscript --vanilla scripts/write_release_identity.R
cmp -s release/production-identity.json docs/release.json
git commit -am "build: regenerate the manifest and release identity for the basemap change"That is the validator's own sequence, lifted verbatim from the two steps above. The prestamp → identity → final-manifest → verify ordering matters: the identity binds the manifest contract, and the final manifest then records the identity file. What I deliberately did not do
The same helper is merged, deployed and confirmed live in Ground Beetle #22, and is green in Plant Diversity, Small Mammal, Vegetation Structure and Water Chemistry. I'll pick this up as soon as there's a new commit. Generated by Claude Code |
The generated authority for this PR's source, produced by the pinned validator rather than by hand, per AGENTS.md. Source: refresh-data.yml run 33409207678 (workflow_dispatch, skip_download), whose four jobs all succeeded against this branch's head 96e6ae7 -- the offline source/science/producer contracts, an independent rebuild of the derived data and preliminary manifest, the exact Pages and Connect release identity, the Living Poster contract, and the full candidate verification. Its publish job wrote the validated tree to automation/invert-data-refresh (cc322fd). These three files are taken verbatim from that branch. Every other file was byte-compared first: automation/invert-data-refresh differs from this branch in exactly manifest.json, release/production-identity.json and docs/release.json, and this tree is now byte-identical to it. runtime_payload_sha256 moves because it hashes global.R, ui.R and server.R, two of which this PR edits; manifest_contract_sha256 and release_id follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01738HeGPEU9NEoczL8RXnu1
Status: complete and validated ✅
This PR opened source-only, because editing
global.Randserver.Rinvalidates two generated authority files thatAGENTS.mdforbids hand-editing:manifest.jsonrelease/production-identity.json— itsruntime_payload_sha256hashesglobal.R,ui.Randserver.RBoth are now regenerated through the sanctioned path, and CI is green.
What
CARTO began watermarking unauthenticated
basemaps.cartocdn.comraster tiles with "API KEY REQUIRED" on 2026-08-26. The national picker on the splash renders defaced on first load — it is the only map in this app, and the first thing every visitor sees. The request still returnsHTTP 200with a valid PNG, so nothing errored or logged.Full suite diagnosis:
docs/SUITE-BASEMAP-INCIDENT-2026-08.md· tgilbert14/NEON-Driver-Cascade#70. Proven first in Ground Beetle #22 — merged, deployed, live map confirmed correct.How
Adds
add_suite_basemap()toglobal.Rand routes both tile call sites through it (server.R:940,:963):CARTO_BASEMAP_KEYset (Connect Cloud content variable): the exact same Positron tiles as before, keyed, viaaddTiles()—addProviderTiles()cannot carry the key because the bundled CartoDB template has no{apikey}placeholder. Explicit OpenStreetMap + CARTO attribution, as CARTO's terms require.maxNativeZoom = 16, that canvas's real content limit.Worth knowing for future sweeps
This app used the object form
leaflet::providers$CartoDB.Positron, not a quoted provider name — so a search for"CartoDBmisses it entirely and would have left this app fully watermarked. The suite-wide sweep is:How the authority was regenerated
refresh-data.ymlwas dispatched on this branch withskip_download=true. All four jobs succeeded against head96e6ae7: the offline source/science/producer contracts, an independent rebuild of the derived data and preliminary manifest, the exact Pages and Connect release identity, the Living Poster contract, and the full candidate verification. Itspublish_candidatejob wrote the validated tree toautomation/invert-data-refresh(cc322fd).The three authority files were taken verbatim from that branch. Before committing, every other file was byte-compared:
automation/invert-data-refreshdiffered from this branch in exactly those three paths, so all 34data/sites/*.rds,data/release_contract.rds, both receipts,data/site_index.rds,data/cross_site.rds,data/search_index.rdsanddata-sample/demo.rdsare byte-identical. No scientific bytes moved.Verified
gate✅ ·produce_candidate✅ ·validate_candidate✅ — including the "Reject a stale committed identity on pull requests" step that failed before the regeneration.grep -E 'addProviderTiles\(|providers\$'outside the helper: zero remaining matches.Before merging (owner)
Connect Cloud → this app's content settings → Variables → add
CARTO_BASEMAP_KEY(thecb1_…key). Until it is set, the deployed app shows the clean Esri fallback rather than CARTO — correct, but visibly greyer than Ground Beetle.