import fs from 'node:fs/promises' import path from 'node:path' import { compileReplacementModule, REPLACEMENT_MODULE_EXTENSIONS, } from '../../../contrast-bundler/src/replacementModule.ts' import { isLoopbackHost } from '../backend-origin.ts' import type { IncomingMessage, ServerResponse } from 'node:http' export const REPLACEMENT_MODULE_ROUTE = '/__sootsim-replacement-module' export interface ReplacementModuleHandlerOptions { projectRoot?: string requireLoopbackOrigin?: boolean requestToken?: string } function sendJavaScript(req: IncomingMessage, res: ServerResponse, source: string): void { const origin = req.headers.origin let allowedOrigin: string | undefined if (origin) { try { if (isLoopbackHost(new URL(origin).hostname)) allowedOrigin = origin } catch {} } res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8', 'Cache-Control': 'no-cache', ...(allowedOrigin ? { 'Access-Control-Allow-Origin': allowedOrigin, Vary: 'Origin' } : {}), }) res.end(req.method === 'HEAD' ? undefined : source) } export function isReplacementModuleRequestUrl(rawUrl?: string): boolean { if (!rawUrl) return false try { const pathname = new URL(rawUrl, 'http://rnx.local').pathname return pathname === REPLACEMENT_MODULE_ROUTE } catch { return false } } export async function handleReplacementModuleRequest( req: IncomingMessage, res: ServerResponse, options: ReplacementModuleHandlerOptions = {}, ): Promise { const rawUrl = req.url || '' let url: URL try { url = new URL(rawUrl, 'http://rnx.local') } catch { return false } if (options.requireLoopbackOrigin) { const origin = req.headers.origin const remoteAddress = req.socket.remoteAddress?.replace(/^::ffff:/, '') let allowed = Boolean(remoteAddress && isLoopbackHost(remoteAddress)) if (allowed && origin) { try { allowed = isLoopbackHost(new URL(origin).hostname) } catch { allowed = false } } if (!allowed) { res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' }) res.end('replacement modules are available only to loopback origins') return true } } if (url.pathname !== REPLACEMENT_MODULE_ROUTE) return false if (options.requestToken && url.searchParams.get('token') !== options.requestToken) { res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' }) res.end('replacement module request token is invalid') return true } const targetPath = url.searchParams.get('path') if (!targetPath) { res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' }) res.end('missing path param') return true } const extension = path.extname(targetPath).toLowerCase() if (!REPLACEMENT_MODULE_EXTENSIONS.some((allowed) => allowed === extension)) { res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' }) res.end(`replacement files must be ${REPLACEMENT_MODULE_EXTENSIONS.join(', ')}`) return true } if (!path.isAbsolute(targetPath)) { res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' }) res.end('replacement file path must be absolute') return true } const runtimePackages = new Set(url.searchParams.getAll('runtimePackage')) let resolvedProjectRoot: string | undefined if (options.projectRoot) { const lexicalProjectRoot = path.resolve(options.projectRoot) resolvedProjectRoot = await fs.realpath(options.projectRoot) const lexicalTarget = path.relative(lexicalProjectRoot, targetPath) if (lexicalTarget.startsWith('..') || path.isAbsolute(lexicalTarget)) { res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' }) res.end('replacement file must be inside the project root') return true } } let resolvedTargetPath: string try { resolvedTargetPath = await fs.realpath(targetPath) } catch (error) { const message = error instanceof Error ? error.message : String(error) res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }) res.end(`replacement file not found: ${message}`) return true } if (resolvedProjectRoot) { const relativeTarget = path.relative(resolvedProjectRoot, resolvedTargetPath) if (relativeTarget.startsWith('..') || path.isAbsolute(relativeTarget)) { res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' }) res.end('replacement file must be inside the project root') return true } } try { const compiled = compileReplacementModule({ filePath: resolvedTargetPath, source: await fs.readFile(resolvedTargetPath, 'utf8'), runtimePackages, }) // inline the map: the file is the app author's own source, and the // simulator page is the only thing that ever loads it. const map = Buffer.from(JSON.stringify(compiled.sourceMap), 'utf8').toString('base64') sendJavaScript( req, res, `${compiled.code}\n//# sourceMappingURL=data:application/json;base64,${map}\n`, ) } catch (error) { const message = error instanceof Error ? error.message : String(error) res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' }) res.end(`replacement compile failed: ${message}`) } return true }