doxa-maps/internal/check-embed-basemap-paints.mjs
doxa-maps-88655c70 ce46d92998
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Composite pages hand a ?tk= or pasted token to their nested maps, and ask when there is none
A composite's nested Mapbox maps read window.MAP_TOKEN, not the page's
profile-config. In a tree with no baked token (the public copy), the token-free
research-map-plus-grid page mounted before reading ?tk= or a stored token, so
its map stayed blank with no prompt. Now every leaf publishes the token it
resolves to window.MAP_TOKEN, and a token-free composite with no token renders
its content plus the paste-once prompt, then reloads with the token.

Verified in a browser against a tree regenerated with no token:
  research-map-plus-grid  no token: prompt shown | ?tk=: map drawn, Mapbox 6 OK / 0 errors
  dashboard-plus-grid     no token: prompt shown | ?tk=: map drawn, Mapbox 6 OK / 0 errors
Negative control (unpatched generator): research-map-plus-grid with ?tk= never
received the token (0 Mapbox requests) and showed no prompt without one.

Also: removed a personal health note the page template wrote into every leaf
page, and the Mapbox account names from three comments. Pages regenerated on
the latest main; nested-token, sri, hygiene, drop, parse, embed-targets pass.
2026-09-25 05:35:46 +00:00

211 lines
9.9 KiB
JavaScript

/**
* 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 domain-restricted, which is
* restricted and 403s on the basemap and country boundaries. The leaf pages BAKE a different
* token, which is not restricted and 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 { serveReaped } from './_serve-reaped.mjs'
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)
/* Reaped on signals as well as on the happy path. This gate used to kill its two servers
only at the end, which is fine until the end is not reached: when the box ran low on
memory it killed gate processes mid-run and their servers were orphaned, holding memory
and making the next kill likelier. Measured both ways before this changed — the old
spawn survived a SIGTERM to its parent, this one does not. */
const serve = (dir, port) => serveReaped(dir, port)
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 = () => {
leafSrv.stop()
hostSrv.stop()
}
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.')