| 1234567891011121314151617181920212223242526272829303132333435363738394041424344 |
- /** Same-origin read service. Production requires a gateway token, kept server-side. */
- import { spawn } from 'node:child_process';
- import { createServer } from 'node:http';
- import { timingSafeEqual } from 'node:crypto';
- import { fileURLToPath } from 'node:url';
- import path from 'node:path';
- const ROOT=path.dirname(fileURLToPath(import.meta.url));
- export function createLibraryApi({db,registry,python=process.env.KSF_LIBRARY_PYTHON||'python',localPreview=false,token=process.env.KSF_LIBRARY_API_TOKEN}={}) {
- let active=0;
- return async (req,res)=>{
- 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));};
- if(req.method!=='POST')return send(405,{ok:false,error:{code:'METHOD_NOT_ALLOWED'}});
- const incoming=Buffer.from(req.headers.authorization||''),expected=Buffer.from('Bearer '+(token||''));
- if(!localPreview&&(!token||incoming.length!==expected.length||!timingSafeEqual(incoming,expected)))return send(401,{ok:false,error:{code:'AUTH_REQUIRED'}});
- if(active>=4)return send(429,{ok:false,error:{code:'LIBRARY_BUSY'}});
- let counted=false;
- try {
- let size=0;const chunks=[];
- for await(const part of req){size+=part.length;if(size>24000)throw Error('REQUEST_TOO_LARGE');chunks.push(part);}
- const request=JSON.parse(Buffer.concat(chunks).toString('utf8'));
- if(!request||typeof request!=='object'||Array.isArray(request))throw Error('INVALID_REQUEST');
- if(!db&&!registry)return send(503,{ok:false,error:{code:'DATABASE_NOT_INSTALLED'}});
- if(active>=4)return send(429,{ok:false,error:{code:'LIBRARY_BUSY'}});
- active++;counted=true;
- const data=await new Promise((resolve,reject)=>{
- const child=spawn(python,['-B','-X','utf8',path.join(ROOT,'voc-library/board_library.py')],{windowsHide:true,stdio:['pipe','pipe','pipe']});
- let output='',bytes=0;const timeout=setTimeout(()=>{child.kill();reject(Error('LIBRARY_TIMEOUT'));},25000);
- child.stdout.setEncoding('utf8');
- child.stdout.on('data',chunk=>{bytes+=Buffer.byteLength(chunk);if(bytes>5000000){child.kill();reject(Error('RESPONSE_TOO_LARGE'));}else output+=chunk;});
- child.stderr.resume();child.stdin.on('error',()=>{});
- child.on('error',()=>{clearTimeout(timeout);reject(Error('LIBRARY_RUNTIME_UNAVAILABLE'));});
- child.on('close',()=>{clearTimeout(timeout);try{resolve(JSON.parse(output));}catch{reject(Error('LIBRARY_READ_FAILED'));}});
- child.stdin.end(JSON.stringify({config:{db,registry},request}));
- });
- return send(data.ok?200:400,{...data,localPreview});
- }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}});}
- finally{if(counted)active--;}
- };
- }
- if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
- if(!process.env.KSF_LIBRARY_API_TOKEN)throw Error('KSF_LIBRARY_API_TOKEN_REQUIRED');
- createServer(createLibraryApi({db:process.env.KSF_VOC_DB,registry:process.env.KSF_LIBRARY_REGISTRY})).listen(Number(process.env.KSF_LIBRARY_PORT||4180),'127.0.0.1');
- }
|