import { createServer } from 'node:http'; import { readFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { randomUUID } from 'node:crypto'; import { runTriage } from './workflow.mjs'; import { applyDecisions,digest } from './core.mjs'; const root=fileURLToPath(new URL('../',import.meta.url)); const port=Number(process.env.PORT??4317); const allowedHosts=new Set([`127.0.0.1:${port}`,`localhost:${port}`]); const staticFiles=new Map([['/',['index.html','text/html']],['/app.js',['app.js','text/javascript']],['/style.css',['style.css','text/css']]]); let currentRun=null,currentIssues=null,busy=false; const revisions=new Map(); function json(response,status,data){response.writeHead(status,{'Content-Type':'application/json; charset=utf-8','Cache-Control':'no-store'});response.end(JSON.stringify(data));} async function body(request){ if(!request.headers['content-type']?.startsWith('application/json'))throw new Error('Use application/json.'); let bytes=0,chunks=[]; for await(const chunk of request){bytes+=chunk.length;if(bytes>512000)throw new Error('Request body too large.');chunks.push(chunk);} try{return JSON.parse(Buffer.concat(chunks).toString('utf8'));}catch{throw new Error('Invalid JSON.');} } const server=createServer(async(request,response)=>{ response.setHeader('X-Content-Type-Options','nosniff'); response.setHeader('Content-Security-Policy',"default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'"); response.setHeader('Referrer-Policy','no-referrer'); if(!allowedHosts.has(request.headers.host)){json(response,403,{error:'Unsupported host.'});return;} const origin=request.headers.origin; if(origin&&origin!==`http://${request.headers.host}`){json(response,403,{error:'Cross-origin requests are disabled.'});return;} try{ const pathname=new URL(request.url,`http://${request.headers.host}`).pathname; if(request.method==='GET'&&staticFiles.has(pathname)){ const [name,type]=staticFiles.get(pathname);response.writeHead(200,{'Content-Type':type+'; charset=utf-8'});response.end(await readFile(root+'public/'+name));return; } if(request.method==='GET'&&pathname==='/api/sample'){json(response,200,JSON.parse(await readFile(root+'data/sample.json','utf8')));return;} if(request.method==='GET'&&pathname==='/api/evaluation'){ let result;try{result=JSON.parse(await readFile(root+'evidence/evaluation.json','utf8'));}catch{result={available:false};}json(response,200,result);return; } if(request.method==='POST'&&pathname==='/api/triage'){ if(busy){json(response,409,{error:'A local inference run is already in progress.'});return;} busy=true; try{const input=await body(request);const result=await runTriage(input);currentRun=result;currentIssues=result.items.map(({id,title,body,labels})=>({id,title,body,labels}));revisions.clear();json(response,200,result);}finally{busy=false;}return; } if(request.method==='POST'&&pathname==='/api/apply'){ if(busy)throw new Error('Wait for the current triage run.'); const input=await body(request); if(!currentRun||currentRun.runId!==input.runId)throw new Error('This preview is stale. Run triage again.'); const before=structuredClone(currentIssues),after=applyDecisions(currentIssues,input.decisions),revisionId=randomUUID(); revisions.set(revisionId,{before,afterHash:digest(after),count:input.decisions.length});currentIssues=after; json(response,200,{revisionId,issues:after,appliedCount:input.decisions.length});return; } if(request.method==='POST'&&pathname==='/api/undo'){ if(busy)throw new Error('Wait for the current triage run.'); const input=await body(request),revision=revisions.get(input.revisionId); if(!revision||digest(currentIssues)!==revision.afterHash)throw new Error('Undo conflicts with newer changes or the revision is no longer available.'); currentIssues=structuredClone(revision.before);revisions.delete(input.revisionId);json(response,200,{issues:currentIssues,restoredCount:revision.count});return; } json(response,404,{error:'Not found.'}); }catch(error){json(response,400,{error:error.message});} }); server.listen(port,'127.0.0.1',()=>console.log(`Slow Triage is local at http://127.0.0.1:${port}. No external API calls.`));