Skip to content

fix: authenticate the CARTO basemaps with the suite key - #12

Merged
tgilbert14 merged 3 commits into
masterfrom
claude/carto-basemap-key
Aug 31, 2026
Merged

fix: authenticate the CARTO basemaps with the suite key#12
tgilbert14 merged 3 commits into
masterfrom
claude/carto-basemap-key

Conversation

@tgilbert14

Copy link
Copy Markdown
Owner

What

CARTO began watermarking unauthenticated basemaps.cartocdn.com raster tiles with "API KEY REQUIRED" on 2026-08-26. The national site-picker renders defaced on first load, and the main map does too whenever a visitor picks the "Light" basemap. The request still returns HTTP 200 with 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() to global.R and routes all three tile call sites through it (server.R:537, :578, :588):

  • With CARTO_BASEMAP_KEY set (Connect Cloud content variable): the exact same Positron tiles as before, keyed, via addTiles()addProviderTiles() cannot carry the key because the bundled CartoDB template has no {apikey} placeholder. Explicit OpenStreetMap + CARTO attribution, as CARTO's terms require.
  • Without the key: falls back to Esri's keyless grey canvas, so the map degrades to a clean basemap rather than a defaced one. Capped at maxNativeZoom = 16, that canvas's real content limit.

ui.R is unchanged. The helper accepts either a leaflet provider name or a CARTO variant, so the Basemap choices vector stays as-is and the Esri Terrain/Satellite options pass straight through to addProviderTiles() exactly as before.

The key is a public, rate-limited identifier, not a credential — it rides in the client-side tile URL by design (CARTO's terms ban server-side proxying). The env var keeps it out of git and makes rotation a Connect setting rather than a release.

Manifest

manifest.json is intentionally untouched in the first commit — AGENTS.md forbids hand-editing it. The first CI run regenerates it in the pinned validator and uploads the validated manifest candidate artifact; the follow-up commit lands exactly those bytes. Expect the first run red at the byte-match gate — that is the designed flow, identical to how Ground Beetle went green.

Before merging (owner)

  1. Connect Cloud → this app's content settings → Variables → add CARTO_BASEMAP_KEY (the cb1_… key). Until it is set, the deployed app shows the clean Esri fallback rather than CARTO.
  2. Merge, then check the live map: picker shows Positron, "Light" shows Positron, no watermark, attribution visible.

Verified

  • The key returns clean tiles where unkeyed requests are watermarked — proven at CDN origin misses (the CDN caches keyed responses separately, so a cached tile is not a valid test).
  • grep -E 'CartoDB[."$]|cartocdn' finds no remaining unkeyed CARTO tile request; the only CartoDB.* strings left are the ui.R choice labels, which now route through the helper.
  • git diff --check clean. No R interpreter in the patch environment, so CI's offline-source step is the syntax gate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01738HeGPEU9NEoczL8RXnu1


Generated by Claude Code

CARTO began watermarking unauthenticated basemaps.cartocdn.com raster
tiles with "API KEY REQUIRED" on 2026-08-26, so the national site-picker
renders defaced on first load, and the main map does too whenever a
visitor selects the "Light" basemap. 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 all three
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.

The helper accepts either a leaflet provider name or a CARTO variant, so
ui.R's Basemap choices vector is unchanged and the Esri Terrain and
Satellite options pass straight through to addProviderTiles() as before.

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.

manifest.json is intentionally not touched in this commit — it is
regenerated in the pinned CI validator per AGENTS.md, and the validated
candidate artifact from that run lands in the follow-up commit.

Copy link
Copy Markdown
Owner Author

CI is red at the manifest byte gate, and this repo exports no artifact for me to shuttle

Failing check: contracts → step 14, "Require committed generated bytes to match"
(run 33229375933)

Everything before it passed — manifest generation, verify_bundle.R, sourcing the complete app offline, and rendering a real bundled site through the Shiny server. So the R change in this PR is sound; the run only stops because manifest.json is deliberately not committed here (it must come from the pinned validator, not from me).

Why I can't complete it the way the siblings did. In Ground Beetle, Plant Diversity, Phenology, Vegetation and Small Mammal, CI uploads the validated manifest unconditionally, so I download that artifact and commit exactly those bytes — that is how Ground Beetle #22 went green and merged. This repo has only:

- name: Upload unvalidated manifest for diagnosis
  if: failure() && steps.manifest.outcome != 'skipped'

That step sits before the byte gate, so when the gate is the only failure nothing has failed yet, the upload is skipped, and the generated manifest is discarded with the runner. I confirmed the run has zero artifacts. I can't regenerate it locally either — there's no R runtime in the patch environment, and the package Built timestamps in the manifest come from the runner's own install.

To unblock, on this branch

Rscript --vanilla scripts/write_manifest.R
git commit -am "build: adopt the validated manifest for the basemap change"

Optional, and worth considering separately

Moving that upload above the byte gate and making it unconditional — as the five sibling repos already do — would let this repo self-serve a validated manifest on exactly this kind of run. I've deliberately not included that here: it's a CI change beyond what this PR is for, and it's your call.

I'll keep watching and pick this up as soon as there's a new commit.


Generated by Claude Code

Claude and others added 2 commits August 31, 2026 15:33
The basemap fix in this PR passed every substantive check — manifest
generation, verify_bundle.R, sourcing the app offline, and rendering a
real bundled site — and stopped only at "Require committed generated
bytes to match", because manifest.json must come from the pinned
validator rather than be written by hand.

There was no way to complete that loop here. The existing upload is
`if: failure() && steps.manifest.outcome != 'skipped'` and sits BEFORE
the byte gate, so when the gate is the only failure nothing has failed
yet, the upload is skipped, and the validated manifest is discarded with
the runner. Five sibling repos (Ground Beetle, Plant Diversity, Plant
Phenology, Vegetation Structure, Small Mammal) already upload theirs
unconditionally, which is exactly how each of them reached green.

Adds the same unconditional upload, matching the Ground Beetle step
verbatim — same pinned action SHA, if-no-files-found: error, three-day
retention. It is additive and placed after the app-render step and
before the gate, so it weakens no check and changes no existing step;
the diagnostic UNVALIDATED upload is left exactly as it was.
manifest.json must come from the pinned R 4.5.0 validator, not from a hand
edit, so this is the artifact that run 33409103282 produced -- copied
verbatim, byte-identical to the uploaded candidate. That run is the first
one to export it: the preceding commit made the upload unconditional so the
bytes survive a run whose only failure is the byte gate itself.

The run was against this branch's exact head (43c8892), and its candidate is
correct on the substance: all 112 file checksums in it match this tree,
including the two the basemap change moves.

  global.R  171cc80c... -> db7dd408...
  server.R  cc5212c5... -> fdb8660d...

Everything else that differs from the previously committed manifest is a
package "Built" timestamp -- 73 of them -- which records when the validator
compiled each source package, not anything about this change. That is the
known non-determinism this repo's byte-exact gate flaps on, and the reason
the bytes have to be taken from the validator rather than reconstructed.
NEON-Driver-Cascade's semantic compare_manifests.R is the fix; promoting it
here is tracked in the suite lessons, not in this PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01738HeGPEU9NEoczL8RXnu1
@tgilbert14
tgilbert14 marked this pull request as ready for review August 31, 2026 16:20
@tgilbert14
tgilbert14 merged commit 550f0bc into master Aug 31, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants