From 70df4f00f64eb0a3017c7763c9b3526afbf73421 Mon Sep 17 00:00:00 2001 From: doxa-maps Date: Tue, 22 Sep 2026 07:56:34 +0000 Subject: [PATCH] Gate that every shipped leaf carries its own map token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A cap-5 shell nests its children as iframes, and window.MAP_TOKEN does not cross an iframe boundary — each frame is its own window. A nested map cannot inherit the parent's token, so it works only because the child leaf carries its own baked copy. Proven in a browser: all thirteen nested frames in the cap-5 shell hold a real token, including a map two levels deep. That was a one-off run; this makes it standing. The existing smokes cannot catch this. Both inject window.MAP_TOKEN with addInitScript before the page runs, which is right for what they test but means the test supplies the thing under test. A user opening a shipped leaf has nothing injecting a token for them. Also asserts the reverse: no token may be compiled into a .js bundle. Bundles are cached, SRI-pinned and reused across hosts, so a token baked into one cannot be rotated without a rebuild and leaks into every embed. Reads the built manifest rather than walking directories, so it checks the set that actually ships. Counted 43 leaves, matching an independent filesystem count. Negative-tested both directions: stripping a leaf's token and planting one in a bundle each fail with the artifact named. --- .../README-SECTION-3-cli-commands.md | 2 +- internal/check-nested-token-delivery.mjs | 146 ++++++++++++++++++ package.json | 5 +- 3 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 internal/check-nested-token-delivery.mjs diff --git a/documentation/README-SECTION-3-cli-commands.md b/documentation/README-SECTION-3-cli-commands.md index adae3e236..e3e915d28 100644 --- a/documentation/README-SECTION-3-cli-commands.md +++ b/documentation/README-SECTION-3-cli-commands.md @@ -25,7 +25,7 @@ The scripts these commands are grounded in (from this branch's `package.json`): // claiming to be ground truth, and this one had silently stopped being it. "dev": "bun x vite", "build": "bash internal/build-lock.sh", -"build:unlocked": "bun run check:template-literal && bun run clean && bun run token && for d in plugin-designs/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:parse && bun run verify && bun run check:sri && bun run check:public-hygiene && bun run check:label-joins && bun run check:descriptions && bun run check:no-abs-paths && bun run check:dep-names && bun run check:composite-srcs && bun run gallery", +"build:unlocked": "bun run check:template-literal && bun run clean && bun run token && for d in plugin-designs/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:parse && bun run verify && bun run check:sri && bun run check:public-hygiene && bun run check:label-joins && bun run check:descriptions && bun run check:no-abs-paths && bun run check:dep-names && bun run check:composite-srcs && bun run check:nested-token && bun run gallery", "preview": "bun x vite preview", "serve": "python3 -m http.server 4173 --bind 0.0.0.0 --directory public", "cap:sync": "./node_modules/.bin/cap sync android", diff --git a/internal/check-nested-token-delivery.mjs b/internal/check-nested-token-delivery.mjs new file mode 100644 index 000000000..dc1e7c127 --- /dev/null +++ b/internal/check-nested-token-delivery.mjs @@ -0,0 +1,146 @@ +#!/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' + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') + +let treeName = 'doxa-maps' +try { + const cfg = JSON.parse(readFileSync(join(ROOT, 'vite.1040-maps-build-config.json'), 'utf8')) + if (cfg?.name) treeName = cfg.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: 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).') diff --git a/package.json b/package.json index 2f6eea567..6c08728e7 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "check:public-hygiene": "node internal/public-hygiene-check.mjs", "check:label-joins": "node internal/check-label-joins.mjs", "check:dxlife": "node internal/check-dxlife-untouched.mjs", - "build:unlocked": "bun run check:template-literal && bun run clean && bun run token && for d in plugin-designs/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:parse && bun run verify && bun run check:sri && bun run check:public-hygiene && bun run check:label-joins && bun run check:descriptions && bun run check:no-abs-paths && bun run check:dep-names && bun run check:composite-srcs && bun run gallery", + "build:unlocked": "bun run check:template-literal && bun run clean && bun run token && for d in plugin-designs/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:parse && bun run verify && bun run check:sri && bun run check:public-hygiene && bun run check:label-joins && bun run check:descriptions && bun run check:no-abs-paths && bun run check:dep-names && bun run check:composite-srcs && bun run check:nested-token && bun run gallery", "check:template-literal": "node internal/template-literal-check.mjs", "verify:map-paint": "node internal/verify-map-paint.mjs", "check:sri": "node internal/sri-consistency-check.mjs", @@ -73,7 +73,8 @@ "check:descriptions": "node internal/check-profile-descriptions.mjs # every shipped profile must say what it is (reads the BUILT manifest)", "check:no-abs-paths": "node internal/check-no-absolute-paths.mjs # no /home/… or /Users/… in any TRACKED file (a clone must work on any machine)", "check:dep-names": "node internal/check-dependency-names.mjs # every declared dep is a REAL package name (a rename once rewrote @capacitor/* and broke bun install in a clone)", - "check:composite-srcs": "node internal/check-composite-srcs.mjs" + "check:composite-srcs": "node internal/check-composite-srcs.mjs", + "check:nested-token": "node internal/check-nested-token-delivery.mjs" }, "devDependencies": { "@capacitor/android": "^7.6.9",