// Instance control uses the existing management transport, independently of channels. const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; const one = (req, key) => req.headersDistinct[key]?.length === 1 ? req.headersDistinct[key][0] : null; const json = (res, status, value) => { res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(value)); }; const errors = { 'human-required': 403, 'read-only': 403, 'processing-state-invalid': 409, 'processing-control-unsafe': 409, 'processing-uninitialized': 409, 'processing-stop-pending': 409, 'processing-generation-changed': 409, 'not-configured': 503, 'outcome-unknown': 503 }; function answer(value) { if (!value || typeof value !== 'object' || Array.isArray(value) || ![true, false, null].includes(value.halted) || (value.invalid !== true && (value.version !== 1 || typeof value.generation !== 'string' || !UUID.test(value.generation))) || (value.quiescent !== undefined && typeof value.quiescent !== 'boolean')) throw Error('invalid receipt'); return Object.fromEntries(['version', 'generation', 'halted', 'invalid', 'code', 'at', 'quiescent', 'remaining', 'stopped', 'stopping', 'recovered'].filter(k => Object.hasOwn(value, k)).map(k => [k, value[k]])); } export async function processingHTTP(req, res, { path, search, processing, named, send, preview }) { const write = req.method === 'POST'; const refuse = (status, code, sent = false) => json(res, status, { error: code, message: code === 'outcome-unknown' ? 'Outcome unknown. Read processing status. Nothing was retried.' : 'Processing control refused.', ...(write ? { sent } : {}) }); if (req.method !== 'GET' && !write) return refuse(405, 'method-not-allowed'); if (search) return refuse(400, 'invalid-request'); if (!write && path !== '/api/processing') return refuse(405, 'method-not-allowed'); let action = 'read', args = {}; if (write) { if (!send || !named || !preview) return refuse(403, 'read-only'); if (preview.sso !== true && one(req, preview.header) !== preview.user) return refuse(401, 'unauthenticated'); if (one(req, 'origin') !== (preview.origin ?? `https://${preview.host}`)) return refuse(403, 'cross-origin'); if (one(req, 'sec-fetch-site') !== 'same-origin') return refuse(403, 'cross-site'); if (one(req, 'content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json') return refuse(415, 'invalid-request'); if (!['/api/processing/halt', '/api/processing/resume'].includes(path)) return refuse(404, 'not-found'); action = path.endsWith('/halt') ? 'halt' : 'resume'; try { let size = 0; const chunks = []; for await (const chunk of req) { size += chunk.length; if (size > 1024) throw Error(); chunks.push(chunk); } args = JSON.parse(Buffer.concat(chunks).toString('utf8')); if (!args || typeof args !== 'object' || Array.isArray(args)) throw Error(); if (action === 'halt' ? Object.keys(args).length !== 0 : Object.keys(args).length !== 1 || typeof args.generation !== 'string' || !UUID.test(args.generation)) throw Error(); } catch { return refuse(400, 'invalid-request'); } } if (typeof processing?.[action] !== 'function') return refuse(503, 'not-configured'); try { const value = answer(await processing[action](args)); return json(res, write && value.quiescent === false ? 202 : 200, value); } catch (e) { const code = Object.hasOwn(errors, e.code) ? e.code : 'outcome-unknown'; return refuse(errors[code], code, code === 'outcome-unknown' ? 'unknown' : false); } }