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

@vafast/webhook

v0.0.17

Published

Webhook dispatch middleware for Vafast framework - automatically trigger webhooks based on route configuration

Downloads

1,472

Readme

@vafast/webhook

Webhook dispatch middleware for Vafast framework.

Automatically trigger webhooks based on route configuration. Zero boilerplate - just add webhook field to your routes!

Features

  • 🚀 Declarative - Configure webhooks directly in route definitions
  • Async - Non-blocking, dispatches after response is sent
  • 🔒 Secure - HMAC-SHA256 signature support
  • 🎯 Flexible - Include/exclude fields, custom transforms, conditions
  • 📝 Logged - Full delivery tracking with customizable storage
  • 🔄 Retry - Automatic retry with exponential backoff on failure
  • 🚦 Concurrency - Control parallel webhook requests
  • 🆔 Event ID - Unique event IDs for idempotency support

Installation

npm install @vafast/webhook
# or
npm install @vafast/webhook

Quick Start

Option 1: Simple Config (No Database)

import { Server } from 'vafast'
import { webhook, defineWebhooks } from '@vafast/webhook'

// Define webhooks in code
const storage = defineWebhooks([
  { eventKey: 'auth.signIn', url: 'https://api.example.com/webhook' },
  { eventKey: 'auth.signUp', url: 'https://api.example.com/webhook' },
  { eventKey: 'users.*', url: 'https://crm.example.com/hook', secret: 'my-secret' }, // Wildcard
])

// Dynamic add (optional)
storage.add({ eventKey: 'order.created', url: 'https://...' })

// Check logs (for testing)
console.log(storage.logs)

const server = new Server(routes)
server.use(webhook({ storage }))

Option 2: Custom Storage (Database/Redis/etc.)

import type { WebhookStorage } from '@vafast/webhook'

const storage: WebhookStorage = {
  async findSubscriptions(appId, eventKey) {
    // Use any data source: MySQL, Redis, API, etc.
    return db.query('SELECT * FROM webhooks WHERE event_key = ? AND status = "enabled"', [eventKey])
  },
  async saveLog(log) {
    await db.insert('webhook_logs', log)
  },
}

server.use(webhook({ 
  storage,
  getAppId: (req) => req.headers.get('app-id'), // Optional: for multi-tenant
}))

3. Configure routes

const routes = [
  {
    method: 'POST',
    path: '/auth/signIn',
    handler: signInHandler,
    name: '用户登录',
    description: '用户登录接口',
    // Enable webhook with field filtering
    webhook: {
      exclude: ['jwtToken', 'refreshToken'],
    },
  },
  {
    method: 'POST',
    path: '/users/create',
    handler: createUserHandler,
    name: '创建用户',
    // Only include specific fields
    webhook: {
      include: ['userId', 'email', 'createdAt'],
    },
  },
]

Configuration

Middleware Options

webhook({
  // Required: Storage adapter for subscriptions and logs
  storage: WebhookStorage,

  // Optional: Logger (default: console)
  logger: WebhookLogger,

  // Optional: API path prefix to strip when generating eventKey
  // e.g., '/restfulApi' → '/restfulApi/auth/signIn' becomes 'auth.signIn'
  pathPrefix: '/restfulApi',

  // Optional: Custom function to extract app ID from request
  // Default: (req) => req.headers.get('app-id')
  getAppId: (req) => {
    // From header
    return req.headers.get('x-app-id')
    // Or from JWT
    // return decodeJwt(req.headers.get('authorization')).appId
    // Or fixed value (single-tenant)
    // return 'my-app'
  },

  // Optional: Custom function to check if response is successful
  // Default: (data) => data.success === true && data.code === 20001
  isSuccess: (data) => {
    // Standard REST API
    return data.code === 200
    // Or simple check
    // return data.ok === true
  },

  // Optional: Custom function to extract payload data from response
  // Default: (data) => data.data || {}
  getData: (data) => data.result || {},

  // Optional: Timeout for webhook requests in ms (default: 30000)
  timeout: 30000,

  // Optional: Fields to always exclude (default: password, token, etc.)
  sensitiveFields: ['password', 'token', 'secret'],

  // Optional: Retry configuration for failed webhooks
  retry: {
    count: 3,      // Number of retry attempts (default: 0)
    delay: 1000,   // Initial delay in ms (default: 1000)
    backoff: 2,    // Exponential backoff multiplier (default: 2)
    maxDelay: 30000, // Max delay in ms (default: 30000)
  },

  // Optional: Max concurrent webhook requests (default: 10)
  concurrency: 10,
})

Route Webhook Options

{
  webhook: {
    // Custom event key (default: auto-generated from path)
    // e.g., '/auth/signIn' -> 'auth.signIn'
    eventKey: 'user.login',

    // Fields to include in payload (whitelist)
    include: ['userId', 'email'],

    // Fields to exclude from payload (blacklist)
    exclude: ['password', 'token'],

    // Condition function - only trigger if returns true
    condition: (data) => data.role === 'admin',

    // Custom transform function
    transform: (data, req) => ({
      ...data,
      source: 'web',
    }),
  }
}

Webhook Payload

{
  "eventId": "evt_m4xr7z_1a2b3c4d5e6f7890",
  "appId": "app_123",
  "eventType": "auth",
  "eventKey": "auth.signIn",
  "timestamp": "2024-01-07T12:00:00.000Z",
  "data": {
    "userId": "user_456",
    "email": "[email protected]",
    "clientIp": "192.168.1.1",
    "userAgent": "Mozilla/5.0...",
    "timestamp": "2024-01-07T12:00:00.000Z"
  }
}

Event ID (eventId): Each webhook delivery has a unique event ID. Use this for idempotency - if your endpoint receives the same eventId twice, you can safely skip processing.

Webhook Headers

| Header | Description | |--------|-------------| | Content-Type | application/json | | X-Webhook-Event | Event key (e.g., auth.signIn) | | X-Webhook-Event-Id | Unique event ID for idempotency (e.g., evt_m4xr7z_1a2b3c4d5e6f7890) | | X-Webhook-Timestamp | ISO timestamp | | X-Webhook-Signature | HMAC-SHA256 signature (if secret configured) |

Manual Dispatch

For redirect scenarios (e.g., OAuth callbacks), use dispatchWebhook:

import { dispatchWebhook } from '@vafast/webhook'

// In OAuth callback handler
const oauthCallback = async (req) => {
  const { userId, email, provider } = await verifyOAuth(req)

  // Manually trigger webhook (redirect responses can't be auto-processed)
  dispatchWebhook(storage, logger, {
    appId,
    eventKey: 'auth.oauth',
    data: { userId, email, provider },
    req,
  })

  // Return redirect response
  return Response.redirect(`${frontendUrl}?token=${token}`)
}

Query Events API

Query available webhook events from route configurations:

import {
  getAllWebhookEvents,
  getWebhookCategories,
  getWebhookEventsByCategory,
} from '@vafast/webhook'

// Get all events (pass pathPrefix if your routes use a prefix)
const events = getAllWebhookEvents('/restfulApi')
// Returns:
// [
//   { eventKey: 'auth.signIn', name: '用户登录', category: 'auth', method: 'POST', path: '/restfulApi/auth/signIn' },
//   { eventKey: 'users.update', name: '更新用户', category: 'users', method: 'PUT', path: '/restfulApi/users/update' },
// ]

// Get all categories
const categories = getWebhookCategories('/restfulApi')
// Returns: ['auth', 'users']

// Get events by category
const authEvents = getWebhookEventsByCategory('auth', '/restfulApi')
// Returns only events with category === 'auth'

Use case: Build an admin UI for webhook configuration by exposing these as API endpoints:

const routes = [
  {
    method: 'GET',
    path: '/webhooks/events',
    handler: () => success(getAllWebhookEvents('/restfulApi')),
  },
  {
    method: 'GET',
    path: '/webhooks/categories',
    handler: () => success(getWebhookCategories('/restfulApi')),
  },
  {
    method: 'GET',
    path: '/webhooks/events/:category',
    handler: ({ params }) => success(getWebhookEventsByCategory(params.category, '/restfulApi')),
  },
]

Storage Adapters

MongoDB Example

import { Collection } from 'mongodb'

const createMongoStorage = (
  webhooks: Collection,
  logs: Collection
): WebhookStorage => ({
  async findSubscriptions(appId, eventKey) {
    const docs = await webhooks.find({ appId, eventKey, status: 'enabled' }).toArray()
    return docs.map((doc) => ({
      id: doc._id.toString(),
      appId: doc.appId,
      eventKey: doc.eventKey,
      endpointUrl: doc.endpointUrl,
      secret: doc.webhookSecret,
      status: doc.status,
    }))
  },
  async saveLog(log) {
    await logs.insertOne({
      ...log,
      createAt: new Date(),
      updateAt: new Date(),
    })
  },
})

In-Memory Example (for testing)

const createMemoryStorage = (): WebhookStorage => {
  const subscriptions: WebhookSubscription[] = []
  const logs: WebhookLog[] = []

  return {
    async findSubscriptions(appId, eventKey) {
      return subscriptions.filter(
        (s) => s.appId === appId && s.eventKey === eventKey && s.status === 'enabled'
      )
    },
    async saveLog(log) {
      logs.push(log)
    },
  }
}

Retry & Concurrency

Retry with Exponential Backoff

Failed webhooks can be automatically retried:

server.use(webhook({
  storage,
  retry: {
    count: 3,        // Try up to 3 more times after initial failure
    delay: 1000,     // Start with 1 second delay
    backoff: 2,      // Double delay each retry: 1s → 2s → 4s
    maxDelay: 30000, // Cap at 30 seconds
  },
}))

Retry timeline example:

  • Attempt 1: Immediate (fails)
  • Attempt 2: Wait 1s, retry (fails)
  • Attempt 3: Wait 2s, retry (fails)
  • Attempt 4: Wait 4s, retry (succeeds or gives up)

Concurrency Control

Control how many webhooks are sent in parallel:

server.use(webhook({
  storage,
  concurrency: 5,  // Max 5 concurrent requests (default: 10)
}))

Use cases:

  • Prevent overwhelming external services
  • Respect rate limits on third-party APIs
  • Reduce memory usage during high traffic

Processing Flow

Request → Handler → Response
                       ↓
               webhookMiddleware
                       ↓
          getWebhookEventConfig() ← Query vafast RouteRegistry
                       ↓
              checkCondition() → Skip if false
                       ↓
              processFields() → Filter sensitive/specified fields
                       ↓
           setImmediate() → Async dispatch (non-blocking)
                       ↓
           dispatchEvent() → Semaphore(concurrency) → Send with retry

License

MIT