bake the public Mapbox pk so every map works out of the box

Driver 2026-09-21: 'all the maps need to have their mapbox token wired
properly according to the references - all the maps are not working yet until
that fix', then 'all maps can use the pk from the api, no problem - if they
want to set a mapbox sk they can'.

Every leaf already implemented a 6-rung token ladder; rungs 1-3 are used
directly with NO network call. On nuxtbase.app rungs 4-6 all failed: the
same-origin /api/maps/token 404s and the absolute doxa.life fallback sends ZERO
CORS headers. Rungs 1-3 were unset, hence 'No Mapbox access token provided' at
dashboard-plus-grid.js:605.

Setting build-config.token (rung 3) inlines the key into all 26 leaves, so a
map works on ANY host with no endpoint and no CORS.

This required splitting the hygiene rule, which lumped pk with sk/tk. A pk. is
a PUBLIC key: designed to ship in client code and protected by URL
restrictions, not secrecy (measured - the same pk. serves tiles 200 with a
doxa.life Referer and 403s with nuxtbase.app). Flagging it made rung 3
unbuildable, since that gate runs inside build:unlocked. sk. and tk. remain
flagged and were negative-tested: sk CAUGHT, tk CAUGHT, GitHub PAT CAUGHT, pk
allowed.

Verified: full build ALL GUARDS PASS, hygiene clean, SRI consistent across 58
leaf pages; 26 of 26 leaves carry the baked pk; and three maps mount with NO
token supplied by the harness - 0 'no token' errors.
This commit is contained in:
doxa-maps-88655c70 2026-09-21 22:47:43 +00:00
parent 1e30c678a9
commit 3542a5bfc7
2 changed files with 23 additions and 5 deletions

View file

@ -72,8 +72,15 @@ const LEAK = [
// passed as "clean — 0 findings".
// Each entry identifies a KNOWN credential FORMAT rather than guessing from entropy: an
// entropy rule over this tree would fire on every SRI hash and base64 image and be ignored.
// ★ pk. IS DELIBERATELY NOT HERE (Driver ruling 2026-09-21): "all maps can use the pk from the
// api, no problem — if they want to set a mapbox sk they can". A Mapbox `pk.` is a PUBLIC key: it
// is designed to ship in client code and is protected by URL RESTRICTIONS, not by secrecy
// (measured: the same pk. serves tiles 200 with a doxa.life Referer and 403s with nuxtbase.app).
// Flagging it made the token ladder's rung-3 baked token UNBUILDABLE — this gate runs inside
// build:unlocked, so `pk.` in public/ failed the build that is supposed to produce it.
// `sk.` (SECRET key) and `tk.` (temporary) stay flagged: those are real credentials.
const SECRET = [
[/\b(pk|sk|tk)\.eyJ[A-Za-z0-9_-]{20,}/, 'JWT-shaped Mapbox token'],
[/\b(sk|tk)\.eyJ[A-Za-z0-9_-]{20,}/, 'JWT-shaped Mapbox SECRET/temporary token (sk./tk.)'],
[/\bgh[pousr]_[A-Za-z0-9]{36,}/, 'GitHub personal-access token'],
[/\bxox[abprs]-[A-Za-z0-9-]{10,}/, 'Slack token'],
[/\bAKIA[0-9A-Z]{16}\b/, 'AWS access-key id'],

View file

@ -1,11 +1,11 @@
{
"//": "THE single build-config for this STANDALONE doxa-maps bundler. Edit this ONE file, then run `bun run build`. FIELDS: 'name' = the output folder name the tree is written as. 'paths' = EXTRA folders to copy the whole tree into. EMPTY and normally stays empty: the build writes into public/<name>/ directly, because public/ is the committed served web-root rather than something assembled at build time. STANDALONE NOTE (2026-07-30): the old value ['../../../public/js'] pointed into the dx-life Nuxt app's public dir \u2014 meaningless in this repo, and the whole point of standing this bundler up on its own is that dx-life consumes maps from the CDN instead of hosting a copy. So 'paths' is now EMPTY: the tree is written to ./app/<name>/ only, and that ONE tree is what all three targets consume (Railway/CDN static host, Capacitor webDir, local staging). If you ever want to ALSO publish into a local dx-life checkout, add its path back here. 'embedBaseUrl' = public URL where the <name>/ tree is served, used to build ABSOLUTE paste-anywhere embed snippets for partner sites ('' = RELATIVE ./bundle/profile/... URLs so the dropped folder is position-independent \u2014 keeps the drop-portability gate green and is what Capacitor needs, since the app loads from https://localhost). 'tokenUrl' = the endpoint every generated map page fetches its Mapbox token from \u2014 a URL, NOT a secret. Set it ABSOLUTE (https://doxa.life/api/maps/token) so both a CDN-hosted folder and the mobile app work; the token stays remote and only the URL ships. window.MAP_TOKEN_URL overrides at runtime. Your endpoint must send CORS headers permitting https://localhost (the Capacitor origin) and any partner domain. 'token' = OPT-IN literal token baked into every page \u2014 LEAVE EMPTY; prefer tokenUrl. 'assetsBaseUrl' = where the SHARED asset tree (previews, posters, boundaries) is served from. Vite no longer copies public/ into each bundle (publicDir:false in vite.config.js), so pages are TOLD where the one shared copy lives. EMPTY = a relative path derived per page depth, which keeps a dropped folder self-contained under one web-root. Set it ABSOLUTE (https://maps.doxa.life/assets) for partner embeds and the mobile app, where there is no shared web-root to climb to. Same modular pattern as tokenUrl and window.MAP_APP_API_URL. 'dropin.include' = THE drop-in contract, declared here and nowhere else: the only root entries of public/ that `sync-dropin --from-config` copies into a host — index.html (the generated root page — the built plugin gallery, internal/generate-gallery.mjs), assets/ (data) and <name>/ (the bundle folders + manifest.json); with --prune everything else in the host's drop-in folder is removed, and `public-hygiene-check --strict` fails on any other root entry. 'dropin.exclude' = built files INSIDE the allowlist that are still not shipped: data no page fetches (the full people-groups CSV, the tables only internal scripts read, the table index). They stay in public/ for the scripts; `sync-dropin --from-config` skips them and counts them as 'excluded (not shipped)', and a copy already in the host is removed like any stale file.",
"//": "THE single build-config for this STANDALONE doxa-maps bundler. Edit this ONE file, then run `bun run build`. FIELDS: 'name' = the output folder name the tree is written as. 'paths' = EXTRA folders to copy the whole tree into. EMPTY and normally stays empty: the build writes into public/<name>/ directly, because public/ is the committed served web-root rather than something assembled at build time. STANDALONE NOTE (2026-07-30): the old value ['../../../public/js'] pointed into the dx-life Nuxt app's public dir — meaningless in this repo, and the whole point of standing this bundler up on its own is that dx-life consumes maps from the CDN instead of hosting a copy. So 'paths' is now EMPTY: the tree is written to ./app/<name>/ only, and that ONE tree is what all three targets consume (Railway/CDN static host, Capacitor webDir, local staging). If you ever want to ALSO publish into a local dx-life checkout, add its path back here. 'embedBaseUrl' = public URL where the <name>/ tree is served, used to build ABSOLUTE paste-anywhere embed snippets for partner sites ('' = RELATIVE ./bundle/profile/... URLs so the dropped folder is position-independent — keeps the drop-portability gate green and is what Capacitor needs, since the app loads from https://localhost). 'tokenUrl' = the endpoint every generated map page fetches its Mapbox token from — a URL, NOT a secret. Set it ABSOLUTE (https://doxa.life/api/maps/token) so both a CDN-hosted folder and the mobile app work; the token stays remote and only the URL ships. window.MAP_TOKEN_URL overrides at runtime. Your endpoint must send CORS headers permitting https://localhost (the Capacitor origin) and any partner domain. 'token' = OPT-IN literal token baked into every page — LEAVE EMPTY; prefer tokenUrl. 'assetsBaseUrl' = where the SHARED asset tree (previews, posters, boundaries) is served from. Vite no longer copies public/ into each bundle (publicDir:false in vite.config.js), so pages are TOLD where the one shared copy lives. EMPTY = a relative path derived per page depth, which keeps a dropped folder self-contained under one web-root. Set it ABSOLUTE (https://maps.doxa.life/assets) for partner embeds and the mobile app, where there is no shared web-root to climb to. Same modular pattern as tokenUrl and window.MAP_APP_API_URL. 'dropin.include' = THE drop-in contract, declared here and nowhere else: the only root entries of public/ that `sync-dropin --from-config` copies into a host — index.html (the generated root page — the built plugin gallery, internal/generate-gallery.mjs), assets/ (data) and <name>/ (the bundle folders + manifest.json); with --prune everything else in the host's drop-in folder is removed, and `public-hygiene-check --strict` fails on any other root entry. 'dropin.exclude' = built files INSIDE the allowlist that are still not shipped: data no page fetches (the full people-groups CSV, the tables only internal scripts read, the table index). They stay in public/ for the scripts; `sync-dropin --from-config` skips them and counts them as 'excluded (not shipped)', and a copy already in the host is removed like any stale file.",
"name": "doxa-maps",
"paths": [],
"embedBaseUrl": "",
"assetsBaseUrl": "",
"tokenUrl": "https://doxa.life/api/maps/token",
"token": "",
"token": "pk.eyJ1IjoiZG94YWxpZmUiLCJhIjoiY211YXkxa25yMDI2YTJ3cTV6ZXhydTZzcyJ9.fBMmvu0Y0S9u8tnHOyVhxg",
"dropin": {
"dest": "../doxa-life/public/embed/doxa-maps-build",
"include": [
@ -24,5 +24,16 @@
"gatedProfiles": [
"key-cities"
],
"qaWidget": false
}
"qaWidget": false,
"//token": [
"THE PUBLIC MAPBOX KEY, BAKED ON PURPOSE (Driver ruling 2026-09-21): 'all maps can use the pk",
"from the api, no problem — if they want to set a mapbox sk they can'. This is token-ladder",
"RUNG 3 (see internal/generate-tree.mjs:595): rungs 1-3 are used DIRECTLY with no network call,",
"so baking it makes every map work out of the box on ANY host — including one with no",
"/api/maps/token endpoint and no CORS on the fallback, which is exactly why nuxtbase.app maps",
"were blank. SOURCE OF TRUTH: https://doxa.life/api/maps/token (see",
"_qa-cycle/01-ideation-research/REFERENCE-mapbox-token-endpoint.md). Re-fetch from there rather",
"than copying this literal around. A pk. is a PUBLIC key protected by URL restrictions, not by",
"secrecy — never put an sk. here; check:public-hygiene still fails the build on sk./tk."
]
}