// helpers for serving the installed sootsim engine runtime from framework // dev servers. the runtime is managed by `sootsim runtime install` and lives // under ~/.rnx/runtimes/. import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' import { isValidRuntimeVersion } from '@contrast/runtime-delivery' import { readActiveRuntime, runtimeDir } from './home-paths.ts' export const SOOTSIM_RUNTIME_MISSING_MESSAGE = '[rnx] no engine runtime installed. run `rnx runtime install`' // --- dev-checkout mode boundary -------------------------------------------- // // there is exactly one rule for which engine gets served: INSIDE the Contrast // monorepo dev checkout the fresh dev-stack build (public/sootsim, kept current // by `bun dev`'s watch:sootsim) IS the engine; OUTSIDE it (a published CLI in a // user's project, or an isolated home) the installed runtime IS. this is a mode // boundary, not a preference or a runtime fallback chain — the check is // structural (the monorepo layout is present next to this module) and // deterministic (the dev build's files exist). it exists because the daemon and // the /__soot plugin otherwise serve ~/.rnx/runtimes/, a PUBLISHED // runtime that `bun dev` never refreshes, so local engine edits are invisible // until a manual publish — the silent trap that cost a full night of // already-fixed bugs re-reproducing. // // the isolated CI headless path (scripts/local-publish-runtime.sh) sets // RNX_HOME to a dedicated dir precisely to validate the published-runtime // code path; keying off that EXISTING isolation env keeps it (and packaged // electron with a custom home) on the installed runtime. let _monorepoRootCache: string | null | undefined function findMonorepoRoot(): string | null { if (_monorepoRootCache !== undefined) return _monorepoRootCache let dir = path.dirname(fileURLToPath(import.meta.url)) // walk up until we find the Contrast monorepo: it has packages/sootsim-engine and // a public/sootsim dev-build output slot. bounded by the filesystem root. for (let i = 0; i < 12 && dir !== path.dirname(dir); i++) { if ( fs.existsSync(path.join(dir, 'packages/sootsim-engine/package.json')) && fs.existsSync(path.join(dir, 'public/sootsim')) ) { _monorepoRootCache = dir return dir } dir = path.dirname(dir) } _monorepoRootCache = null return null } /** the fresh dev-stack engine build to serve when running inside the Contrast * monorepo dev checkout, or null when the installed runtime should serve * (published CLI, packaged electron, or an isolated RNX_HOME). */ export function resolveDevCheckoutRuntimeRoot(): string | null { // an explicit home that has a STAGED/active runtime is the isolation the // CI/local-publish path sets up to validate the installed-runtime code path — // honor that staged runtime. an explicit home with NO active runtime is just // an isolated dev daemon (own lockfile, doesn't clobber ~/.rnx), which // still wants the dev build; and the default home (RNX_HOME unset) always // does. keying on "explicit home + a runtime deliberately staged there" keeps // it structural, not a bespoke toggle. if (process.env.RNX_HOME && readActiveRuntime()) return null const root = findMonorepoRoot() if (!root) return null const devBuild = path.join(root, 'public/sootsim') // require the runtime shape the installed runtime also has, so serving it is // a drop-in (index.html shell + engine/ + engine-tenant/worker.js tenant). if ( fs.existsSync(path.join(devBuild, 'index.html')) && fs.existsSync(path.join(devBuild, 'engine/index.html')) && fs.existsSync(path.join(devBuild, 'engine-tenant/worker.js')) ) { return devBuild } return null } const MIME_TYPES: Record = { '.js': 'application/javascript', '.mjs': 'application/javascript', '.css': 'text/css', '.html': 'text/html', '.wasm': 'application/wasm', '.json': 'application/json', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.glb': 'model/gltf-binary', '.ttf': 'font/ttf', '.otf': 'font/otf', '.woff': 'font/woff', '.woff2': 'font/woff2', '.mp3': 'audio/mpeg', '.wav': 'audio/wav', } const ROOT_RUNTIME_PATHS = [ '/assets/', '/engine/', '/engine-tenant/', '/photos/', '/three-mode/', '/fonts/', '/icons/', '/sounds/', '/spike/', '/test-wallpaper.jpg', '/preview-sw.js', '/sootsim.svg', ] export function resolveActiveRuntimeRoot(): string | null { // mode boundary: inside the dev checkout the dev-stack build is the engine. const devRoot = resolveDevCheckoutRuntimeRoot() if (devRoot) return devRoot const active = readActiveRuntime() if (!active) return null const dir = runtimeDir(active) if (!fs.existsSync(path.join(dir, 'index.html'))) return null return dir } const RUNTIME_VERSION_HOSTNAME_SUFFIX = '.runtime.localhost' export function runtimeVersionHostname(version: string): string { const trimmed = version.trim() if (!isValidRuntimeVersion(trimmed)) { throw new Error(`invalid rnx runtime version: ${version}`) } const encoded = Buffer.from(trimmed, 'utf8').toString('hex') const labels = encoded.match(/.{1,60}/g) if (!labels) throw new Error(`invalid rnx runtime version: ${version}`) return `${labels.join('.')}${RUNTIME_VERSION_HOSTNAME_SUFFIX}` } export function isRuntimeVersionHostname(hostname: string): boolean { return hostname.toLowerCase().endsWith(RUNTIME_VERSION_HOSTNAME_SUFFIX) } export function runtimeVersionFromHostname(hostname: string): string | null { if (!isRuntimeVersionHostname(hostname)) return null const labels = hostname.slice(0, -RUNTIME_VERSION_HOSTNAME_SUFFIX.length).split('.') if (labels.length === 0 || labels.some((label) => !/^[0-9a-f]{1,60}$/i.test(label))) { return null } const encoded = labels.join('') if (encoded.length % 2 !== 0) return null try { const version = Buffer.from(encoded, 'hex').toString('utf8') return isValidRuntimeVersion(version) ? version : null } catch { return null } } export function resolveInstalledRuntimeRoot(version: string): string | null { const dir = runtimeDir(version) if (!fs.existsSync(path.join(dir, 'index.html'))) return null return dir } /** select one immutable installed runtime for a version-owned origin. ordinary * localhost requests follow the active runtime; a malformed or unavailable * version origin fails closed instead of borrowing active assets. */ export function resolveRuntimeRootForHostname(hostname: string): string | null { if (!isRuntimeVersionHostname(hostname)) return resolveActiveRuntimeRoot() const version = runtimeVersionFromHostname(hostname) return version ? resolveInstalledRuntimeRoot(version) : null } export function resolveRuntimeRootForRequestHost( host: string | undefined, ): string | null { if (!host) return resolveActiveRuntimeRoot() try { return resolveRuntimeRootForHostname(new URL(`http://${host}`).hostname) } catch { return null } } export function isRootRuntimeAssetPath(pathname: string): boolean { return ROOT_RUNTIME_PATHS.some((p) => pathname === p || pathname.startsWith(p)) } export function resolveRuntimeFilePath( runtimeRoot: string, pathname: string, ): string | null { if (!pathname.startsWith('/')) return null if (pathname.includes('\0') || pathname.includes('\\')) return null for (const segment of pathname.split('/')) { if (segment === '..') return null } const fullPath = path.resolve(runtimeRoot, pathname.replace(/^\/+/, '')) const rootWithSep = runtimeRoot.endsWith(path.sep) ? runtimeRoot : runtimeRoot + path.sep if (!fullPath.startsWith(rootWithSep) && fullPath !== runtimeRoot) return null if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) return null return fullPath } export function serveRuntimeFile(req: any, res: any, fullPath: string) { const ext = path.extname(fullPath) const stat = fs.statSync(fullPath) const lastModified = stat.mtime.toUTCString() res.setHeader('content-type', MIME_TYPES[ext] || 'application/octet-stream') if (/^canvaskit-graphite-pipelines-[0-9a-f]{8}\.bin$/.test(path.basename(fullPath))) { res.setHeader('cache-control', 'public, max-age=31536000, immutable') fs.createReadStream(fullPath).pipe(res) return } // these urls are STABLE across dev rebuilds and runtime version switches // (worker.js, canvaskit-graphite.wasm, the html shells are unhashed), so the // browser must revalidate every load — the old year-long immutable here made every // new tab run a stale cached engine-tenant worker.js against a fresh engine, // which also broke `do reload` ("bridge never reconnected"). no-cache + // last-modified keeps repeat loads cheap 304s on localhost. res.setHeader('cache-control', 'no-cache') res.setHeader('last-modified', lastModified) if (req?.headers?.['if-modified-since'] === lastModified) { res.statusCode = 304 res.end() return } fs.createReadStream(fullPath).pipe(res) }