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

@zap-studio/webhooks

v2.1.0

Published

A lightweight, type-safe, tree-shakeable webhook router with Standard Schema validation, signature verification, and lifecycle hooks.

Readme

@zap-studio/webhooks

Schema-first, type-safe webhook routing built on the standard Web API Request and Response primitives, with runtime-agnostic signature verification support.

Full documentation: zapstudio.dev/webhooks

Motivation

Webhook endpoints are usually built by hand, per provider, and this tends to repeat the same two mistakes. First, signature checks often use Node's crypto module, which does not exist on Cloudflare Workers, Deno, or Bun's edge runtimes — so the same code cannot run everywhere the webhook needs to be received.

Second, signatures are often compared with ===, which leaks timing information and opens a timing-attack risk that most teams do not know they have.

@zap-studio/webhooks fixes both. It is built on the standard Request/Response objects, so the same router works on Bun, Deno, Cloudflare Workers, or any framework that accepts a Request.

Signature verification is opt-in through the verify option, and the built-in HMAC verifier uses the Web Crypto API (globalThis.crypto.subtle) with a constant-time comparison, so once you turn it on, you get the safe comparison without writing it yourself. Payloads are validated and typed straight from your Standard Schema — no any from JSON.parse, no manual casts.

Installation

npm install @zap-studio/webhooks

You also need a schema library that implements Standard Schema, such as Zod, Valibot, or ArkType.

Features

  • Web API nativehandle(request: Request) returns a Response, so the router plugs directly into Bun, Deno, Cloudflare Workers, Next.js route handlers, Hono, and any other fetch-compatible runtime.
  • Type-safe routing — handler payload types are inferred from the route schema.
  • Standard Schema validation — bring Zod, Valibot, ArkType, or any compatible library.
  • Signature verification — built-in HMAC verifier with constant-time comparison, or plug in your own verify function.
  • Lifecycle hooks — global before, after, and onError hooks for cross-cutting behavior.
  • Runtime-agnostic — uses the Web Crypto API, not Node-specific APIs.
  • Optional logging through createWebhookRouter({ logger }) (@zap-studio/logger) — omit it and there's zero logging overhead.
  • Tree-shakeable — validation and hook-running internals are standalone functions; unused exports are dropped by any modern bundler.

Quick Start

import { ConsoleLogger } from "@zap-studio/logger";
import { createWebhookRouter } from "@zap-studio/webhooks";
import { z } from "zod";

const logger = new ConsoleLogger({ minLevel: "debug" });
const router = createWebhookRouter({ prefix: "/webhooks", logger });

router.register("/payments/succeeded", {
  schema: z.object({ id: z.string(), amount: z.number().positive() }),
  handler: ({ payload }) => {
    // payload is inferred from schema
    return Response.json({ processed: payload.id });
  },
});

export default {
  fetch: (request: Request) => router.handle(request),
};

Web API Native

handle(request: Request) returns a Response, so the router plugs directly into any fetch-compatible runtime.

// Bun / Deno / Cloudflare Workers
export default { fetch: (request: Request) => router.handle(request) };

// Next.js route handler (app/webhooks/[...path]/route.ts)
export const POST = (request: Request) => router.handle(request);

// Hono
app.all("/webhooks/*", (c) => router.handle(c.req.raw));

Type-Safe Routing

Handler payload types are inferred from the route schema.

router.register("/payments/succeeded", {
  schema: z.object({ id: z.string(), amount: z.number() }),
  handler: ({ payload }) => {
    // payload.id: string, payload.amount: number — inferred from schema
    return Response.json({ ok: true });
  },
});

Standard Schema Validation

Bring Zod, Valibot, ArkType, or any compatible library.

import { z } from "zod";
// or: import * as v from "valibot"; import { type } from "arktype";

router.register("/event", {
  schema: z.object({ id: z.string() }),
  handler: ({ payload }) => Response.json(payload),
});

Signature Verification

Built-in HMAC verifier with constant-time comparison, or plug in your own verify function.

import { createHmacVerifier, createWebhookRouter, VerificationError } from "@zap-studio/webhooks";

const router = createWebhookRouter({
  verify: createHmacVerifier({
    headerName: "x-hub-signature-256",
    secret: process.env.WEBHOOK_SECRET!,
  }),
  onError: (error) => {
    if (error instanceof VerificationError) {
      return Response.json({ error: "invalid signature" }, { status: 401 });
    }
  },
});

verify also works per-route on register(), overriding the router-level one — useful for a router handling multiple providers, each with its own signing scheme:

router.register("/github", {
  verify: createHmacVerifier({ headerName: "x-hub-signature-256", secret: githubSecret }),
  schema: githubEventSchema,
  handler: githubHandler,
});

router.register("/stripe", {
  verify: stripeVerify, // a different scheme entirely
  schema: stripeEventSchema,
  handler: stripeHandler,
});

Lifecycle Hooks

Global before, after, and onError hooks for cross-cutting behavior.

const router = createWebhookRouter({
  before: (ctx) => console.log("incoming", ctx.path),
  after: (_ctx, response) => console.log("status", response.status),
  onError: (error) => Response.json({ error: error.message }, { status: 500 }),
});

Runtime-Agnostic

Uses the Web Crypto API, not Node-specific APIs.

// Uses globalThis.crypto.subtle — no Node `crypto` import required
const verify = createHmacVerifier({
  headerName: "x-hub-signature-256",
  secret: process.env.WEBHOOK_SECRET!,
});

Logging

Pass a logger?: Logger from @zap-studio/logger to createWebhookRouter(...) to observe delivery attempts, dispatch, verification failures, and unmatched routes. Omit it and nothing is logged.

import { ConsoleLogger } from "@zap-studio/logger";
import { createWebhookRouter } from "@zap-studio/webhooks";

const logger = new ConsoleLogger({ minLevel: "debug" });
const router = createWebhookRouter({ prefix: "/webhooks", logger });

Each delivery attempt and handler dispatch logs at debug; verification failures and unmatched routes log at warn.

OpenTelemetry

@opentelemetry/api is a required peer dependency — tiny, side-effect-free, and a no-op until an app registers a real SDK, so installing it costs nothing at runtime for consumers who never set one up.

Each delivery gets a SERVER span, and the sender's traceparent header is extracted so the delivery continues their trace instead of starting a new one. Each handler dispatch gets its own child INTERNAL span:

npm install @opentelemetry/api
import { createWebhookRouter } from "@zap-studio/webhooks";

const router = createWebhookRouter({ prefix: "/webhooks" });
router.register("/stripe", { schema: stripeEventSchema, handler });

// If your app has registered an OpenTelemetry SDK, router.handle(request)
// now produces a SERVER span per delivery (continuing the sender's trace
// when a traceparent header is present) and an INTERNAL span per handler
// dispatch. If not, it's a no-op — no wiring required either way.
export default { fetch: (request: Request) => router.handle(request) };

A non-2xx response (unmatched route, validation failure, verification failure, handler error) marks the delivery span ERROR; a thrown handler error is also recorded as an exception on the handler span.

Runtime Support

| Runtime | Minimum version | | ------------------ | ------------------------------------------------ | | Node.js | 18.0.0 (router), 19.0.0 (verification helper) | | Bun | 1.0.0 | | Deno | 1.42 | | Cloudflare Workers | Any current release | | Browsers | Latest evergreen (Chrome, Edge, Firefox, Safari) |

The router only needs the standard Request/Response APIs, available globally since Node.js 18. The verification helper additionally needs globalThis.crypto.subtle, which is global by default from Node.js 19 (on Node.js 18, pass the --experimental-global-webcrypto flag). In browsers, Web Crypto requires a secure context (HTTPS). Deno 1.42 is the first release that can install packages from JSR (deno add jsr:@zap-studio/webhooks).

License

MIT