Ir a la documentación
SDK

Verificar una entrega

`verifyWebhookSignature`: en tiempo constante, con una ventana de repetición, y devuelve el evento ya analizado.

En un handler de solicitudes

La URL de un webhook es pública. Cualquier cosa en internet puede hacerle POST de un JSON con la forma correcta, así que un handler que lee payload.type sin comprobar la firma es una API de escritura abierta.

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 })}

Pasa el cuerpo EN CRUDO. Analizarlo y volver a serializarlo cambia el orden de las claves y los espacios, y la firma no coincidirá. headers acepta un objeto Headers o un objeto plano como el req.headers de Node, y X-OpenEmail-Signature se encuentra sea cual sea su capitalización.

Dos cosas que esto resuelve y que una comprobación casera no suele resolver: compara el MAC en tiempo constante, de modo que el prefijo correcto no puede recuperarse midiendo tiempos, y rechaza una entrega con más de toleranceSeconds de antigüedad en cualquiera de las dos direcciones, cinco minutos salvo que digas otra cosa, así que una solicitud capturada no se puede repetir para siempre. toleranceSeconds: 0 desactiva la comprobación de repetición. Ambos fallos son silenciosos. Un handler con cualquiera de los dos pasa todas las pruebas que se te ocurriría escribir.

Lanza un error ante cualquier fallo: una cabecera X-OpenEmail-Signature ausente, una que no tenga la forma t=<seconds>,v1=<hex>, una marca de tiempo fuera de la ventana o una firma que no coincide. Si tiene éxito, resuelve con el cuerpo analizado como un WebhookPayload, así que no hay un segundo JSON.parse en el que equivocarse. Pasa un argumento de tipo como EmailOpenedData para tipar data.

Necesita globalThis.crypto.subtle, que proporcionan Node 20+, Bun, Deno y Cloudflare Workers.