npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@krovacloud/webhook

v0.1.7

Published

Verify Krova Cloud outbound webhook signatures — HMAC-SHA256, constant-time comparison, replay protection. Express, Next.js & Fastify helpers. Zero runtime dependencies.

Readme

@krovacloud/webhook

npm version npm downloads bundle size license types

Verify Krova Cloud outbound webhook signatures in Node.js — HMAC-SHA256, constant-time comparison, and replay protection. Zero runtime dependencies (built on node:crypto), ESM + CommonJS, first-class TypeScript types.

When Krova Cloud delivers an outbound webhook it signs the request so you can prove it genuinely came from Krova Cloud and was not tampered with in transit or replayed. This library performs exactly that verification.

Install

npm install @krovacloud/webhook
# or: pnpm add @krovacloud/webhook  /  yarn add @krovacloud/webhook

Requires Node.js 18 or newer.

How Krova Cloud signs a webhook

On every delivery, Krova Cloud sends these headers alongside the raw JSON body:

| Header | Example | Meaning | | ------------------- | ----------------------------------------- | -------------------------------- | | X-Krova-Signature | t=1700000000,v1=3f8a… | signed timestamp + HMAC-SHA256 | | X-Krova-Event | cube.provisioned | the event name | | X-Krova-Delivery | dlv_abc123 | unique id for this delivery |

The signature scheme

timestamp = floor(Date.now() / 1000)
signed    = `${timestamp}.${rawBody}`             // "t.body", literal dot separator
v1        = HMAC_SHA256(secret, signed)           // lowercase hex

X-Krova-Signature: t=<timestamp>,v1=<hex>
  • Algorithm — HMAC-SHA256, keyed with your webhook signing secret.
  • Signed content — the timestamp, a literal ., then the exact raw request body bytes.
  • Encoding — the digest is lowercase hex in the v1= field.
  • HeaderX-Krova-Signature: t=..,v1=... Fields may appear in any order; unknown fields are ignored, so future v2/v3 schemes stay backward compatible.
  • Replay window — the signed timestamp must be within 300 seconds (5 minutes) of the receiver's clock, in either direction. Configurable via toleranceSeconds.
  • Comparison — the recomputed digest is compared to v1 in constant time (crypto.timingSafeEqual), never with a short-circuiting ===.

To verify, you recompute HMAC_SHA256(secret, "${t}.${rawBody}"), compare it to v1 in constant time, and reject the request if the timestamp is outside the tolerance window to stop replays. This library does all of that for you.

The raw body matters. The signature is over the exact bytes Krova Cloud sent. If your framework parses the JSON and you re-serialize it, the bytes (and therefore the signature) will differ. Always verify against the raw request body.

Quickstart — Express

Mount express.raw({ type: "application/json" }) on the webhook route so the handler receives the raw Buffer, then use the krovaWebhook middleware:

import express from "express";
import { krovaWebhook } from "@krovacloud/webhook";

const app = express();

app.post(
  "/webhooks/krova",
  express.raw({ type: "application/json" }), // REQUIRED: gives us the raw body
  krovaWebhook({ secret: process.env.KROVA_WEBHOOK_SECRET! }),
  (req, res) => {
    // Signature already verified. Now it's safe to parse.
    const payload = JSON.parse(req.body.toString("utf8"));

    // The middleware attaches verified metadata:
    const { event, delivery, timestamp } = req.krovaWebhook!;
    console.log(`Received ${event} (${delivery}) signed at ${timestamp}`);

    res.status(200).json({ received: true });
  },
);

app.listen(3000);

On a bad signature, stale timestamp, or missing header the middleware responds 401 { "error": "<reason>" } and does not call next().

Quickstart — raw / framework-agnostic verify

Use verifyKrovaWebhook directly when you have the raw body and the signature header in hand (any framework, serverless, or a queue consumer):

import { verifyKrovaWebhook } from "@krovacloud/webhook";

// `signature` must be the single X-Krova-Signature header value (a string).
// Node's `req.headers[...]` is `string | string[] | undefined`, so coalesce
// to a string; a missing header safely yields `reason: "malformed_header"`.
const signatureHeader = req.headers["x-krova-signature"] ?? "";

const result = verifyKrovaWebhook({
  payload: rawBody,                       // string or Buffer — the raw body
  signature: signatureHeader,             // the X-Krova-Signature value
  secret: process.env.KROVA_WEBHOOK_SECRET!,
  // toleranceSeconds: 300,               // optional, defaults to 300 (5 min)
});

if (!result.valid) {
  // result.reason is "invalid_signature" | "timestamp_out_of_tolerance"
  //                | "malformed_header"
  throw new Error(`Krova webhook rejected: ${result.reason}`);
}

// Safe to trust and process the payload now.

Prefer exceptions? Use verifyKrovaWebhookOrThrow, which throws a KrovaWebhookError (with a .reason) instead of returning a result:

import { verifyKrovaWebhookOrThrow, KrovaWebhookError } from "@krovacloud/webhook";

try {
  verifyKrovaWebhookOrThrow({ payload, signature, secret });
} catch (err) {
  if (err instanceof KrovaWebhookError) {
    console.error("Rejected:", err.reason);
  }
  throw err;
}

Quickstart — Next.js Route Handler

In the App Router, read the raw body with await req.text() and pass the Headers object straight through to verifyKrovaRequest:

// app/api/webhooks/krova/route.ts
import { verifyKrovaRequest } from "@krovacloud/webhook";

export async function POST(req: Request) {
  const rawBody = await req.text(); // raw, un-parsed body
  const result = verifyKrovaRequest({
    payload: rawBody,
    headers: req.headers, // a standard Headers instance — matched case-insensitively
    secret: process.env.KROVA_WEBHOOK_SECRET!,
  });

  if (!result.valid) {
    return Response.json({ error: result.reason }, { status: 401 });
  }

  const payload = JSON.parse(rawBody);
  // result.event / result.delivery / result.timestamp are verified
  return Response.json({ received: true });
}

Quickstart — Fastify

Register a content-type parser that keeps the raw body, then verify:

import Fastify from "fastify";
import { verifyKrovaRequest } from "@krovacloud/webhook";

const app = Fastify();

app.addContentTypeParser(
  "application/json",
  { parseAs: "string" },
  (_req, body, done) => done(null, body), // keep the raw string
);

app.post("/webhooks/krova", (req, reply) => {
  const result = verifyKrovaRequest({
    payload: req.body as string,
    headers: req.headers,
    secret: process.env.KROVA_WEBHOOK_SECRET!,
  });
  if (!result.valid) {
    reply.code(401).send({ error: result.reason });
    return;
  }
  reply.send({ received: true });
});

API

verifyKrovaWebhook(options): VerifyResult

Non-throwing verification.

| Option | Type | Default | Description | | ------------------ | ------------------ | ------- | ------------------------------------------------- | | payload | string \| Buffer | — | The raw request body. | | signature | string | — | The X-Krova-Signature header value. | | secret | string | — | Your Krova Cloud webhook signing secret. | | toleranceSeconds | number | 300 | Max allowed clock skew before replay rejection. |

Returns { valid: true, timestamp } or { valid: false, reason, timestamp? }.

verifyKrovaWebhookOrThrow(options): { timestamp }

Same checks; throws KrovaWebhookError on failure instead of returning a result.

verifyKrovaRequest({ payload, headers, secret, toleranceSeconds? }): ({ valid: true } & KrovaWebhookContext) | VerifyFailure

Framework-agnostic helper. headers may be a Headers instance, a plain header object, or anything with a .get() method (header names are matched case-insensitively), so it works with Next.js Route Handlers, Fastify, plain node:http, and most other frameworks. On success it also returns the event and delivery values pulled from the X-Krova-Event / X-Krova-Delivery headers.

const result = verifyKrovaRequest({
  payload: rawBody, // string or Buffer
  headers: req.headers, // Headers | Record<string, string | string[]> | { get() }
  secret: process.env.KROVA_WEBHOOK_SECRET!,
});
if (result.valid) {
  // result.timestamp, result.event?, result.delivery?
}

krovaWebhook({ secret, toleranceSeconds? })

Express middleware. Requires the raw body (express.raw({ type: "application/json" })). Attaches req.krovaWebhook = { timestamp, event?, delivery? } on success; responds 401 with { error: <reason> } on failure.

parseSignatureHeader(header): { timestamp, signature }

Parses t=<timestamp>,v1=<hex>. Throws KrovaWebhookError("malformed_header") if either field is missing. Unknown fields are ignored (forward-compatible).

computeSignature(secret, timestamp, payload): string

Computes the lowercase-hex HMAC_SHA256(secret, "${timestamp}.${payload}"). Useful for tests and for signing in your own tooling.

KrovaWebhookError

Thrown by the throwing helpers. Has a .reason of "invalid_signature" | "timestamp_out_of_tolerance" | "malformed_header".

Security

  • Signatures are compared with crypto.timingSafeEqual — no early-exit string comparison that could leak timing information.
  • Stale requests are rejected via the timestamp tolerance window, mitigating replay attacks.
  • Always keep your webhook secret out of source control and rotate it if exposed. See SECURITY.md.

Related packages

Other official Krova Cloud libraries:

| Package | Language | Purpose | | -------------------------------------------------------------------------- | ---------- | ---------------------------------------- | | @krovacloud/sdk | TypeScript | The Krova Cloud API client for Node.js. | | @krovacloud/mcp | TypeScript | Model Context Protocol server for Krova. |

Requirements

  • Node.js 18 or newer. The library uses node:crypto (createHmac, timingSafeEqual) and has no other runtime dependencies.
  • Works in both ESM and CommonJS projects; TypeScript declarations are bundled.
  • Any runtime that provides the Node crypto API works (Node.js, Bun, and Deno via its Node compatibility layer). Edge runtimes without node:crypto are not supported.

License

MIT © 2026 Krova Inc. See LICENSE.