From 551df57915c39bde6f56ed4c0e9ff3f36670bfae Mon Sep 17 00:00:00 2001 From: doxa-maps Date: Tue, 29 Sep 2026 04:48:47 +0000 Subject: [PATCH] New check: check:origins, no deployment address baked into the site bun scripts/check-runtime-origins.ts (bun run check:origins) holds the branch's rule: - nuxt.config declares neither per-deployment address; - under app/, only the runtime loader reads dxMapsBase, and only it and the menu read researchGalleryUrl; - no source file or static page names a deployment host (nuxtbase.app, sslip.io, a *.localhost address or a 46xx laptop port). It refuses to pass on fewer than 50 files. This branch: PASS, 306 files. The subdomain branch it was cut from fails with 12 problems: two addresses in nuxt.config, six reads under app/, and the hardcoded maps host in nuxt.config and in the two static pages. --- package.json | 3 +- scripts/check-runtime-origins.ts | 82 ++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 scripts/check-runtime-origins.ts diff --git a/package.json b/package.json index 4467e8fd..49e147c9 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "start": "bun run .output/server/index.mjs", "postinstall": "nuxt prepare", "lint": "eslint .", - "typecheck": "nuxt typecheck" + "typecheck": "nuxt typecheck", + "check:origins": "bun scripts/check-runtime-origins.ts" }, "dependencies": { "@aws-sdk/client-s3": "^3.914.0", diff --git a/scripts/check-runtime-origins.ts b/scripts/check-runtime-origins.ts new file mode 100644 index 00000000..8668e876 --- /dev/null +++ b/scripts/check-runtime-origins.ts @@ -0,0 +1,82 @@ +#!/usr/bin/env bun +// check-runtime-origins: no deployment's address is baked into this site. +// +// The plugin-studio branch sends readers to the Research Gallery and loads its maps from the gallery's +// /cdn. Both addresses differ per deployment, so the server reads them at REQUEST time +// (server/utils/dx-runtime.ts), and pages ask /api/dx-runtime or link through /research-gallery. About +// 4,000 pages are prerendered, and a prerendered page carries the build's config. An address that +// reaches nuxt.config's runtimeConfig, or a page, is baked into the build, which is how the CDN branch +// failed. This check holds the line: +// 1. nuxt.config.ts declares neither address; +// 2. under app/, only app/utils/dxRuntime.ts touches dxMapsBase, and only it, app/utils/nav.ts and +// app/components/public/TheHeader.vue touch researchGalleryUrl (the menu's key, resolved to +// /research-gallery); +// 3. no source file or static page names a deployment host (nuxtbase.app, sslip.io, a *.localhost +// address or a 46xx laptop port), since those belong in the environment. +// Static: reads files, starts nothing. `bun scripts/check-runtime-origins.ts [root]`. Exit 0 pass, 1 fail. +import { readdirSync, readFileSync, statSync } from 'node:fs' +import { join, relative, resolve } from 'node:path' + +const ROOT = resolve(process.argv[2] || join(import.meta.dir, '..')) +const fails: string[] = [] +let scanned = 0 + +function walk(dir: string, out: string[] = []): string[] { + for (const e of readdirSync(dir)) { + if (e === 'node_modules' || e.startsWith('.')) continue + const p = join(dir, e) + if (statSync(p).isDirectory()) walk(p, out) + else if (/\.(vue|ts|js|mjs|html)$/.test(e)) out.push(p) + } + return out +} +const rel = (p: string) => relative(ROOT, p) +const lines = (text: string, re: RegExp) => + text.split('\n').map((l, i) => [i + 1, l] as const).filter(([, l]) => re.test(l)) + +// 1. nuxt.config.ts declares neither address. +const config = readFileSync(join(ROOT, 'nuxt.config.ts'), 'utf8') +scanned += 1 +for (const [n, l] of lines(config, /\b(dxMapsBase|researchGalleryUrl)\s*:/)) { + fails.push(`nuxt.config.ts:${n} declares a per-deployment address in the build's config: ${l.trim()}`) +} + +// 2. Only the runtime loader (and the menu's key) touch the two names under app/. +const ALLOWED: Record = { + dxMapsBase: ['app/utils/dxRuntime.ts'], + researchGalleryUrl: ['app/utils/dxRuntime.ts', 'app/utils/nav.ts', 'app/components/public/TheHeader.vue'] +} +for (const file of walk(join(ROOT, 'app'))) { + scanned += 1 + const text = readFileSync(file, 'utf8') + for (const [name, allowed] of Object.entries(ALLOWED)) { + if (allowed.includes(rel(file))) continue + for (const [n, l] of lines(text, new RegExp(`\\b${name}\\b`))) { + fails.push(`${rel(file)}:${n} reads ${name} outside the runtime loader (use loadDxRuntime or /research-gallery): ${l.trim()}`) + } + } +} + +// 3. No deployment host in source or static pages (data under public/assets is not scanned). +const HOST = /(\bnuxtbase\.app\b|\bsslip\.io\b|\b[a-z0-9-]+\.localhost\b|localhost:46\d\d)/ +const sources = [ + ...walk(join(ROOT, 'app')), ...walk(join(ROOT, 'server')), ...walk(join(ROOT, 'modules')), + join(ROOT, 'nuxt.config.ts'), + ...walk(join(ROOT, 'public')).filter((f) => f.endsWith('.html') && !rel(f).startsWith('public/assets/')) +] +for (const file of sources) { + scanned += 1 + for (const [n, l] of lines(readFileSync(file, 'utf8'), HOST)) { + fails.push(`${rel(file)}:${n} names a deployment host (it belongs in the environment): ${l.trim().slice(0, 160)}`) + } +} + +// Refuse to pass vacuously: the walk must have read the site. +if (scanned < 50) fails.push(`only ${scanned} file(s) scanned under ${ROOT}: the walk is broken or the root is wrong`) + +if (fails.length) { + console.error(`RUNTIME-ORIGINS: FAIL — ${fails.length} problem(s)`) + for (const f of fails) console.error(` ✗ ${f}`) + process.exit(1) +} +console.log(`RUNTIME-ORIGINS: PASS — ${scanned} files; no deployment address in the build's config, the pages or the static pages`)