library-api.mjs 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
  1. /** Same-origin read service. Production requires a gateway token, kept server-side. */
  2. import { spawn } from 'node:child_process';
  3. import { createServer } from 'node:http';
  4. import { timingSafeEqual } from 'node:crypto';
  5. import { fileURLToPath } from 'node:url';
  6. import path from 'node:path';
  7. import { unifiedReader } from './unified-worker.mjs';
  8. const ROOT=path.dirname(fileURLToPath(import.meta.url));
  9. export function createLibraryApi({db,registry,unifiedConfig=process.env.KSF_UNIFIED_DB_CONFIG,python=process.env.KSF_LIBRARY_PYTHON||'python',localPreview=false,token=process.env.KSF_LIBRARY_API_TOKEN}={}) {
  10. let active=0;
  11. const unified = unifiedConfig ? unifiedReader({ python, unifiedConfig }) : null;
  12. const handler = async (req,res)=>{
  13. const send=(status,data)=>{res.writeHead(status,{'content-type':'application/json; charset=utf-8','cache-control':'no-store','x-content-type-options':'nosniff'});res.end(JSON.stringify(data));};
  14. if(req.method!=='POST')return send(405,{ok:false,error:{code:'METHOD_NOT_ALLOWED'}});
  15. const incoming=Buffer.from(req.headers.authorization||''),expected=Buffer.from('Bearer '+(token||''));
  16. if(!localPreview&&(!token||incoming.length!==expected.length||!timingSafeEqual(incoming,expected)))return send(401,{ok:false,error:{code:'AUTH_REQUIRED'}});
  17. if(active>=4)return send(429,{ok:false,error:{code:'LIBRARY_BUSY'}});
  18. let counted=false;
  19. try {
  20. let size=0;const chunks=[];
  21. for await(const part of req){size+=part.length;if(size>24000)throw Error('REQUEST_TOO_LARGE');chunks.push(part);}
  22. const request=JSON.parse(Buffer.concat(chunks).toString('utf8'));
  23. if(!request||typeof request!=='object'||Array.isArray(request))throw Error('INVALID_REQUEST');
  24. if(!db&&!registry&&!unifiedConfig)return send(503,{ok:false,error:{code:'DATABASE_NOT_INSTALLED'}});
  25. if(active>=4)return send(429,{ok:false,error:{code:'LIBRARY_BUSY'}});
  26. active++;counted=true;
  27. const data=unified ? await unified('library',{unifiedConfig},request) : await new Promise((resolve,reject)=>{
  28. const child=spawn(python,['-B','-X','utf8',path.join(ROOT,'voc-library/board_library.py')],{windowsHide:true,stdio:['pipe','pipe','pipe']});
  29. let output='',bytes=0;const timeout=setTimeout(()=>{child.kill();reject(Error('LIBRARY_TIMEOUT'));},25000);
  30. child.stdout.setEncoding('utf8');
  31. child.stdout.on('data',chunk=>{bytes+=Buffer.byteLength(chunk);if(bytes>5000000){child.kill();reject(Error('RESPONSE_TOO_LARGE'));}else output+=chunk;});
  32. child.stderr.resume();child.stdin.on('error',()=>{});
  33. child.on('error',()=>{clearTimeout(timeout);reject(Error('LIBRARY_RUNTIME_UNAVAILABLE'));});
  34. child.on('close',()=>{clearTimeout(timeout);try{resolve(JSON.parse(output));}catch{reject(Error('LIBRARY_READ_FAILED'));}});
  35. child.stdin.end(JSON.stringify({config:{db,registry},request}));
  36. });
  37. return send(data.ok?200:400,{...data,localPreview});
  38. }catch(e){const code=['REQUEST_TOO_LARGE','INVALID_REQUEST','LIBRARY_TIMEOUT','LIBRARY_RUNTIME_UNAVAILABLE','RESPONSE_TOO_LARGE'].includes(e.message)?e.message:'LIBRARY_READ_FAILED';return send(400,{ok:false,error:{code}});}
  39. finally{if(counted)active--;}
  40. };
  41. handler.close = () => unified?.close();
  42. return handler;
  43. }
  44. if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
  45. if(!process.env.KSF_LIBRARY_API_TOKEN)throw Error('KSF_LIBRARY_API_TOKEN_REQUIRED');
  46. if(process.env.KSF_UNIFIED_DB_CONFIG&&(process.env.KSF_VOC_DB||process.env.KSF_LIBRARY_REGISTRY))throw Error('MULTIPLE_DATA_BINDINGS');
  47. const handler = createLibraryApi({db:process.env.KSF_VOC_DB,registry:process.env.KSF_LIBRARY_REGISTRY});
  48. const server = createServer(handler);
  49. server.on('close', handler.close);
  50. server.listen(Number(process.env.KSF_LIBRARY_PORT||4180),'127.0.0.1');
  51. const close = () => { handler.close(); server.close(); setTimeout(() => process.exit(1), 30000).unref(); };
  52. process.on('SIGTERM',close); process.on('SIGINT',close);
  53. }