| 123456789101112131415161718192021222324252627282930313233343536373839404142434445 |
- /** Read-only industry projection, sharing the library's server-side access model. */
- import { spawn } from 'node:child_process';
- import { timingSafeEqual } from 'node:crypto';
- import { fileURLToPath } from 'node:url';
- import { unifiedReader } from './unified-worker.mjs';
- const PROGRAM = fileURLToPath(new URL('./voc-library/industry_monitor.py', import.meta.url));
- 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 } = {}) {
- let active = 0;
- const unified = unifiedConfig ? unifiedReader({ python, unifiedConfig }) : null;
- const handler = async (req, res) => {
- 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)); };
- if (req.method !== 'POST') return send(405, { ok: false, error: { code: 'METHOD_NOT_ALLOWED' } });
- const given = Buffer.from(req.headers.authorization || ''), expected = Buffer.from('Bearer ' + (token || ''));
- if (!localPreview && (!token || given.length !== expected.length || !timingSafeEqual(given, expected))) return send(401, { ok: false, error: { code: 'AUTH_REQUIRED' } });
- if (!db && !registry && !snapshot && !unifiedConfig) return send(503, { ok: false, error: { code: 'DATABASE_NOT_INSTALLED' } });
- if (active >= 3) 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 > 12000) 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 (active >= 3) return send(429, { ok: false, error: { code: 'LIBRARY_BUSY' } });
- active++; counted = true;
- const data = unified ? await unified('industry', {unifiedConfig, monitoringStore, policyUsageReview, policyUsageSha256}, request) : await new Promise((resolve, reject) => {
- const child = spawn(python, ['-B', '-X', 'utf8', PROGRAM], { windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] });
- let output = '', bytes = 0;
- const timer = setTimeout(() => { child.kill(); reject(Error('LIBRARY_TIMEOUT')); }, 25000);
- child.stdout.setEncoding('utf8');
- child.stdout.on('data', part => { bytes += Buffer.byteLength(part); if (bytes > 5000000) { child.kill(); reject(Error('RESPONSE_TOO_LARGE')); } else output += part; });
- child.stderr.resume(); child.stdin.on('error', () => {});
- child.on('error', () => { clearTimeout(timer); reject(Error('LIBRARY_RUNTIME_UNAVAILABLE')); });
- child.on('close', () => { clearTimeout(timer); try { resolve(JSON.parse(output)); } catch { reject(Error('INDUSTRY_READ_FAILED')); } });
- child.stdin.end(JSON.stringify({ config: { db, registry, snapshot, monitoringStore, policyUsageReview, policyUsageSha256 }, request }));
- });
- return send(data.ok ? 200 : 400, { ...data, localPreview });
- } catch (error) {
- const code = ['REQUEST_TOO_LARGE', 'INVALID_REQUEST', 'LIBRARY_TIMEOUT', 'RESPONSE_TOO_LARGE', 'LIBRARY_RUNTIME_UNAVAILABLE'].includes(error.message) ? error.message : 'INDUSTRY_READ_FAILED';
- return send(400, { ok: false, error: { code } });
- } finally { if (counted) active--; }
- };
- handler.close = () => unified?.close();
- return handler;
- }
|