// Přijímač webhooků POLITRAN (Node 18+, bez frameworku). Důležité: podpis se počítá nad SYROVÝM tělem. // POLITRAN_WEBHOOK_SECRET=whsec_... node server.mjs import { createServer } from 'node:http'; import { createHmac, timingSafeEqual } from 'node:crypto'; const secret = process.env.POLITRAN_WEBHOOK_SECRET; if (!secret) throw new Error('POLITRAN_WEBHOOK_SECRET missing'); const seen = new Set(); // v produkci DB/Redis s TTL function verify(header, rawBody) { const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('='))); if (!parts.t || !parts.v1 || Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false; const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex'); return expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1)); } createServer((req, res) => { if (req.method !== 'POST') { res.writeHead(405).end(); return; } const chunks = []; req.on('data', (c) => chunks.push(c)); req.on('end', () => { const raw = Buffer.concat(chunks).toString('utf8'); if (!verify(req.headers['x-politran-signature'] ?? '', raw)) { res.writeHead(401).end('bad signature'); return; } const event = JSON.parse(raw); if (seen.has(event.id)) { res.writeHead(200).end('duplicate'); return; } seen.add(event.id); res.writeHead(202).end('accepted'); // odpovědět do 5 s, zpracovat asynchronně setImmediate(() => { console.log(event.event, event.data?.notice?.short_code, '→', event.data?.notice?.status); // např. queue.push(event) }); }); }).listen(process.env.PORT ?? 8787, () => console.log('listening'));