#!/usr/bin/env node /** * check-leaf-drops-tk.mjs — a leaf page that owns its address does not keep a token in it. * * THE DEFECT. A generated leaf page reads a Mapbox token from ?tk= and keeps it in the browser (MAPS_STAGING_TK), but it * left it in the address. Now that the address names the open tab (?tab=, useViewAddress), people copy the address bar * to share a tab, and the token travelled with it. internal/generate-tree.mjs's leaf script now removes tk from the * address with a replace, on a page that owns its address (urlOwner 'app'; a framed page has none), and only when tk is * there, keeping the other parameters' exact text, the hash and history.state. * * Every generated leaf page (public///[/]/index.html) runs its own leaf script in a * sandbox (location, history, localStorage and a stub DOM; fetch never answers): * W. the page carries the template's block verbatim (a stale tree fails: regenerate it); * A. owned, ?tk=pk.T&tab=x&lang=ar#h → ONE replace, to ?tab=x&lang=ar#h, history.state kept; the token stored; and * where a page that needs a token mounts, the element is given that token; * B. framed (a partner's iframe) with ?tk= → no write; * C. owned, no tk → no write; * D. other parameters keep their exact text (?f=a,b&tk=…&q=%C3%A9 → ?f=a,b&q=%C3%A9); * E. an encoded key (?%74k=…) is still a tk. * `--self-test`: no strip, a strip on a framed page, a strip that drops every parameter, one that loses the hash and a * push instead of a replace must each FAIL. `--splice` runs on pages with the template's block put in place, as the * generator will emit them (for a source change whose tree is not regenerated yet; never in gates). */ import { readFileSync, readdirSync, existsSync } from 'node:fs' import { join } from 'node:path' import vm from 'node:vm' const REPO = new URL('..', import.meta.url).pathname const TREE = join(REPO, 'public/dx-maps') const TEMPLATE = readFileSync(join(REPO, 'internal/generate-tree.mjs'), 'utf8') const ANCHOR = "if (urlTk) { try { localStorage.setItem('MAPS_STAGING_TK', urlTk); } catch (e) {} }\n" // The block as the template writes it: from its comment to the closing brace of its `if`. function templateBlock() { const at = TEMPLATE.indexOf(' // ★ THE TOKEN LEAVES THE ADDRESS.') if (at < 0) return '' const end = TEMPLATE.indexOf('\n }\n', TEMPLATE.indexOf("if (urlTk && BASE_CONFIG.urlOwner === 'app') {", at)) return end < 0 ? '' : TEMPLATE.slice(at, end + '\n }\n'.length) } function leafPages() { const out = [] const walk = (d, depth) => { for (const e of readdirSync(d, { withFileTypes: true })) { const p = join(d, e.name) if (e.isDirectory() && depth < 3) walk(p, depth + 1) else if (e.name === 'index.html' && depth >= 2) { const html = readFileSync(p, 'utf8') const s = [...html.matchAll(/]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/g)].map((m) => m[1]).find((x) => x.includes('var BASE_CONFIG')) if (s) out.push({ file: p.slice(REPO.length), script: s }) } } } if (existsSync(TREE)) walk(TREE, 0) return out } // Run one leaf script; returns what it did to the address, the store and the element it mounted. function run(script, { search, hash = '', framed = false, state = { k: 1 } }) { const writes = [] const loc = { pathname: '/dx-maps/b/p/', search, hash } const hist = { state, replaceState(s, _, url) { writes.push('replace'); this.state = s; const u = new URL(url, 'https://x.test'); loc.pathname = u.pathname; loc.search = u.search; loc.hash = u.hash }, pushState() { writes.push('push') }, } const store = new Map() const mounted = [] const stub = () => ({ style: {}, className: '', textContent: '', innerHTML: '', parentNode: null, attrs: {}, setAttribute(k, v) { this.attrs[k] = v }, appendChild(c) { mounted.push(c) }, removeChild() {}, addEventListener() {} }) const win = { location: loc, history: hist, URL, URLSearchParams, JSON, Object, String, console: { log() {}, warn() {}, error() {} }, localStorage: { getItem: (k) => store.get(k) ?? null, setItem: (k, v) => store.set(k, String(v)), removeItem: (k) => store.delete(k) }, document: { getElementById: () => stub(), createElement: () => stub(), documentElement: { style: {} }, body: { style: {} } }, fetch: () => new Promise(() => {}), } win.window = win; win.self = win; win.top = framed ? {} : win let error = '' try { vm.runInNewContext(script, win, { timeout: 2000 }) } catch (e) { error = String(e && e.message) } const el = mounted.find((m) => m.attrs['profile-config']) let tk if (el) { try { tk = JSON.parse(el.attrs['profile-config']).tk } catch { tk = undefined } } return { writes, search: loc.search, hash: loc.hash, state: hist.state, stored: store.get('MAPS_STAGING_TK'), mountedTk: el ? tk : null, error } } function audit({ splice = false, mutate = (s) => s } = {}) { const f = [] const block = templateBlock() if (!block) return ['W: internal/generate-tree.mjs has no token-leaves-the-address block'] const pages = leafPages() if (pages.length < 20) f.push(`W: only ${pages.length} generated leaf pages found under public/dx-maps`) let mountedWithTk = 0 for (const p of pages) { let s = p.script if (splice && !s.includes(block)) s = s.includes(ANCHOR) ? s.replace(ANCHOR, ANCHOR + block) : s if (!s.includes(block)) { f.push(`W: ${p.file} does not carry the template's block (regenerate the tree)`); continue } s = mutate(s) const st = { k: 1 } const a = run(s, { search: '?tk=pk.T&tab=x&lang=ar', hash: '#h', state: st }) if (a.error) f.push(`A: ${p.file} threw: ${a.error.slice(0, 80)}`) if (a.writes.join() !== 'replace') f.push(`A: ${p.file} wrote [${a.writes}] (want one replace)`) if (a.search !== '?tab=x&lang=ar' || a.hash !== '#h') f.push(`A: ${p.file} left ${a.search}${a.hash} (want ?tab=x&lang=ar#h)`) if (a.state !== st) f.push(`A: ${p.file} did not keep history.state`) if (a.stored !== 'pk.T') f.push(`A: ${p.file} did not keep the token (MAPS_STAGING_TK)`) // "used" applies to a page that needs a token; a token-free one (a shell, a MapLibre map) mounts without it. const needsToken = /var TOKEN_FREE = false;/.test(s) if (needsToken && a.mountedTk !== null) { if (a.mountedTk !== 'pk.T') f.push(`A: ${p.file} mounted without the token`); else mountedWithTk++ } const b = run(s, { search: '?tk=pk.T&tab=x', framed: true }) if (b.writes.length || b.search !== '?tk=pk.T&tab=x') f.push(`B: ${p.file} framed: wrote [${b.writes}], left ${b.search}`) const c = run(s, { search: '?tab=x&lang=ar', hash: '#h' }) if (c.writes.length || c.search !== '?tab=x&lang=ar') f.push(`C: ${p.file} without tk: wrote [${c.writes}]`) const d = run(s, { search: '?f=a,b&tk=pk.T&q=%C3%A9' }) if (d.search !== '?f=a,b&q=%C3%A9') f.push(`D: ${p.file} left ${d.search} (want ?f=a,b&q=%C3%A9)`) const e = run(s, { search: '?%74k=pk.T&tab=x' }) if (e.search !== '?tab=x') f.push(`E: ${p.file} left ${e.search} for an encoded tk`) } if (pages.length && !mountedWithTk) f.push('A: no page mounted its element, so "the token is used" was never checked') return f } if (process.argv.includes('--self-test')) { const RE = "history.replaceState(history.state, '', location.pathname + (keep ? '?' + keep : '') + location.hash);" const poisons = [ ['no strip', (s) => s.replace(RE, ''), /^A: /], ['a strip on a framed page', (s) => s.replace("if (urlTk && BASE_CONFIG.urlOwner === 'app') {", 'if (urlTk) {'), /^B: /], ['a strip that drops every parameter', (s) => s.replace("return p && k !== 'tk';", 'return false;'), /^[AD]: /], ['a strip that loses the hash', (s) => s.replace(RE, RE.replace(' + location.hash', '')), /^A: /], ['a push instead of a replace', (s) => s.replace(RE, RE.replace('replaceState', 'pushState')), /^A: /], ] let missed = 0 for (const [name, mutate, rx] of poisons) { const f = audit({ splice: true, mutate }); const hit = f.find((x) => rx.test(x)) console.log(hit ? `✓ caught: ${name} -> ${hit}` : `✗ MISSED: ${name}${f.length ? ` (failed instead: ${f[0]})` : ''}`) if (!hit) missed++ } if (missed) process.exitCode = 1 } else { const f = audit({ splice: process.argv.includes('--splice') }) if (f.length) { for (const x of f.slice(0, 30)) console.log('✗ ' + x); if (f.length > 30) console.log(`… ${f.length - 30} more`); console.log(`FAIL check:leaf-drops-tk (${f.length})`); process.exitCode = 1 } else console.log(`PASS check:leaf-drops-tk — every generated leaf that owns its address drops ?tk= with one replace (the other parameters, the hash and history.state kept; the token kept in the browser and used); a framed page and an address without tk are never written${process.argv.includes('--splice') ? ' [--splice: pages as the generator will emit them]' : ''}`) }