// hightrusted CAPTURE — Webhook Receiver (Express) // // Empfängt capture.ready / capture.failed Events, verifiziert die HMAC-Signatur, // lädt das fertige PDF herunter und legt es im Archiv ab. // // Use-Case: // Du sendest Capture-Anfragen mit mode=webhook. Statt zu pollen, bekommst du // den fertigen Capture per HTTP-POST geliefert. Dieser Server nimmt das // entgegen, validiert und archiviert. // // Setup: // npm install express @hightrusted/capture // export HIGHTRUSTED_API_KEY=ht_live_... // export HIGHTRUSTED_WEBHOOK_SECRET=wh_secret_... // node server.mjs import express from 'express'; import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { Client, verifyWebhookSignature } from '@hightrusted/capture'; const PORT = process.env.PORT ?? 8000; const WEBHOOK_SECRET = process.env.HIGHTRUSTED_WEBHOOK_SECRET; const ARCHIVE_DIR = process.env.ARCHIVE_DIR ?? './archiv'; if (!WEBHOOK_SECRET) { console.error('HIGHTRUSTED_WEBHOOK_SECRET fehlt — siehe Dashboard.'); process.exit(1); } const client = new Client(); // API-Key aus ENV const app = express(); // WICHTIG: raw body brauchen wir für die HMAC-Verifikation. // Express muss den Body als Buffer durchreichen, nicht parsen. app.post( '/webhooks/capture', express.raw({ type: 'application/json', limit: '1mb' }), async (req, res) => { const signature = req.header('X-Hightrusted-Signature') ?? ''; if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) { console.warn('[%s] Signature mismatch', new Date().toISOString()); return res.status(401).json({ error: 'invalid_signature' }); } let payload; try { payload = JSON.parse(req.body.toString('utf-8')); } catch { return res.status(400).json({ error: 'invalid_json' }); } const { event, capture } = payload; console.log('[%s] %s — %s', new Date().toISOString(), event, capture?.id); // Sofort 200 — der Rest läuft asynchron, sonst rennt der Webhook in Timeout res.status(200).json({ ok: true }); if (event === 'capture.ready') { try { await archiveCapture(capture); } catch (err) { console.error('Archive-Fehler:', err); } } else if (event === 'capture.failed') { console.error('Capture failed: ref=%s reason=%s', capture.reference, payload.error?.code); // → hier: Mandant benachrichtigen, ggf. retry } } ); app.get('/health', (_req, res) => res.json({ ok: true })); app.listen(PORT, () => { console.log(`Webhook-Receiver lauscht auf :${PORT}/webhooks/capture`); }); // ───────────────────────────────────────────────────────────────── async function archiveCapture(capture) { const date = new Date(capture.timestamp.issued_at).toISOString().slice(0, 10); const dir = join(ARCHIVE_DIR, date); await mkdir(dir, { recursive: true }); const ref = capture.reference ?? capture.id.slice(0, 8); const safe = ref.replace(/[^a-zA-Z0-9_-]/g, '_'); const pdfPath = join(dir, `${safe}_${capture.id.slice(0, 8)}.pdf`); await client.downloadPdf(capture.id, pdfPath); await writeFile( `${pdfPath}.json`, JSON.stringify(capture, null, 2), 'utf-8' ); console.log(' → archiviert: %s', pdfPath); }