// Node.js 22+. Synthetic-event demo; set ISURVEY_WEBHOOK_SECRET via a secret store. const {createHmac, timingSafeEqual} = require('node:crypto'); const {createServer} = require('node:http'); const assert = require('node:assert/strict'); function verify(body, timestamp, signature, secret, now = Date.now()) { if (typeof timestamp !== 'string' || !/^\d{1,12}$/.test(timestamp) || Math.abs(now - Number(timestamp) * 1000) > 300000 || typeof signature !== 'string' || !/^v1=[a-f0-9]{64}$/.test(signature)) return false; const expected = createHmac('sha256', secret).update(timestamp + '.').update(body).digest(); return timingSafeEqual(expected, Buffer.from(signature.slice(3), 'hex')); } if (process.argv.includes('--self-test')) { const body = Buffer.from('{"id":"test-1","test":true}'); const timestamp = '1700000000', secret = 'synthetic-secret'; const signature = 'v1=' + createHmac('sha256', secret).update(timestamp + '.').update(body).digest('hex'); assert(verify(body, timestamp, signature, secret, 1700000000000)); assert(!verify(Buffer.from('{}'), timestamp, signature, secret, 1700000000000)); assert(!verify(body, timestamp, signature, secret, 1700000300001)); assert(!verify(body, timestamp, signature + 'f', secret, 1700000000000)); console.log('Signature checks passed'); } else if (require.main === module) { const secret = process.env.ISURVEY_WEBHOOK_SECRET; if (!secret) throw new Error('Set ISURVEY_WEBHOOK_SECRET before starting the receiver'); // ponytail: process-local deduplication for synthetic demo only; use a durable // unique event ID and transactionally enqueue work before accepting live data. const accepted = new Set(); createServer(async (req, res) => { if (req.method !== 'POST' || req.url !== '/webhook') { res.writeHead(404).end(); return; } const chunks = []; let bytes = 0; try { for await (const chunk of req) { bytes += chunk.length; if (bytes > 1048576) { res.writeHead(413).end(); req.destroy(); return; } chunks.push(chunk); } const body = Buffer.concat(chunks); if (!verify(body, req.headers['x-isurvey-timestamp'], req.headers['x-isurvey-signature'], secret)) { res.writeHead(401).end(); return; } const event = JSON.parse(body.toString('utf8')); if (event.test !== true || typeof event.id !== 'string' || event.id.length > 128) { res.writeHead(422).end('Synthetic events only'); return; } if (!accepted.has(event.id) && accepted.size >= 1000) { res.writeHead(503).end('Restart demo receiver'); return; } accepted.add(event.id); res.writeHead(204).end(); } catch { res.writeHead(400).end(); } }).listen(3000, '127.0.0.1', () => console.log('Synthetic receiver: http://127.0.0.1:3000/webhook; use your own public HTTPS forwarding endpoint for delivery tests.')); } module.exports = {verify};