/** 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; }