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

@kotodayori/core

v1.0.0

Published

Type-safe webhook routing framework for any event source

Downloads

130

Readme

@kotodayori/core

Type-safe webhook routing framework for any event source.

Overview

@kotodayori/core provides the foundational routing logic for building type-safe webhook handlers. It offers a flexible, framework-agnostic API for routing webhook events with full TypeScript support, middleware capabilities, and flexible routing patterns.

Installation

npm install @kotodayori/core
# or
pnpm add @kotodayori/core
# or
yarn add @kotodayori/core

Features

  • Type-Safe Event Routing: Generic event type definitions for full IDE autocomplete
  • Middleware Support: Add logging, error handling, and other cross-cutting concerns
  • Flexible Routing: Group handlers, mount nested routers, and fanout patterns
  • Pluggable Verification: Bring your own verifier for any webhook provider
  • Framework Agnostic: Works with any HTTP framework via adapters

Quick Start

Basic Usage

import { WebhookRouter, type WebhookEvent, type Verifier } from '@kotodayori/core';

// Define your event types
interface MyEvent extends WebhookEvent {
  type: 'my.event';
  data: { object: { id: string; name: string } };
}

type MyEventMap = {
  'my.event': MyEvent;
};

// Create a router
const router = new WebhookRouter<MyEventMap>();

// Register event handlers
router.on('my.event', async (event) => {
  console.log('Event received:', event.data.object);
});

// Dispatch events
await router.dispatch({
  id: '123',
  type: 'my.event',
  data: { object: { id: '1', name: 'Test' } },
});

With a Custom Verifier

import crypto from 'crypto';

// Create a verifier for GitHub webhooks
function createGitHubVerifier(secret: string): Verifier {
  return (payload, headers) => {
    const signature = headers['x-hub-signature-256'];
    if (!signature) {
      throw new Error('Missing x-hub-signature-256 header');
    }

    const hmac = crypto.createHmac('sha256', secret);
    const digest = 'sha256=' + hmac.update(payload).digest('hex');

    if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest))) {
      throw new Error('Invalid signature');
    }

    const body = JSON.parse(payload.toString());
    return {
      event: {
        id: headers['x-github-delivery'] ?? crypto.randomUUID(),
        type: headers['x-github-event'] ?? 'unknown',
        data: { object: body },
      },
    };
  };
}

API Reference

WebhookRouter

The main router class for handling webhook events.

on(event, handler)

Register a handler for a specific event type.

router.on('payment.succeeded', async (event) => {
  // Handle payment success
});

// Multiple events with the same handler
router.on(['order.created', 'order.updated'], async (event) => {
  // Handle order events
});

use(middleware)

Register middleware that runs before event handlers.

router.use(async (event, next) => {
  console.log(`Processing ${event.type}`);
  await next();
});

group(prefix, callback)

Group related event handlers with a common prefix.

router.group('payment', (r) => {
  r.on('succeeded', async (event) => {
    // Handles 'payment.succeeded'
  });

  r.on('failed', async (event) => {
    // Handles 'payment.failed'
  });
});

route(prefix, router)

Mount a nested router under a prefix.

const paymentRouter = new WebhookRouter();
paymentRouter.on('succeeded', async (event) => { /* ... */ });
paymentRouter.on('failed', async (event) => { /* ... */ });

const mainRouter = new WebhookRouter();
mainRouter.route('payment', paymentRouter);
// paymentRouter handlers are now available as 'payment.succeeded', 'payment.failed'

fanout(event, handlers, options)

Execute multiple handlers in parallel for the same event.

router.fanout('user.created', [
  async (event) => await sendWelcomeEmail(event),
  async (event) => await createAnalyticsProfile(event),
  async (event) => await notifySlack(event),
], {
  strategy: 'best-effort', // Continue even if some handlers fail
  onError: (error) => console.error('Handler failed:', error),
});

Strategies:

  • all-or-nothing (default): All handlers must succeed or the entire operation fails
  • best-effort: Continue executing handlers even if some fail

dispatch(event)

Dispatch an event to registered handlers.

await router.dispatch({
  id: '123',
  type: 'payment.succeeded',
  data: { object: { amount: 1000 } },
});

Types

WebhookEvent

Base interface for all webhook events.

interface WebhookEvent {
  id: string;
  type: string;
  data: { object: unknown };
}

EventHandler

Handler function type for processing events.

type EventHandler<T extends WebhookEvent> = (event: T) => Promise<void>;

Middleware

Middleware function type for cross-cutting concerns.

type Middleware<T extends WebhookEvent> = (
  event: T,
  next: () => Promise<void>
) => Promise<void>;

Verifier

Function type for verifying webhook signatures and parsing payloads.

type Verifier<T extends WebhookEvent> = (
  payload: string | Buffer,
  headers: Record<string, string | undefined>
) => VerifyResult<T> | Promise<VerifyResult<T>>;

Advanced Usage

Multiple Handlers per Event

Register multiple handlers for the same event type:

router.on('user.created', async (event) => {
  await sendWelcomeEmail(event);
});

router.on('user.created', async (event) => {
  await createUserProfile(event);
});

router.on('user.created', async (event) => {
  await trackSignup(event);
});
// All three handlers will execute sequentially

Middleware Examples

Logging Middleware

router.use(async (event, next) => {
  const start = Date.now();
  console.log(`[${event.type}] Processing event ${event.id}`);

  await next();

  const duration = Date.now() - start;
  console.log(`[${event.type}] Completed in ${duration}ms`);
});

Error Handling Middleware

router.use(async (event, next) => {
  try {
    await next();
  } catch (error) {
    console.error(`Error processing ${event.type}:`, error);
    // Send to error tracking service
    await Sentry.captureException(error, {
      tags: { eventType: event.type, eventId: event.id },
    });
    throw error;
  }
});

Known Limitations

Group Middleware Scope

There is a known limitation with the group().use() method that differs from expected behavior:

Current Behavior: Middleware registered within a group() using .use() applies to the entire router, not just handlers within that group.

const router = new WebhookRouter();

router.use(async (event, next) => {
  console.log('Router-level middleware');
  await next();
});

router.group('payment', (group) => {
  // ⚠️ This middleware runs for ALL events, not just 'payment.*'
  group.use(async (event, next) => {
    console.log('Group middleware - runs for ALL events');
    await next();
  });

  group.on('succeeded', async (event) => {
    console.log('Handler executed');
  });
});

// Both middlewares run for 'payment.succeeded' AND 'user.created'
await router.dispatch({ type: 'payment.succeeded', ... });
await router.dispatch({ type: 'user.created', ... });

Workaround: To apply middleware only to specific events within a group, use one of these alternatives:

  1. Event-level filtering in router middleware:
router.use(async (event, next) => {
  if (event.type.startsWith('payment.')) {
    console.log('Only for payment events');
  }
  await next();
});
  1. Handler-level error handling:
router.group('payment', (group) => {
  group.on('succeeded', async (event) => {
    try {
      // Your handler logic
    } catch (error) {
      console.error('Error in payment handler:', error);
      throw error;
    }
  });
});
  1. Separate routers for different concerns:
const paymentRouter = new WebhookRouter();
paymentRouter.use(async (event, next) => {
  console.log('Only for payment events');
  await next();
});

paymentRouter.on('succeeded', async (event) => {
  // Handle payment success
});

const mainRouter = new WebhookRouter();
mainRouter.route('payment', paymentRouter);

Using with Framework Adapters

@kotodayori/core is framework-agnostic. Use it with framework-specific adapters:

Example with Hono:

import { Hono } from 'hono';
import { WebhookRouter } from '@kotodayori/core';
import { honoAdapter } from '@kotodayori/hono';

const router = new WebhookRouter();
router.on('my.event', async (event) => {
  console.log('Event received');
});

const app = new Hono();
app.post('/webhook', honoAdapter(router, {
  verifier: createGitHubVerifier(process.env.GITHUB_WEBHOOK_SECRET!),
}));

TypeScript Tips

Strict Event Typing

import type { WebhookEvent } from '@kotodayori/core';

// Define your events
interface PaymentSucceededEvent extends WebhookEvent {
  type: 'payment.succeeded';
  data: {
    object: {
      id: string;
      amount: number;
      currency: string;
    };
  };
}

interface PaymentFailedEvent extends WebhookEvent {
  type: 'payment.failed';
  data: {
    object: {
      id: string;
      errorMessage: string;
    };
  };
}

// Create event map
type EventMap = {
  'payment.succeeded': PaymentSucceededEvent;
  'payment.failed': PaymentFailedEvent;
};

// Router has full type safety
const router = new WebhookRouter<EventMap>();

router.on('payment.succeeded', async (event) => {
  // TypeScript knows event.data.object has amount, currency, etc.
  const amount = event.data.object.amount;
});

Related Packages

Documentation

For more examples and guides, see the main documentation.

License

MIT