verae-zapier-simulator/src/server.js

87 lines
2.9 KiB
JavaScript

/**
* Local Zapier interface simulator + trace console.
* Default http://0.0.0.0:3847
*/
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { ACTIONS, Simulator } from './pipeline.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PUBLIC = path.join(__dirname, '..', 'public');
const PORT = Number(process.env.SIM_PORT || 3847);
const BIND = process.env.SIM_BIND || '0.0.0.0';
const world = new Simulator();
const mime = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.svg': 'image/svg+xml',
};
function send(res, code, body, type = 'application/json; charset=utf-8') {
const buf = typeof body === 'string' ? body : JSON.stringify(body, null, 2);
res.writeHead(code, { 'content-type': type, 'cache-control': 'no-store' });
res.end(buf);
}
function readBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf8');
if (!raw) return resolve({});
try {
resolve(JSON.parse(raw));
} catch (err) {
reject(err);
}
});
req.on('error', reject);
});
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://127.0.0.1:${PORT}`);
try {
if (req.method === 'GET' && url.pathname === '/health') {
return send(res, 200, { ok: true, port: PORT });
}
if (req.method === 'GET' && url.pathname === '/api/actions') {
return send(res, 200, { actions: ACTIONS, state: world.snapshot() });
}
if (req.method === 'GET' && url.pathname === '/api/state') {
return send(res, 200, world.snapshot());
}
if (req.method === 'POST' && url.pathname === '/api/reset') {
world.reset();
return send(res, 200, { ok: true, state: world.snapshot() });
}
if (req.method === 'POST' && url.pathname === '/api/run') {
const body = await readBody(req);
const result = await world.run(body);
return send(res, 200, result);
}
let file = url.pathname === '/' ? '/index.html' : url.pathname;
const dest = path.normalize(path.join(PUBLIC, file));
if (!dest.startsWith(PUBLIC)) {
return send(res, 403, { error: 'forbidden' });
}
if (!fs.existsSync(dest) || fs.statSync(dest).isDirectory()) {
return send(res, 404, { error: 'not found' });
}
const ext = path.extname(dest);
res.writeHead(200, { 'content-type': mime[ext] || 'application/octet-stream' });
fs.createReadStream(dest).pipe(res);
} catch (err) {
send(res, 500, { error: err.message });
}
});
server.listen(PORT, BIND, () => {
process.stdout.write(`Zapier simulator http://${BIND}:${PORT}/\n`);
});