Files
picpeak/dev/webhook-receiver/server.js
T
2026-07-07 08:56:39 +02:00

94 lines
2.7 KiB
JavaScript

// Tiny dev-only webhook receiver. Logs every request as one JSON line per
// hit so the E2E spec can poll the log file (or hit GET /requests to read
// from memory). Holds the last 200 requests in a ring buffer.
//
// Endpoints:
// POST / — accept any webhook; records and returns 200
// GET /requests — returns the ring buffer as JSON
// POST /reset — clear the ring buffer
// GET /health — 200 ok
//
// Configurable response status via FORCE_STATUS env (e.g. 500 to test retries).
const http = require('http');
const PORT = parseInt(process.env.PORT || '8888', 10);
const RING_SIZE = parseInt(process.env.RING_SIZE || '200', 10);
const FORCE_STATUS = parseInt(process.env.FORCE_STATUS || '200', 10);
const ring = [];
function readBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
let total = 0;
req.on('data', (chunk) => {
total += chunk.length;
if (total > 1024 * 1024) {
reject(new Error('payload too large'));
return;
}
chunks.push(chunk);
});
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
req.on('error', reject);
});
}
const server = http.createServer(async (req, res) => {
if (req.method === 'GET' && req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
return;
}
if (req.method === 'GET' && req.url === '/requests') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(ring));
return;
}
if (req.method === 'POST' && req.url === '/reset') {
ring.length = 0;
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('cleared');
return;
}
// Treat every other request as a webhook delivery to record.
let body = '';
try {
body = await readBody(req);
} catch (err) {
res.writeHead(413, { 'Content-Type': 'text/plain' });
res.end(err.message);
return;
}
const entry = {
receivedAt: new Date().toISOString(),
method: req.method,
url: req.url,
headers: req.headers,
body,
};
ring.push(entry);
if (ring.length > RING_SIZE) ring.shift();
// Log a single line so docker logs gives a quick readable trace.
process.stdout.write(
`[webhook-receiver] ${req.method} ${req.url} sig=${
req.headers['x-picpeak-signature'] || '-'
} type=${(() => {
try { return JSON.parse(body)?.type || '-'; } catch { return '-'; }
})()}\n`
);
res.writeHead(FORCE_STATUS, { 'Content-Type': 'text/plain' });
res.end(FORCE_STATUS >= 200 && FORCE_STATUS < 300 ? 'ok' : 'forced-failure');
});
server.listen(PORT, () => {
process.stdout.write(`webhook-receiver listening on :${PORT}\n`);
});