Function verifyWebhookSignature

  • Verify a NessyAPI webhook signature. Throws NessyWebhookSignatureError on any failure — never returns false, so you can't accidentally skip a branch.

    The signature is HMAC-SHA256 over payload + "." + timestamp_bytes, hex- encoded, prefixed with sha256=. Timestamps prevent replay attacks within the toleranceSeconds window (default 5 minutes).

    Security notes:

    • Uses crypto.timingSafeEqual on equal-length Buffers (no byte-leakage via early-exit comparison).
    • Rejects non-Buffer payloads (defense against string-encoding confusion).
    • Explicit error throws — if (!valid) patterns are easy to miss.

    Parameters

    Returns void

    With .reason:

    • "bad_timestamp" — timestamp header isn't a finite number.
    • "stale_timestamp" — timestamp outside tolerance (replay attempt?).
    • "missing_prefix" — signature doesn't start with sha256=.
    • "length_mismatch" — hex digest has wrong length.
    • "mismatch" — HMAC didn't match (tampered payload / wrong secret).
    • "invalid_payload_type"payload wasn't a Buffer / Uint8Array.
    import express from "express";
    import { verifyWebhookSignature, NessyWebhookSignatureError } from "@nessyapi/sdk/webhook";

    app.post(
    "/webhooks/nessy",
    express.raw({ type: "application/json", limit: "1mb" }),
    (req, res) => {
    try {
    verifyWebhookSignature({
    payload: req.body,
    secret: process.env.NESSY_WEBHOOK_SECRET!,
    signatureHeader: req.header("x-nessyapi-signature-256") ?? "",
    timestampHeader: req.header("x-nessyapi-timestamp") ?? "",
    });
    } catch (err) {
    if (err instanceof NessyWebhookSignatureError) return res.status(401).end();
    throw err;
    }
    const event = JSON.parse(req.body.toString("utf8"));
    // handle event.event, event.data ...
    res.status(200).end();
    }
    );
    export async function POST(req: Request) {
    const payload = Buffer.from(await req.arrayBuffer());
    verifyWebhookSignature({
    payload,
    secret: process.env.NESSY_WEBHOOK_SECRET!,
    signatureHeader: req.headers.get("x-nessyapi-signature-256") ?? "",
    timestampHeader: req.headers.get("x-nessyapi-timestamp") ?? "",
    });
    // handle the parsed event
    return new Response(null, { status: 200 });
    }