diff --git a/internal/_published-base.mjs b/internal/_published-base.mjs new file mode 100644 index 000000000..375013d03 --- /dev/null +++ b/internal/_published-base.mjs @@ -0,0 +1,44 @@ +/** + * _published-base.mjs — where the PUBLISHED tree lives, for the few tools that measure the live catalogue. + * + * The repo names no host (white-label: it ships to people who publish somewhere else). A tool that + * compares against the live tree reads its location from DOXA_PUBLISHED_BASE: the URL the tree folders sit + * under, e.g. https:///plugin-assets (the tree name is appended here, from _tree-name.mjs). Set it + * in the environment, or in the gitignored .env / .env.local at the repo root. + * + * With nothing set, such a tool has nothing to measure: it says NOT MEASURED and exits 75, the fleet's + * third bucket, instead of passing or failing against a host it was never told about. + */ +import { readFileSync } from 'node:fs' +import { join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { TREE_NAME } from './_tree-name.mjs' + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') + +function fromEnvFiles() { + for (const f of ['.env.local', '.env']) { + try { + const m = readFileSync(join(ROOT, f), 'utf8').match(/^\s*DOXA_PUBLISHED_BASE\s*=\s*["']?([^"'\s#]+)/m) + if (m) return m[1] + } catch { /* no such file */ } + } + return '' +} + +/** The published tree's URL (…/, no trailing slash), or '' when none is configured. */ +export function publishedTreeUrl() { + const base = (process.env.DOXA_PUBLISHED_BASE || fromEnvFiles()).replace(/\/+$/, '') + return base ? `${base}/${TREE_NAME}` : '' +} + +/** For a tool that cannot run without it: the tree URL, or NOT MEASURED (exit 75) naming what to set. */ +export function requirePublishedTreeUrl(label) { + const url = publishedTreeUrl() + if (!url) { + console.log(`${label}: NOT MEASURED — no published tree to measure. Set DOXA_PUBLISHED_BASE (environment or .env)`) + console.log(` to the URL your tree folders sit under, e.g. https:///plugin-assets`) + process.exit(75) + } + return url +} diff --git a/internal/check-catalog-currency.mjs b/internal/check-catalog-currency.mjs index d5f8c98f4..955915079 100644 --- a/internal/check-catalog-currency.mjs +++ b/internal/check-catalog-currency.mjs @@ -34,11 +34,12 @@ import { createHash } from 'node:crypto' import { resolve, dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { TREE_NAME } from './_tree-name.mjs' +import { requirePublishedTreeUrl } from './_published-base.mjs' const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const args = process.argv.slice(2) const argOf = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : d } -const BASE = (argOf('--base', `https://nuxtbase.app/plugin-assets/${TREE_NAME}`)).replace(/\/+$/, '') +const BASE = (argOf('--base', '') || requirePublishedTreeUrl('CATALOG-CURRENCY')).replace(/\/+$/, '') const TIMEOUT = Number(argOf('--timeout', 25000)) const sha384 = (buf) => 'sha384-' + createHash('sha384').update(buf).digest('base64') diff --git a/internal/check-catalog-identity.mjs b/internal/check-catalog-identity.mjs index f6110859f..fcd70e5ce 100644 --- a/internal/check-catalog-identity.mjs +++ b/internal/check-catalog-identity.mjs @@ -6,7 +6,7 @@ * Card 1 (IDEA-MAPS-BUNDLER-PUBLIC-FORGEJO-PIPELINE) asks that ONE public Forgejo repo be the live * catalog source, and that the merge lose nothing. The tempting verification is `curl -o /dev/null * -w '%{http_code}'` over a few URLs. That proves only that SOMETHING answered: - * · https://nuxtbase.app/app/doxa-maps/manifest.json returns **200** and is a 2,872-byte HTML + * · the host platform's /app/doxa-maps/manifest.json returns **200** and is a 2,872-byte HTML * SIGN-IN PAGE. An http-status check reads it as healthy. * · a stale CDN copy also returns 200, with the wrong bytes. * @@ -22,7 +22,7 @@ * additionally require that it matches a cloned catalog checkout. * * MEASURED 2026-09-21: 17 of 17 bundles identity-proven, 0 mismatched, and the live public manifest - * is byte-identical (sha256 88ae3832…) to platform/mfe-index's. The catalog IS what nuxtbase serves. + * is byte-identical (sha256 88ae3832…) to platform/mfe-index's. The catalog IS what the host platform serves. * * USAGE * bun run check:catalog-identity # live public door vs its own manifest @@ -35,8 +35,9 @@ import { readFileSync, existsSync } from 'node:fs' import { createHash } from 'node:crypto' import { join } from 'node:path' import { TREE_NAME } from './_tree-name.mjs' +import { requirePublishedTreeUrl } from './_published-base.mjs' -const BASE = (process.env.BASE || `https://nuxtbase.app/plugin-assets/${TREE_NAME}/`).replace(/\/?$/, '/') +const BASE = (process.env.BASE || requirePublishedTreeUrl('CATALOG-IDENTITY')).replace(/\/?$/, '/') const CATALOG = process.env.CATALOG || '' const sha384 = (buf) => 'sha384-' + createHash('sha384').update(buf).digest('base64') diff --git a/internal/check-embed-base-resolution.mjs b/internal/check-embed-base-resolution.mjs index 8be0ca31d..19a2b223a 100644 --- a/internal/check-embed-base-resolution.mjs +++ b/internal/check-embed-base-resolution.mjs @@ -54,7 +54,7 @@ try { // The leaf's own arithmetic is the oracle — computed here rather than hardcoded, so the // expectation cannot drift from what the generated HTML actually means. const TREES = [ - ['live catalog', `https://nuxtbase.app/plugin-assets/${TREE_NAME}`], + ['live catalog', `https://maps.example.org/plugin-assets/${TREE_NAME}`], ['dev tree', `http://127.0.0.1:4173/${TREE_NAME}`], ['doxa.life drop-in', `https://doxa.life/js/doxa-maps-build/${TREE_NAME}`], ['deep subpath', `https://cdn.example.org/a/b/c/${TREE_NAME}`], diff --git a/internal/check-published-matches-build.mjs b/internal/check-published-matches-build.mjs index 7bc873b7b..3bd5721da 100644 --- a/internal/check-published-matches-build.mjs +++ b/internal/check-published-matches-build.mjs @@ -38,11 +38,12 @@ import { createHash } from 'node:crypto' import { resolve, dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { TREE_NAME } from './_tree-name.mjs' +import { requirePublishedTreeUrl } from './_published-base.mjs' const HERE = dirname(fileURLToPath(import.meta.url)) const REPO = process.env.DOXA_REPO || resolve(HERE, '..') const PUBLISHED_BASE = (process.env.PUBLISHED_BASE - || `https://nuxtbase.app/plugin-assets/${TREE_NAME}`).replace(/\/$/, '') + || requirePublishedTreeUrl('PUBLISHED-MATCHES-BUILD')).replace(/\/$/, '') const TREE = resolve(REPO, `public/${TREE_NAME}`) const MANIFEST = join(TREE, 'manifest.json') diff --git a/internal/generate-tree.mjs b/internal/generate-tree.mjs index 29be65885..ba81419fa 100644 --- a/internal/generate-tree.mjs +++ b/internal/generate-tree.mjs @@ -86,14 +86,14 @@ function readBuildConfig() { // The data API every generated page points at, baked as window.MAP_APP_API_URL before the // bundle loads — the HOT-SWAPPABLE backend seam. Empty = same-origin. Set it to the live API // now (https://pray.doxa.life) and flip this ONE field to the self-hosted campaigns-server URL - // once it runs on go-base; every map repoints with no code change. A per-instance apiBaseUrl + // once it runs on the host platform; every map repoints with no code change. A per-instance apiBaseUrl // in a *.instances.json still overrides this for that instance. const apiBaseUrl = (typeof cfg.apiBaseUrl === 'string' ? cfg.apiBaseUrl.trim() : '').replace(/\/+$/, '') // GATED profiles: bundle folder names withheld from the generated tree, staging pages and // manifest (governance — e.g. key-cities). Their .js still BUILDS via vite; they simply never // appear on any public surface the generator emits. convert.py gates the plugin index too. const gatedProfiles = Array.isArray(cfg.gatedProfiles) ? cfg.gatedProfiles.filter((x) => typeof x === 'string' && x.trim()).map((x) => x.trim()) : [] - // QA corner widget baked into generated embed pages (the go-base plugin surface). Default true. + // QA corner widget baked into generated embed pages (the host platform's plugin surface). Default true. // Partner embeds use the copy-embed SNIPPET with their own config, so they never get this. const qaWidget = cfg.qaWidget !== false // Ship the 42 MB print-resolution posters/ inside the tree? DEFAULT FALSE. The wall renders @@ -758,7 +758,7 @@ function mapPageHtml(opts) { const apiScript = apiBaseUrl ? `\n ` : '' - // Bake the QA corner widget onto the go-base embed surface (opt-out via build-config qaWidget:false). + // Bake the QA corner widget onto the host platform's embed surface (opt-out via build-config qaWidget:false). const pageConfig = DEST_CONFIG.qaWidget ? { ...config, qa: true } : config // PROVIDER-AWARE PAGE (see library/composables/useMapProvider.js): a profile or // instance whose config says provider:'maplibre' gets the MapLibre peer (token- diff --git a/internal/install-shell.mjs b/internal/install-shell.mjs index 9969a0d1a..191943d68 100644 --- a/internal/install-shell.mjs +++ b/internal/install-shell.mjs @@ -85,7 +85,7 @@ for (const layout of new Set(nested)) { } // SUBPATH-PORTABLE imports: `..//` resolves only at the web-root (../ cannot escape -// root); served under a subpath (e.g. as a go-base plugin at /plugin-assets/doxa-maps-spa/) +// root); served under a subpath (e.g. as a host-platform plugin at /plugin-assets/doxa-maps-spa/) // it 404s. The layouts are copied as SIBLINGS of index.html, so `.//` is correct at // EVERY depth. Rewrite the copied entry so the shell works both at root and under any subpath. { diff --git a/internal/public-hygiene-check.mjs b/internal/public-hygiene-check.mjs index 829fce5f6..1f4c34be6 100644 --- a/internal/public-hygiene-check.mjs +++ b/internal/public-hygiene-check.mjs @@ -81,7 +81,7 @@ const LEAK_STRICT = [ [/doxa-life\/doxa-maps/, 'private repo name'], [/\bdoxa-life\b/, 'private org name'], [/\bforgejo\b/i, 'self-hosted forge name'], - [/\bnuxtbase\b/, 'internal platform name'], + [new RegExp('\\b' + ['nuxt', 'base'].join('') + '\\b'), 'internal platform name'], // built from parts so this file does not itself carry the name [/\bpersecution\b/, 'operational-context vocabulary (allowed only inside data CSVs)'], [/\.forgejo\/workflows/, 'CI workflow path — process, never ships in a public tree'], ] @@ -97,7 +97,7 @@ const LEAK_STRICT = [ // ★ pk. IS DELIBERATELY NOT HERE (Driver ruling 2026-09-21): "all maps can use the pk from the // api, no problem — if they want to set a mapbox sk they can". A Mapbox `pk.` is a PUBLIC key: it // is designed to ship in client code and is protected by URL RESTRICTIONS, not by secrecy -// (measured: the same pk. serves tiles 200 with a doxa.life Referer and 403s with nuxtbase.app). +// (measured: the same pk. serves tiles 200 with a doxa.life Referer and 403s with the live test host's Referer). // Flagging it made the token ladder's rung-3 baked token UNBUILDABLE — this gate runs inside // build:unlocked, so `pk.` in public/ failed the build that is supposed to produce it. // `sk.` (SECRET key) and `tk.` (temporary) stay flagged: those are real credentials. diff --git a/internal/readme-shots.mjs b/internal/readme-shots.mjs index 4dec82a45..a9f5be65d 100644 --- a/internal/readme-shots.mjs +++ b/internal/readme-shots.mjs @@ -148,7 +148,7 @@ async function shot(name, path, { // Verified in-browser before wiring: 8/8 tile responses 200 with the header, 403 without. // This unblocks BUG-README-SCREENSHOTS-SHOW-BLANK-BASEMAPS and // BUG-FOUR-README-PROOF-IMAGES-SHIP-A-BLANK-BASEMAP without needing the Driver to change the - // token's allow-list. That change is still worth making for LIVE nuxtbase.app rendering, which + // token's allow-list. That change is still worth making for rendering on the LIVE test host, which // this does not address — a browser sets its own Referer and cannot be overridden this way. // // ★ WHY A CONTEXT-WIDE HEADER AND NOT ROUTE INTERCEPTION, measured both ways 2026-09-22: diff --git a/internal/serve-strict.mjs b/internal/serve-strict.mjs index 31316cb5f..8d6b3c60a 100644 --- a/internal/serve-strict.mjs +++ b/internal/serve-strict.mjs @@ -4,7 +4,7 @@ * * ★ THE DEFECT THIS EXISTS FOR, hit live 2026-09-23. `bun run serve` was * `python3 -m http.server`, which resolves a DIRECTORY request to its index.html. The live - * nuxtbase.app static layer does NOT: + * test host's static layer does NOT: * * /plugin-assets/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/ -> 404 live, 200 local * /plugin-assets/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/index.html -> 200 both @@ -122,10 +122,10 @@ createServer((req, res) => { // composed surfaces (cap-4/cap-5) then FETCH manifest.json + assets/ -- and fetch() // enforces CORS, so on any third-party host the shell mounted and every manifest-composed // tab died with 'Could not load ...' (measured 2026-09-24 from a second local origin; the - // LIVE nuxtbase static layer has the same gap -- carded with the Traefik ready-fix). + // LIVE test host's static layer has the same gap -- carded with the Traefik ready-fix). // ACAO:* is the standard for credential-less public assets; production parity is the // whole point of this server, so it must teach the same requirement. - // LIVE_CORS=1 reproduces the LIVE static layer's rule instead (measured on nuxtbase 2026-09-25): + // LIVE_CORS=1 reproduces the LIVE static layer's rule instead (measured on the live test host 2026-09-25): // ACAO on every path INSIDE a folder of the tree, NOT on the tree-root manifest.json. Off by // default; check:gallery-embed --live-cors uses it to prove the gallery still boots cross-origin // under the rule partners actually get. diff --git a/internal/verify-5-capabilities.mjs b/internal/verify-5-capabilities.mjs index c1fd17b6e..5a58e9da9 100644 --- a/internal/verify-5-capabilities.mjs +++ b/internal/verify-5-capabilities.mjs @@ -37,9 +37,10 @@ import { requireThisTree } from './_serves-this-tree.mjs' import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path' import { fileURLToPath as _fileURLToPath } from 'node:url' import { TREE_NAME } from './_tree-name.mjs' +import { requirePublishedTreeUrl } from './_published-base.mjs' const GL=['--use-gl=swiftshader','--enable-unsafe-swiftshader','--ignore-gpu-blocklist','--enable-webgl','--no-sandbox'] -const B=(process.env.BASE || `https://nuxtbase.app/plugin-assets/${TREE_NAME}/`).replace(/\/?$/,'/') +const B=(process.env.BASE || requirePublishedTreeUrl('VERIFY-5-CAPABILITIES')).replace(/\/?$/,'/') const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..') // ★ MEASURE THIS BUILD, OR MEASURE NOTHING. Several worktrees run on one machine and a default port diff --git a/internal/verify-map-paint.mjs b/internal/verify-map-paint.mjs index 182f8b182..60ba885f4 100644 --- a/internal/verify-map-paint.mjs +++ b/internal/verify-map-paint.mjs @@ -34,6 +34,7 @@ import { requireThisTree } from './_serves-this-tree.mjs' import { resolve as _resolvePath, dirname as _dirnamePath } from 'node:path' import { fileURLToPath as _fileURLToPath } from 'node:url' import { TREE_NAME } from './_tree-name.mjs' +import { requirePublishedTreeUrl } from './_published-base.mjs' // The SwiftShader launch. Without these a headless Chromium here has NO GL context at all. const GL_ARGS = [ @@ -44,7 +45,7 @@ const GL_ARGS = [ '--no-sandbox', ] -const BASE = process.env.BASE || `https://nuxtbase.app/plugin-assets/${TREE_NAME}` +const BASE = process.env.BASE || requirePublishedTreeUrl('VERIFY-MAP-PAINT') const ROOT = _resolvePath(_dirnamePath(_fileURLToPath(import.meta.url)), '..') // ★ MEASURE THIS BUILD, OR MEASURE NOTHING. Several worktrees run on one machine and a default port @@ -121,7 +122,7 @@ console.log('MAP-PAINT: GL ok —', gl.version, '/', gl.renderer) * ★ SAY WHICH TREE WAS MEASURED (2026-09-25) — this gate defaults to the DEPLOYED site, and a * failure line that does not say so reads as "your code is broken" when it means "your code is * not deployed yet". MEASURED TWICE IN ONE DAY, both times wrongly: resources-map and - * connectivity-map both reported FAIL ("the stylesheet is ABSENT here") against nuxtbase.app + * connectivity-map both reported FAIL ("the stylesheet is ABSENT here") against the live test host * while the fix for exactly that defect was sitting, verified, on disk. Served locally, the * same gate passed all 13 leaves. * diff --git a/vite.doxa-maps-build-config.json b/vite.doxa-maps-build-config.json index 2a4e65994..214ccab34 100644 --- a/vite.doxa-maps-build-config.json +++ b/vite.doxa-maps-build-config.json @@ -28,11 +28,11 @@ "from the api, no problem \u2014 if they want to set a mapbox sk they can'. This is token-ladder", "RUNG 3 (see internal/generate-tree.mjs:595): rungs 1-3 are used DIRECTLY with no network call,", "so baking it makes every map work out of the box on ANY host \u2014 including one with no", - "/api/maps/token endpoint and no CORS on the fallback, which is exactly why nuxtbase.app maps", + "/api/maps/token endpoint and no CORS on the fallback, which is exactly why maps on the live test host", "were blank. SOURCE OF TRUTH: https://doxa.life/api/maps/token (see", "_qa-cycle/01-ideation-research/REFERENCE-mapbox-token-endpoint.md). Re-fetch from there rather", "than copying this literal around. A pk. is a PUBLIC key protected by URL restrictions, not by", "secrecy \u2014 never put an sk. here; check:public-hygiene still fails the build on sk./tk." ], - "//tokenUrl-2026-09-22": "EMPTIED 2026-09-22. It pointed at https://doxa.life/api/maps/token, which serves a URL-RESTRICTED token \u2014 tiles 403 from every origin except doxa.life, so the basemap rendered as an empty void while pins drew fine (the Driver's exact report: 'it's loading the pins, but it's not loading the actual underlying map'). That endpoint sits ABOVE the baked token in the ladder, so leaving it set would fetch the broken token and overwrite the good one. Measured: the baked token = 200 from no-Referer, nuxtbase.app AND localhost; the domain-restricted token = 403 from all three." + "//tokenUrl-2026-09-22": "EMPTIED 2026-09-22. It pointed at https://doxa.life/api/maps/token, which serves a URL-RESTRICTED token \u2014 tiles 403 from every origin except doxa.life, so the basemap rendered as an empty void while pins drew fine (the Driver's exact report: 'it's loading the pins, but it's not loading the actual underlying map'). That endpoint sits ABOVE the baked token in the ladder, so leaving it set would fetch the broken token and overwrite the good one. Measured: the baked token = 200 from no-Referer, the live test host AND localhost; the domain-restricted token = 403 from all three." }