Zur Dokumentation springen
SDK

Eine Zustellung verifizieren

`verifyWebhookSignature`: in konstanter Zeit, mit einem Replay-Fenster und dem geparsten Event zurück.

In einem Request-Handler

Eine Webhook-URL ist öffentlich. Alles im Internet kann JSON in der richtigen Form dorthin POSTen; ein Handler, der payload.type liest, ohne die Signatur zu prüfen, ist eine offene Schreib-API.

webhook-handler.ts
import { verifyWebhookSignature } from '@openemail/sdk' export async function POST(request: Request) {  try {    const event = await verifyWebhookSignature({      payload: await request.text(),      headers: request.headers,      secret: process.env.OPENEMAIL_WEBHOOK_SECRET!,      toleranceSeconds: 300,    })     console.log(event.type, event.data)  } catch {    return new Response('bad signature', { status: 400 })  }   return new Response(null, { status: 204 })}

Übergeben wird der ROHE Body. Parsen und erneutes Serialisieren ändert Schlüsselreihenfolge und Whitespace, und die Signatur passt dann nicht mehr. headers nimmt ein Headers-Objekt oder ein einfaches Objekt wie Nodes req.headers entgegen, und X-OpenEmail-Signature wird unabhängig von der Schreibweise gefunden.

Zwei Dinge, die das hier erledigt und die eine selbstgebaute Prüfung meist nicht leistet: Es vergleicht den MAC in konstanter Zeit, sodass sich das korrekte Präfix nicht über die Laufzeit rekonstruieren lässt, und es weist eine Zustellung zurück, die in eine der beiden Richtungen mehr als toleranceSeconds alt ist – fünf Minuten, sofern nicht anders angegeben –, sodass eine mitgeschnittene Anfrage nicht für immer wiederholbar ist. toleranceSeconds: 0 schaltet die Replay-Prüfung ab. Beide Fehler sind stumm. Ein Handler mit einem von beiden besteht jeden Test, auf den man zu schreiben käme.

Es wirft bei jedem Fehlschlag: bei einem fehlenden X-OpenEmail-Signature-Header, bei einem, der nicht die Form t=<seconds>,v1=<hex> hat, bei einem Zeitstempel außerhalb des Fensters oder bei einer Signatur, die nicht passt. Bei Erfolg löst es sich zu dem als WebhookPayload geparsten Body auf, es gibt also kein zweites JSON.parse, das man falsch machen könnte. Ein Typargument wie EmailOpenedData übergeben, um data zu typisieren.

Es benötigt globalThis.crypto.subtle, das Node 20+, Bun, Deno und Cloudflare Workers alle bereitstellen.