From a5ab04ec753a38b72ee70b0873946d2889d1aab7 Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Thu, 24 Sep 2026 19:13:46 +0000 Subject: [PATCH] Measure the basemap, in a real embed, on the token the page ships MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "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. --- internal/check-embed-basemap-paints.mjs | 210 ++++++++++++++++++++++++ internal/verify-browser-all.mjs | 3 + package.json | 1 + 3 files changed, 214 insertions(+) create mode 100644 internal/check-embed-basemap-paints.mjs diff --git a/internal/check-embed-basemap-paints.mjs b/internal/check-embed-basemap-paints.mjs new file mode 100644 index 000000000..4c7118bca --- /dev/null +++ b/internal/check-embed-basemap-paints.mjs @@ -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'), + `third-party host + + +`, +) + +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.') diff --git a/internal/verify-browser-all.mjs b/internal/verify-browser-all.mjs index 5ee868b21..f12f7d42e 100644 --- a/internal/verify-browser-all.mjs +++ b/internal/verify-browser-all.mjs @@ -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' }, diff --git a/package.json b/package.json index ff2530c07..bbf368490 100644 --- a/package.json +++ b/package.json @@ -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",