doxa-maps/internal/check-nested-token-delivery.mjs
doxa-maps-88655c70 44ef31b2c6 Tooling reads the tree's name from the build config, in one place
The output tree's name has always been configurable (vite.doxa-maps-build-config.json "name"),
but 61 tooling files ignored it and hardcoded 'doxa-maps' (181 literals), so a rename would have
moved the build while the gates kept reading the old folder.

- internal/_tree-name.mjs reads it once (validated as one URL-safe path segment; falls back to the
  historical name) and exports TREE_NAME / TREE_NAME_RE; `node internal/_tree-name.mjs` prints it
  for shell scripts.
- A codemod rewrote 171 literals in internal/ to TREE_NAME; the regex literals, messages with
  backticks, atomic-public-swap.sh (fails loudly if the name cannot be read), build-lock.sh and
  vite.config.js (its duplicate readMfeName is gone) were done by hand.
- The name is unchanged: old and new tooling produce all 1408 files under public/ byte for byte.
  Code lines still naming the tree in internal/ + vite.config.js: 2, both intended (the helper's
  fallback, the private-repo-name pattern in public-hygiene-check).

This renames nothing visible; the eventual rename becomes a one-line config change.
2026-09-25 16:25:22 +00:00

156 lines
8.1 KiB
JavaScript

#!/usr/bin/env node
/**
* check-nested-token-delivery.mjs — every shipped leaf must carry its OWN map token.
*
* WHY THIS IS NOT COVERED BY THE EXISTING SMOKES. Both `smoke:embeds` and `verify:5-caps` open a
* page with `page.addInitScript(tk => { window.MAP_TOKEN = tk })` — they INJECT the token before
* the page runs. That is correct for what those checks test (does the element mount, does the
* capability hold), but it means the test SUPPLIES the very thing under test. A real user opening
* a shipped leaf has no Playwright injecting anything: the leaf must already carry a baked token.
* So no gate currently proves the delivery path itself. This one does, and it needs no browser.
*
* ★ WHY DELIVERY IS A SEPARATE PROPERTY FROM PAINT. A token can be present and the basemap still
* blank (this build box has no WebGL; Mapbox's URL restriction can also 403 the tiles). Those are
* CONSUMPTION failures. This gate asserts DELIVERY only — "does the token reach the page?" — which
* is the half that is our fault when it breaks, deterministic, and checkable offline.
*
* ★ WHY IT MATTERS FOR NESTING (the card this came from). A cap-5 SPA nests its children as
* iframes, and `window.MAP_TOKEN` does NOT cross an iframe boundary — each frame is its own
* window. A nested map therefore cannot inherit the parent's token; it works only because the
* CHILD LEAF carries its own baked copy. Proven live 2026-09-22: 13 of 13 nested frames in the
* cap-5 SPA reported a real pk. token, including a map nested two levels deep (SPA -> cap-4
* composite -> cap-3 map). That proof was a one-off browser run; this makes it a standing gate.
*
* WHAT IT CHECKS: every leaf the manifest actually ships (profile leaves and instance leaves)
* contains a real `pk.` token. It reads the BUILT manifest rather than walking directories, so it
* checks the set that is published, not whatever happens to be on disk.
*
* ★ AND IT ENFORCES THE INVARIANT IN BOTH DIRECTIONS: a token belongs in leaf HTML and must NEVER
* appear in a `.js` bundle. A bundle is cached, SRI-pinned and reused across hosts; a token baked
* into one cannot be rotated without a rebuild, and it leaks into every embed. The leaf is the
* right place precisely because it is regenerated on every build.
*
* NEGATIVE-TESTED: stripping the token from one leaf makes this FAIL naming that leaf; planting a
* pk. string in a .js bundle makes this FAIL naming the bundle; restoring returns PASS. A manifest
* with no leaves FAILS (vacuous guard).
*/
import { readFileSync, existsSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
// ★ REPORT WHERE THE TREE NAME CAME FROM, not just what it is (2026-09-23).
// This read named `vite.1040-maps-build-config.json` — the PRE-RENAME project name, a file that
// does not exist — so the try always threw and treeName was always the hardcoded default. It was
// correct only because the default happens to equal the config's current name. A dead read is
// INVISIBLE when only the result is printed, so the source is now part of the output: a future
// regression shows up on every run instead of waiting for a tree rename to expose it.
let treeName = TREE_NAME
let treeNameSource = 'built-in default (build config unreadable)'
try {
const cfg = JSON.parse(readFileSync(join(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8'))
if (cfg?.name) { treeName = cfg.name; treeNameSource = 'vite.doxa-maps-build-config.json' }
else treeNameSource = 'built-in default (config has no `name`)'
} catch { /* default */ }
const TREE = join(ROOT, 'public', treeName)
const MANIFEST = join(TREE, 'manifest.json')
if (!existsSync(MANIFEST)) {
console.log(`NESTED-TOKEN: SKIP — no built manifest at public/${treeName}/manifest.json.`)
console.log(' Run `bun run build` first. An absent tree is not evidence either way.')
process.exit(0)
}
let manifest
try {
manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'))
} catch (e) {
console.error(`NESTED-TOKEN: FAIL — manifest does not parse (${String(e.message).slice(0, 60)}).`)
process.exit(1)
}
// A real Mapbox public token. Deliberately strict about the `pk.` prefix and a minimum body: an
// empty string or a placeholder like "REPLACE_ME" must NOT read as delivered.
const TOKEN_RE = /pk\.[A-Za-z0-9._-]{20,}/
// Collect every leaf URL the manifest publishes. Profiles carry iframeUrl; instances nest one
// level deeper and carry their own. Reading the manifest (not the filesystem) keeps this aligned
// with what is actually served.
const leaves = []
for (const b of manifest.bundles || []) {
for (const p of b.profiles || []) {
if (p.iframeUrl) leaves.push({ bundle: b.bundle, profile: p.profile, url: p.iframeUrl })
for (const inst of p.instances || []) {
if (inst.iframeUrl) leaves.push({ bundle: b.bundle, profile: `${p.profile}/${inst.id || inst.instance}`, url: inst.iframeUrl })
}
}
}
// Vacuous guard: "0 leaves" must never read as "0 problems".
const MIN_LEAVES = 20
if (leaves.length < MIN_LEAVES) {
console.error(`NESTED-TOKEN: FAIL — manifest lists only ${leaves.length} leaf page(s) (expected >= ${MIN_LEAVES}).`)
console.error(' The manifest is truncated or the shape changed. Refusing to pass vacuously.')
process.exit(1)
}
const missing = []
let checked = 0
for (const leaf of leaves) {
const rel = leaf.url.replace(/^\.\//, '')
const abs = join(TREE, rel)
if (!existsSync(abs)) {
missing.push({ ...leaf, why: 'leaf file does not exist in the built tree' })
continue
}
checked++
const html = readFileSync(abs, 'utf8')
if (!TOKEN_RE.test(html)) {
missing.push({ ...leaf, why: 'no pk. token baked into the leaf — a nested map here would get NOTHING' })
}
}
// The other direction: a token must never be compiled into a .js bundle.
const inBundles = []
for (const b of manifest.bundles || []) {
const rel = (b.scriptUrl || '').replace(/^\.\//, '')
if (!rel) continue
const abs = join(TREE, rel)
if (!existsSync(abs)) continue
if (TOKEN_RE.test(readFileSync(abs, 'utf8'))) inBundles.push(b.bundle)
}
console.log(`NESTED-TOKEN: ${checked} shipped leaf page(s) checked, ${(manifest.bundles || []).length} bundle(s) scanned.`)
if (missing.length || inBundles.length) {
if (missing.length) {
console.error(`\nNESTED-TOKEN: FAIL — ${missing.length} leaf page(s) ship without a token:`)
for (const m of missing.slice(0, 20)) console.error(` ✗ ${m.bundle}/${m.profile}\n ${m.url}\n ${m.why}`)
if (missing.length > 20) console.error(` … +${missing.length - 20} more`)
console.error(
'\n window.MAP_TOKEN does NOT cross an iframe boundary — each frame is its own window. A cap-5\n' +
' SPA nests its children as iframes, so a nested map cannot inherit the parent\'s token: it\n' +
' works only because the CHILD leaf carries its own baked copy. A leaf without one is a map\n' +
' that silently renders nothing for every user who opens it directly or through a shell.'
)
}
if (inBundles.length) {
console.error(`\nNESTED-TOKEN: FAIL — ${inBundles.length} bundle(s) have a token COMPILED IN:`)
for (const b of inBundles) console.error(` ✗ ${b}`)
console.error(
'\n A token belongs in leaf HTML, never in a .js bundle. Bundles are cached, SRI-pinned and\n' +
' reused across hosts, so a baked-in token cannot be rotated without a rebuild and leaks into\n' +
' every embed. Leaves are regenerated on every build, which is exactly why they are the right\n' +
' place. Check whether a source file reads the token at module scope instead of at mount.'
)
}
process.exit(1)
}
console.log(`NESTED-TOKEN: tree public/${treeName}/ (name from ${treeNameSource}).`)
console.log('NESTED-TOKEN: PASS — every shipped leaf carries its own token, and no bundle does.')
console.log(' This is DELIVERY only. It does not prove a basemap paints: that needs a real GPU and')
console.log(' a token whose URL restrictions allow the serving host (see verify:map-paint).')