Measure the basemap, in a real embed, on the token the page ships

"The map renders" had never been measured. Every browser gate here
forces a Mapbox token onto the page from a shared file; that token is
restricted and 403s on the basemap, while the leaf bakes a different
one that works. The gates then counted PINS — which are drawn from our
own GeoJSON and paint with no token at all — so they passed on a
basemap the probe itself had broken.

This gate forces nothing. It serves the leaf on one port and a host
page on another so the iframe is genuinely cross-origin, loads it as a
stranger's browser would, and takes evidence two ways: Mapbox responses
counted by status, and colour variety in a slice of the canvas. The
gospel map passes — 9 tile responses, none denied, and the screenshot
shows coastlines, ocean shading and country labels.

The negative control runs every time, and the check reports PROVED
NOTHING rather than a pass if the control succeeds. That is not
decoration: the first two attempts at the control did nothing at all.
A page-level init script never reaches a cross-origin iframe, and even
at context level it was ignored, because this leaf injects `tk`
straight into the element and only falls back to window.MAP_TOKEN —
so it is immune to the forced-token hazard by construction. The
control had to move to the wire. With the token rewritten there, 6 of
8 tile requests are denied and the basemap vanishes, leaving only the
pins and choropleth: the picture that shows exactly why a pin count
was never evidence of a map.
This commit is contained in:
doxa-maps-88655c70 2026-09-24 19:13:46 +00:00
parent 3a70abf248
commit a5ab04ec75
3 changed files with 214 additions and 0 deletions

View file

@ -0,0 +1,210 @@
/**
* check-embed-basemap-paints.mjs — the basemap really paints, in a real third-party iframe,
* on the token the page actually ships with.
*
* WHY THIS EXISTS. Every browser probe in this repo forced a Mapbox token onto the page
* (`?tk=`, or `window.MAP_TOKEN`) from a shared file. That token is `u=doxalife`, which is
* restricted and 403s on the basemap and country boundaries. The leaf pages BAKE a different
* token, `u=gosystems`, which works. So every probe measured a basemap that was broken only
* because the probe broke it — and passed anyway, because it went on to count PINS, which are
* drawn from our own GeoJSON and paint with no token at all.
*
* The consequence is that "the map renders" has never actually been measured. A pin count is
* not a basemap. This check exists to make that claim measurable, and it deliberately does
* NOT force a token: it loads the page exactly as a stranger's browser would.
*
* IT IS A REAL EMBED. The leaf is served on one port and the host page on another, so the
* iframe is genuinely cross-origin — the same relationship a third-party site has to it.
*
* IT CAN FAIL, AND IS SHOWN TO. A check that cannot fail proves nothing, so every run also
* loads the SAME page with the restricted token forced on, and requires that run to fail the
* basemap test. If the negative control passes, the check reports that it proved nothing
* rather than reporting success — a broken detector must never read as a green result.
*
* Evidence is taken two independent ways, because either alone has a failure mode:
* 1. NETWORK — Mapbox tile/style responses, counted by status. 401/403 is decisive.
* 2. PIXELS — colour variety in a slice of the canvas. A page can 200 on everything and
* still paint nothing, and a screenshot is the only witness to that.
*/
import { chromium } from 'playwright'
import { spawn } from 'node:child_process'
import { writeFileSync, mkdtempSync, readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url'
const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = resolve(HERE, '..')
const PUBLIC = resolve(REPO, 'public')
const LEAF = 'doxa-maps/gospel-resources/resources-map/index.html'
const leafPort = 5810 + Math.floor(Math.random() * 60)
const hostPort = leafPort + 1
/** Mapbox endpoints that actually draw the map. Telemetry is not evidence of anything. */
const isBasemap = (u) =>
/api\.mapbox\.com\/(styles|v4|fonts|tilesets)|tiles\.mapbox\.com/.test(u) &&
!/events\.mapbox\.com/.test(u)
function serve(dir, port) {
return spawn(process.execPath, [resolve(REPO, 'internal/serve-strict.mjs'), '--port', String(port), '--directory', dir], {
stdio: 'ignore',
})
}
async function waitFor(url, ms = 20000) {
const until = Date.now() + ms
while (Date.now() < until) {
if ((await fetch(url).catch(() => null))?.ok) return true
await new Promise((r) => setTimeout(r, 250))
}
return false
}
/* A third-party page: nothing but an iframe, served from another origin. */
const hostDir = mkdtempSync(resolve(tmpdir(), 'gr-embed-'))
writeFileSync(
resolve(hostDir, 'index.html'),
`<!doctype html><meta charset="utf-8"><title>third-party host</title>
<body style="margin:0;background:#f3f3f1">
<iframe id="f" src="http://127.0.0.1:${leafPort}/${LEAF}" style="width:1100px;height:760px;border:0"></iframe>
</body>`,
)
const leafSrv = serve(PUBLIC, leafPort)
const hostSrv = serve(hostDir, hostPort)
const stop = () => {
try { leafSrv.kill() } catch {}
try { hostSrv.kill() } catch {}
}
if (!(await waitFor(`http://127.0.0.1:${leafPort}/${LEAF}`)) || !(await waitFor(`http://127.0.0.1:${hostPort}/index.html`))) {
console.error('EMBED-BASEMAP: NOT MEASURED — a server did not come up.')
stop()
process.exit(1)
}
const browser = await chromium.launch({
args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--no-sandbox'],
})
/**
* @param {string|null} forceToken null = the page's OWN baked token (the real case).
*/
async function run(forceToken) {
/* The init script MUST be installed on the CONTEXT, not the page: the leaf lives in a
cross-origin iframe, and a page-level init script never reaches it. Installing it on the
page made both runs use the baked token and the check correctly reported that it had
proved nothing — which is the only reason this was caught rather than shipped as a pass. */
const ctx = await browser.newContext({ viewport: { width: 1200, height: 820 } })
const page = await ctx.newPage()
if (forceToken) {
/* THE CONTROL HAS TO WORK AT THE NETWORK LAYER. Setting window.MAP_TOKEN does nothing to
this leaf: it injects `tk` straight into the element's profile-config, and the window
global is only a fallback for leaves that do not. That is worth knowing on its own —
this page is immune to the forced-token hazard by construction — but it means the only
way to ask "what if the token were the restricted one" is to rewrite the token on the
requests themselves. */
await ctx.route(/api\.mapbox\.com|tiles\.mapbox\.com/, async (route) => {
const u = new URL(route.request().url())
if (u.searchParams.has('access_token')) u.searchParams.set('access_token', forceToken)
await route.continue({ url: u.toString() })
})
}
const net = { ok: 0, denied: 0, other: 0 }
page.on('response', (r) => {
if (!isBasemap(r.url())) return
const s = r.status()
if (s === 401 || s === 403) net.denied++
else if (s >= 200 && s < 300) net.ok++
else net.other++
})
await page.goto(`http://127.0.0.1:${hostPort}/index.html`, { waitUntil: 'networkidle', timeout: 60000 })
await page.waitForTimeout(12000)
const frame = page.frames().find((f) => f.url().includes('resources-map'))
if (!frame) return { net, pixels: null, reason: 'the iframe never loaded the leaf' }
/* A slice well away from the sheet and the controls, so we sample map, not chrome. */
const seen = await frame.evaluate(() => (window.MAP_TOKEN || '').slice(0, 24) || '(none on window)')
const shot = await page.screenshot({ clip: { x: 60, y: 180, width: 700, height: 420 } })
const png = shot
await page.close()
await ctx.close()
return { net, png, seen }
}
const real = await run(null)
const forcedTokenPath = '/tmp/mbtk'
let restricted = ''
try { restricted = readFileSync(forcedTokenPath, 'utf8').trim() } catch { /* control simply cannot run */ }
const control = restricted ? await run(restricted) : null
/* Pixel variety, measured in-browser so no image library is needed. */
const shotPage = await browser.newPage()
async function variety(png) {
if (!png) return 0
return shotPage.evaluate(async (b64) => {
const img = new Image()
img.src = `data:image/png;base64,${b64}`
await img.decode()
const c = document.createElement('canvas')
c.width = img.width
c.height = img.height
c.getContext('2d').drawImage(img, 0, 0)
const d = c.getContext('2d').getImageData(0, 0, c.width, c.height).data
const seen = new Set()
for (let i = 0; i < d.length; i += 4) {
seen.add(((d[i] >> 3) << 10) | ((d[i + 1] >> 3) << 5) | (d[i + 2] >> 3))
}
return seen.size
}, png.toString('base64'))
}
const realColours = await variety(real.png)
const ctlColours = control ? await variety(control.png) : null
if (process.env.EMBED_SHOTS) {
const { writeFileSync: wf } = await import('node:fs')
if (real.png) wf(`${process.env.EMBED_SHOTS}/embed-baked.png`, real.png)
if (control && control.png) wf(`${process.env.EMBED_SHOTS}/embed-restricted.png`, control.png)
console.log(` shots written to ${process.env.EMBED_SHOTS}`)
}
await browser.close()
stop()
/* A painted basemap has many colours; a blank ground has a handful. */
const COLOUR_FLOOR = 40
const realPaints = real.net.denied === 0 && real.net.ok > 0 && realColours >= COLOUR_FLOOR
console.log(`EMBED-BASEMAP: cross-origin iframe, leaf on :${leafPort} inside a host on :${hostPort}.`)
console.log(` baked token tiles ${real.net.ok} ok / ${real.net.denied} denied · ${realColours} distinct colours`)
if (control) {
console.log(` restricted token tiles ${control.net.ok} ok / ${control.net.denied} denied · ${ctlColours} distinct colours`)
console.log(` token the frame used: baked="${real.seen}" control="${control.seen}"`)
}
if (!control) {
console.error(`EMBED-BASEMAP: NOT MEASURED — ${forcedTokenPath} is absent, so the negative control could not run.`)
console.error(' Without a control this check cannot be shown to fail, and a result it cannot')
console.error(' fail on is not evidence. Refusing to report a pass.')
process.exit(1)
}
const controlFails = control.net.denied > 0 || ctlColours < COLOUR_FLOOR
if (!controlFails) {
console.error('EMBED-BASEMAP: PROVED NOTHING — the restricted token painted a basemap too,')
console.error(' so this check cannot tell a working token from a broken one. Fix the control')
console.error(' before trusting any pass it reports.')
process.exit(1)
}
if (!realPaints) {
console.error('EMBED-BASEMAP: FAIL — the page does NOT paint a basemap on its own baked token')
console.error(` in a third-party iframe (${real.net.denied} denied tile response(s), ${realColours} colours).`)
console.error(' Pins can still draw here: they come from our GeoJSON and need no token, which is')
console.error(' exactly why a pin count must never be read as "the map renders".')
process.exit(1)
}
console.log('EMBED-BASEMAP: PASS — the basemap paints on the baked token in a third-party iframe,')
console.log(' and the same page with the restricted token forced on fails, so the check discriminates.')

View file

@ -71,6 +71,9 @@ const GATES = [
// and answer a tap. It also scans every link in the index against the screened-publisher
// list, which is a doctrine boundary rather than a rendering one.
{ script: 'verify:gospel-map', why: 'the gospel map renders its pins, answers a tap, and links only screened publishers' },
// verify:gospel-map counts pins, which paint with no token at all. This one is the only
// gate that measures the BASEMAP, in a real cross-origin iframe, on the baked token.
{ script: 'check:embed-basemap', why: "the gospel map's basemap paints in a third-party iframe on its own baked token" },
{ script: 'verify:gate1', why: 'the Gate-1 chain filters live data end to end' },
{ script: 'check:a11y-button-names', why: 'every visible button has an accessible name' },
{ script: 'check:nav-reachable', why: 'a phone can open the cap-5 tab rail' },

View file

@ -155,6 +155,7 @@
"check:coverage-consistent:poison": "node internal/check-gospel-coverage-consistent.mjs --poison",
"check:surfaces-read-the-tier": "node internal/check-surfaces-read-the-tier.mjs # a surface may READ the tier the pin index computed, never re-derive 'this group has nothing' from the raw resource fields. That hand-rolled field list went stale every time a source was added: 7 codes drawn as having nothing while the map showed them a link, then 28 when Scripture Earth landed. Static, sub-second. Self-test: `bun run check:surfaces-read-the-tier:poison` re-injects the old formula into every surface and each must be caught.",
"check:surfaces-read-the-tier:poison": "node internal/check-surfaces-read-the-tier.mjs --poison",
"check:embed-basemap": "node internal/check-embed-basemap-paints.mjs # the gospel map's BASEMAP must paint inside a genuinely cross-origin iframe on the token the page BAKES, not one a probe forces on it. Every other browser gate here forces a restricted token (u=doxalife) that 403s on the basemap, then counts PINS — which come from our own GeoJSON and paint with no token at all — so 'the map renders' had never been measured. Runs a negative control every time by rewriting the token on the wire; if the control passes it reports PROVED NOTHING rather than a pass. ~60s, needs a browser.",
"check:synced-css:self-test": "node internal/check-synced-css-copies.mjs --self-test",
"serve:permissive": "python3 -m http.server 4173 --bind 127.0.0.1 --directory public # LOOPBACK ONLY. This was --bind 0.0.0.0, which published the built tree on every interface of whatever machine ran a gate; measured on the build VPS it answered HTTP 200 on the box's PUBLIC IP while a browser gate was running. Every consumer (verify-browser-all, check:a11y-button-names, check:nav-reachable, smoke:share-all) already defaults to 127.0.0.1, so nothing needed the wider bind. Override with BASE_URL/BASE if you deliberately want to reach it from another host.",
"check:doc-scripts": "node internal/check-doc-quotes-scripts.mjs",