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.
This commit is contained in:
doxa-maps 2026-09-29 04:48:47 +00:00
parent 5dea900183
commit 80b6cd5775
2 changed files with 84 additions and 1 deletions

View file

@ -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",

View file

@ -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<string, string[]> = {
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`)