import http from 'node:http'; import { randomBytes, timingSafeEqual } from 'node:crypto'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; // Expose only built assets and the application API, with a temporary password. const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../apps/web/dist'); export function createPreviewServer({password=process.env.PREVIEW_PASSWORD || randomBytes(12).toString('base64url')}={}) { const sessions = new Map(); let attempts = []; const equal = (a, b) => { const x=Buffer.from(a), y=Buffer.from(b); return x.length===y.length && timingSafeEqual(x,y); }; const loginPage = (failed=false) => `Beryl Agent · 测试登录

Beryl Agent

受密码保护的临时测试入口

${failed?'

密码不正确,请重试。

':''}

仅供受邀测试者使用。登录有效期 8 小时。

`; const types = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css', '.svg': 'image/svg+xml', '.png': 'image/png', '.woff2': 'font/woff2' }; return http.createServer(async (req, res) => { res.setHeader('Cache-Control','no-store'); res.setHeader('X-Content-Type-Options','nosniff'); res.setHeader('Referrer-Policy','no-referrer'); res.setHeader('X-Frame-Options','DENY'); const externalLoginNavigation=req.method==='GET' && ['/', '/__preview/login'].includes(req.url) && req.headers['sec-fetch-site']==='cross-site' && req.headers['sec-fetch-dest']==='document'; if(externalLoginNavigation) {res.writeHead(200,{'Content-Type':'text/html; charset=utf-8'});return res.end(loginPage());} if ((req.headers.origin && req.headers.origin !== `https://${req.headers.host}`) || req.headers['sec-fetch-site']==='cross-site' || (!['GET','HEAD'].includes(req.method) && req.headers.origin !== `https://${req.headers.host}`)) { res.writeHead(403); return res.end('Origin denied'); } let pathname; try { pathname = decodeURIComponent(new URL(req.url, 'http://localhost').pathname); } catch { res.writeHead(400); return res.end(); } if(pathname==='/__preview/login' && req.method==='POST') { attempts=attempts.filter(t=>Date.now()-t<60000); if(attempts.length>=10) {res.writeHead(429,{'Retry-After':'60'});return res.end('Too many attempts; retry in one minute');} attempts.push(Date.now()); if(!req.headers['content-type']?.startsWith('application/x-www-form-urlencoded')) {res.writeHead(415);return res.end();} let body=''; try {for await(const chunk of req) {body+=chunk;if(Buffer.byteLength(body)>2048){res.writeHead(413);res.end();return;}}} catch {res.writeHead(400);return res.end();} if(!equal(new URLSearchParams(body).get('password')??'',password)) {res.writeHead(401,{'Content-Type':'text/html; charset=utf-8'});return res.end(loginPage(true));} for(const [key,expiry] of sessions) if(expiry=100) {res.writeHead(503);return res.end('Session limit reached');} const token=randomBytes(32).toString('base64url'); sessions.set(token,Date.now()+8*3600000); res.writeHead(303,{'Location':'/','Set-Cookie':`__Host-beryl_preview=${token}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=28800`});return res.end(); } const token=(req.headers.cookie??'').split(';').map(x=>x.trim()).find(x=>x.startsWith('__Host-beryl_preview='))?.slice('__Host-beryl_preview='.length); if(!token || (sessions.get(token)??0)<=Date.now()) { if((pathname==='/'||pathname==='/__preview/login')&&['GET','HEAD'].includes(req.method)) {res.writeHead(200,{'Content-Type':'text/html; charset=utf-8'});return res.end(req.method==='HEAD'?undefined:loginPage());} res.writeHead(401,{'Content-Type':'application/json'});return res.end(JSON.stringify({error:'PREVIEW_LOGIN_REQUIRED',message:'请打开网站首页输入访问密码'})); } if(pathname==='/__preview/logout' && req.method==='POST') { sessions.delete(token); res.writeHead(303,{'Location':'/','Set-Cookie':'__Host-beryl_preview=; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=0'});return res.end(); } if(pathname==='/__preview/login' && req.method==='GET') { res.writeHead(200,{'Content-Type':'text/html; charset=utf-8'});return res.end('测试访问管理

已登录 Beryl Agent

进入 Agent

'); } if (pathname.startsWith('/api/') || pathname === '/health') { const headers = { ...req.headers, host: 'localhost:3001' }; delete headers.authorization; delete headers.cookie; const upstream = http.request({ hostname: '127.0.0.1', port: 3001, path: req.url, method: req.method, headers }, response => { const safeHeaders = { ...response.headers, 'cache-control': 'no-store' }; delete safeHeaders['access-control-allow-origin']; res.writeHead(response.statusCode, safeHeaders); response.pipe(res); }); upstream.on('error', () => { if (!res.headersSent) res.writeHead(502); res.end('Backend unavailable'); }); res.on('close', () => upstream.destroy()); req.pipe(upstream); return; } if (!['GET', 'HEAD'].includes(req.method)) { res.writeHead(405); return res.end(); } const relative = pathname === '/' ? 'index.html' : pathname.slice(1); const file = path.resolve(root, relative); if (!file.startsWith(root + path.sep) || relative.split('/').some(p => p.startsWith('.'))) { res.writeHead(404); return res.end(); } try { const data = await readFile(file); res.writeHead(200, { 'Content-Type': types[path.extname(file)] || 'application/octet-stream', 'X-Content-Type-Options': 'nosniff', 'Cache-Control': 'no-store' }); res.end(req.method === 'HEAD' ? undefined : data); } catch { res.writeHead(404); res.end('Not found'); } }); } if(process.argv[1] && path.resolve(process.argv[1])===fileURLToPath(import.meta.url)) { const password=process.env.PREVIEW_PASSWORD || randomBytes(12).toString('base64url'); createPreviewServer({password}).listen(4173, '127.0.0.1', () => { console.log('Preview listening on 127.0.0.1:4173'); console.log('Login: password form (no username required)'); console.log('Password: ' + password); }); }