doxa-maps/deployment/cloud/vite.cloud.config.mjs
Doxa Maps 28c4bacd2a Public Studio: the checkout's .git is not served (fs.deny), with Vite's defaults kept
The resolved config in ACCESS=public (Vite 5.4.21) had strict file serving with /app/repo as the only
allowed root, and a deny list of Vite's defaults (.env, .env.*, certificates). The .git folder sat
inside the allowed root and was not denied, so a public Studio could serve the repository's git
internals. In public mode the deny list adds **/.git/**; the defaults are restated because setting
deny replaces them.
2026-09-29 05:31:37 +00:00

89 lines
4.3 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.
// - /__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')) 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/**'] } } : {}),
},
})
}