industry-api.mjs 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445
  1. /** Read-only industry projection, sharing the library's server-side access model. */
  2. import { spawn } from 'node:child_process';
  3. import { timingSafeEqual } from 'node:crypto';
  4. import { fileURLToPath } from 'node:url';
  5. import { unifiedReader } from './unified-worker.mjs';
  6. const PROGRAM = fileURLToPath(new URL('./voc-library/industry_monitor.py', import.meta.url));
  7. export function createIndustryApi({ db, registry, snapshot, unifiedConfig = process.env.KSF_UNIFIED_DB_CONFIG, monitoringStore, policyUsageReview, policyUsageSha256, python = process.env.KSF_LIBRARY_PYTHON || 'python3', localPreview = false, token = process.env.KSF_LIBRARY_API_TOKEN } = {}) {
  8. let active = 0;
  9. const unified = unifiedConfig ? unifiedReader({ python, unifiedConfig }) : null;
  10. const handler = async (req, res) => {
  11. const send = (status, body) => { res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store', 'x-content-type-options': 'nosniff' }); res.end(JSON.stringify(body)); };
  12. if (req.method !== 'POST') return send(405, { ok: false, error: { code: 'METHOD_NOT_ALLOWED' } });
  13. const given = Buffer.from(req.headers.authorization || ''), expected = Buffer.from('Bearer ' + (token || ''));
  14. if (!localPreview && (!token || given.length !== expected.length || !timingSafeEqual(given, expected))) return send(401, { ok: false, error: { code: 'AUTH_REQUIRED' } });
  15. if (!db && !registry && !snapshot && !unifiedConfig) return send(503, { ok: false, error: { code: 'DATABASE_NOT_INSTALLED' } });
  16. if (active >= 3) return send(429, { ok: false, error: { code: 'LIBRARY_BUSY' } });
  17. let counted = false;
  18. try {
  19. let size = 0; const chunks = [];
  20. for await (const part of req) { size += part.length; if (size > 12000) throw Error('REQUEST_TOO_LARGE'); chunks.push(part); }
  21. const request = JSON.parse(Buffer.concat(chunks).toString('utf8'));
  22. if (!request || typeof request !== 'object' || Array.isArray(request)) throw Error('INVALID_REQUEST');
  23. if (active >= 3) return send(429, { ok: false, error: { code: 'LIBRARY_BUSY' } });
  24. active++; counted = true;
  25. const data = unified ? await unified('industry', {unifiedConfig, monitoringStore, policyUsageReview, policyUsageSha256}, request) : await new Promise((resolve, reject) => {
  26. const child = spawn(python, ['-B', '-X', 'utf8', PROGRAM], { windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] });
  27. let output = '', bytes = 0;
  28. const timer = setTimeout(() => { child.kill(); reject(Error('LIBRARY_TIMEOUT')); }, 25000);
  29. child.stdout.setEncoding('utf8');
  30. child.stdout.on('data', part => { bytes += Buffer.byteLength(part); if (bytes > 5000000) { child.kill(); reject(Error('RESPONSE_TOO_LARGE')); } else output += part; });
  31. child.stderr.resume(); child.stdin.on('error', () => {});
  32. child.on('error', () => { clearTimeout(timer); reject(Error('LIBRARY_RUNTIME_UNAVAILABLE')); });
  33. child.on('close', () => { clearTimeout(timer); try { resolve(JSON.parse(output)); } catch { reject(Error('INDUSTRY_READ_FAILED')); } });
  34. child.stdin.end(JSON.stringify({ config: { db, registry, snapshot, monitoringStore, policyUsageReview, policyUsageSha256 }, request }));
  35. });
  36. return send(data.ok ? 200 : 400, { ...data, localPreview });
  37. } catch (error) {
  38. const code = ['REQUEST_TOO_LARGE', 'INVALID_REQUEST', 'LIBRARY_TIMEOUT', 'RESPONSE_TOO_LARGE', 'LIBRARY_RUNTIME_UNAVAILABLE'].includes(error.message) ? error.message : 'INDUSTRY_READ_FAILED';
  39. return send(400, { ok: false, error: { code } });
  40. } finally { if (counted) active--; }
  41. };
  42. handler.close = () => unified?.close();
  43. return handler;
  44. }