Files
stack/packages/webui/src/processing-http.mjs
T
rocko 4bdc7e0e86 processing: inspectable per-run stop seam for lifecycle fire
Unify halt and fire cancellation by verified run identity with durable first-owner receipts and shared deadlines. Integrate the instance control with the current host, library and tester-socket paths. Focused nine-file evidence is 62 passing tests; broad suites, mutants and review remain pending.
2026-10-21 22:42:12 -05:00

51 lines
3.6 KiB
JavaScript

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