doxa-maps/deployment/cloud/vite.cloud.config.mjs
Doxa Maps c9caf68e44 Public Studio: the raw plugin registry is refused too, so the gated bundle's name stays private
In ACCESS=public mode the guard refused a gated bundle's source and its /__leaf/ pages, but not
/__plugin-gallery/registry.json. That file is the raw discovery list for the smoke tools, and it still
names key-cities, its tag and its two profiles to any visitor. No data, but a gated plugin must not reach a
public surface. The Studio reads only manifest.json, which already leaves gated bundles out, so the
registry is now refused in public mode as well. Dev mode is unchanged, and the smoke tools that read the
registry run against a dev server.
Measured with the cloud config run locally:
- before: public mode served registry.json (200) naming key-cities;
- after: public mode answers 404, including with a query string and a percent-encoded name;
- manifest.json and / still answer 200 in public mode, and dev mode still serves registry.json.
2026-09-29 13:54:30 +00:00

91 lines
4.5 KiB
JavaScript

// Overlay for the cloud dev server: the repo's own vite.config.js plus what a shared, public
// host needs. Every value comes from the environment (see deployment/cloud/README.md).
import base from '/app/repo/vite.config.js'
import { mergeConfig } from 'vite'
import { readFileSync } from 'node:fs'
import { GATED_PROFILES } from '/app/repo/internal/_tree-name.mjs'
const env = process.env
const SETUP_STATE = '/tmp/rudder-setup.state' // written by entry.sh: running -> done | failed
const setupState = () => { try { return readFileSync(SETUP_STATE, 'utf8').trim() } catch { return '' } }
// Rudder's setup runs in the background (entry.sh), so the site serves before the engine exists. While it
// runs, /api/rudder answers "starting" (the widget shows "Loading speech model…"; /events is held open so it
// does not retry in a loop). When setup ends, the dev server restarts ONCE: the repo's backend plugin only
// looks for the engine binary at start, and the restart is what starts it. 'pre' puts this ahead of that
// plugin's own "engine offline" answer.
const rudderStarting = {
name: 'cloud-rudder-starting',
enforce: 'pre',
apply: 'serve',
configureServer(server) {
if (setupState() !== 'running') return
server.middlewares.use('/api/rudder', (req, res, next) => {
if (setupState() !== 'running') return next()
const status = { engine: 'starting', error: '', audio: 'browser' }
if ((req.url || '').startsWith('/events')) {
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' })
res.write(`data: ${JSON.stringify({ type: 'status', status })}\n\n`)
return
}
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify(status))
})
const poll = setInterval(() => {
if (setupState() === 'running') return
clearInterval(poll)
server.config.logger.info(`[rudder] setup ${setupState() || 'ended'}: restarting the dev server to start the engine`)
server.restart()
}, 2000)
server.httpServer?.once('close', () => clearInterval(poll))
},
}
// ACCESS=public (entry.sh sets DX_PUBLIC_STUDIO=1): Vite owns the public port with no sign-in in front of it.
// - The backend's paths go to admin.mjs, whose gates decide: /cdn/ and /api-prod/api/ public, /pb/ records
// by PocketBase's own rules, and /pb/_/, _superusers and /admin/ for the admin password.
// - GATED bundles (gatedProfiles, e.g. key-cities) must never reach a public surface. The Studio already leaves
// them out of its list; here their source and dev pages are refused too, so a typed URL can't load them. So is
// /__plugin-gallery/registry.json, the raw discovery list for smoke tools: it still names the gated bundles and
// their profiles, and the Studio itself reads only manifest.json.
// - /__open-in-editor (Vite's own) would start an editor process on this server for a visitor: refused.
const PUBLIC = env.DX_PUBLIC_STUDIO === '1'
const publicGuard = {
name: 'cloud-public-guard',
enforce: 'pre',
apply: 'serve',
configureServer(server) {
if (!PUBLIC) return
server.middlewares.use((req, res, next) => {
let p = (req.url || '').split('?')[0]
try { p = decodeURIComponent(p) } catch { /* keep it raw */ }
const gated = GATED_PROFILES.some((g) => p.includes(`/plugin-designs/${g}/`) || p === `/__leaf/${g}` || p.startsWith(`/__leaf/${g}/`))
if (!gated && !p.startsWith('/__open-in-editor') && p !== '/__plugin-gallery/registry.json') return next()
res.statusCode = 404
res.setHeader('content-type', 'text/plain; charset=utf-8')
res.end('not found\n')
})
},
}
export default async (e) => {
const b = typeof base === 'function' ? await base(e) : base
const hmr = env.HMR_CLIENT_PORT
? { clientPort: Number(env.HMR_CLIENT_PORT), protocol: env.HMR_PROTOCOL || 'wss' }
: undefined
return mergeConfig(b, {
plugins: [publicGuard, rudderStarting],
cacheDir: '/tmp/vite-cache',
server: {
host: env.VITE_HOST || '0.0.0.0',
port: Number(env.VITE_PORT || 5173),
strictPort: true,
allowedHosts: true,
...(hmr ? { hmr } : {}),
...(PUBLIC ? { proxy: { '^/(cdn|api-prod|pb|admin)/': { target: 'http://127.0.0.1:5175', ws: true } } } : {}),
// Public: the checkout's git internals are not served either (the private history, .git/config). Vite's
// defaults are restated because setting deny replaces them.
...(PUBLIC ? { fs: { strict: true, deny: ['.env', '.env.*', '*.{crt,pem}', '**/.git/**'] } } : {}),
},
})
}