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

@nebutra/webhooks

v0.1.2

Published

> **Status: Foundation** — Type definitions, factory pattern, provider adapters, and injectable dead-letter storage are complete. Production custom deployments still need durable store adapters and queue infrastructure.

Readme

Status: Foundation — Type definitions, factory pattern, provider adapters, and injectable dead-letter storage are complete. Production custom deployments still need durable store adapters and queue infrastructure.

@nebutra/webhooks

Provider-agnostic webhook outbound management system for Nebutra. Supports Svix (managed) and custom (self-hosted) webhook delivery.

Installation

pnpm add @nebutra/webhooks

Quick Start

1. Initialize the webhooks provider

import { getWebhooks } from "@nebutra/webhooks";

// Auto-detects provider from environment
const webhooks = await getWebhooks();

// Or explicit config
import { createWebhooks } from "@nebutra/webhooks";

const webhooks = await createWebhooks({
  provider: "svix",
  apiKey: "svix_test_...",
});

2. Create a webhook endpoint

const endpoint = await webhooks.createEndpoint(
  "org_123", // tenantId
  {
    url: "https://example.com/webhooks",
    eventTypes: ["user.created", "invoice.paid"], // optional; empty = all events
    active: true,
    metadata: { team: "engineering" },
  }
);

console.log(endpoint.id);     // whe_...
console.log(endpoint.secret); // signing secret

3. Dispatch an event

const messageId = await webhooks.sendEvent({
  eventType: "user.created",
  payload: {
    userId: "user_123",
    email: "[email protected]",
    createdAt: new Date().toISOString(),
  },
  tenantId: "org_123",
});

console.log(messageId); // msg_...

4. Verify incoming webhooks (consumer side)

import { verifyPayload } from "@nebutra/webhooks";

// In your API route handler
export async function POST(req: Request) {
  const signature = req.headers.get("Webhook-Signature");
  const payload = await req.text();

  // Extract timestamp from signature header
  const parts = signature.split(".");
  const timestamp = parts[1];

  try {
    verifyPayload(payload, parts[2], secret, timestamp);
    // Signature valid, process webhook
  } catch (error) {
    // Invalid or expired signature
    return new Response("Unauthorized", { status: 401 });
  }
}

Providers

Svix (Managed)

Best for: SaaS products, managed infrastructure, enterprise features.

Auto-detects if SVIX_API_KEY is set. Otherwise, pass config:

const webhooks = await createWebhooks({
  provider: "svix",
  apiKey: "svix_test_...",
});

Features:

  • ✅ Managed retry logic (exponential backoff)
  • ✅ Built-in rate limiting & security
  • ✅ Event replay and retry UI
  • ✅ Webhook signing (Svix format)
  • ✅ Application isolation per tenant
  • ❌ No direct access to delivery attempts (API limitation)

Environment variables:

SVIX_API_KEY=svix_test_...

Custom (Self-Hosted)

Best for: Full control, on-premise deployments, fine-grained observability.

Auto-detects if SVIX_API_KEY is not set. Otherwise:

const webhooks = await createWebhooks({
  provider: "custom",
  redisUrl: "redis://localhost:6379", // optional, for persistence
  maxRetries: 6,
  initialBackoffSec: 5,
});

Features:

  • ✅ Full control over delivery logic
  • ✅ Injectable dead-letter store seam; default store is in-memory
  • ✅ Exponential backoff: 5s, 30s, 2m, 15m, 1h, 6h
  • ✅ Manual retry & delivery observability
  • ✅ Dead-letter metadata after retry exhaustion
  • ✅ HMAC-SHA256 signing (industry standard)
  • ❌ You handle infra, scaling, monitoring

Note: The bundled dead-letter store is intentionally an adapter seam, not a database coupling. For production use, inject a Redis/PostgreSQL-backed implementation and integrate with @nebutra/queue for distributed delivery.

import { createWebhooks, type WebhookDeadLetterStore } from "@nebutra/webhooks";

const deadLetterStore: WebhookDeadLetterStore = {
  async upsert(record) {
    // Persist by `${record.messageId}:${record.endpointId}` in Redis/PostgreSQL.
  },
  async delete(messageId, endpointId) {
    // Remove the dead-letter record after a successful manual replay.
  },
  async list(messageId) {
    // Return all records or records for one message.
    return [];
  },
};

const webhooks = await createWebhooks({
  provider: "custom",
  maxRetries: 6,
  deadLetterStore,
});

API

createWebhooks(config?: WebhookConfig): Promise<WebhookProvider>

Create a webhooks provider.

interface WebhookConfig {
  provider: "svix" | "custom";
  // ... provider-specific options
}

getWebhooks(): Promise<WebhookProvider>

Get the default (singleton) provider. Auto-detects from environment.

WebhookProvider Interface

createEndpoint(tenantId, endpoint): Promise<WebhookEndpoint>

Register a new webhook endpoint for a tenant.

interface WebhookEndpoint {
  id: string;                    // whe_...
  url: string;                   // https://example.com/webhooks
  tenantId: string;              // org_123
  secret: string;                // signing secret (base64)
  eventTypes: string[];          // ["user.created", "invoice.paid"]
  active: boolean;
  createdAt: string;             // ISO-8601
  metadata?: Record<string, unknown>;
}

updateEndpoint(endpointId, updates): Promise<WebhookEndpoint>

Update an endpoint (URL, eventTypes, active status, metadata).

deleteEndpoint(endpointId): Promise<void>

Delete an endpoint.

listEndpoints(tenantId): Promise<WebhookEndpoint[]>

List all endpoints for a tenant.

sendEvent(event): Promise<string>

Dispatch an event to all matching endpoints. Returns message ID.

interface WebhookMessage {
  eventType: string;                // "user.created"
  payload: Record<string, unknown>; // event data
  tenantId: string;                 // org_123
}

getDeliveryAttempts(messageId): Promise<WebhookDeliveryAttempt[]>

Get delivery attempt history for a message.

interface WebhookDeliveryAttempt {
  id: string;
  messageId: string;
  endpointId: string;
  status: "success" | "failed" | "pending" | "timeout";
  statusCode: number | null;
  response: string | null;
  attemptNumber: number;
  nextRetryAt: string | null; // ISO-8601
  attemptedAt: string;        // ISO-8601
}

getDeadLetterDeliveries(messageId?): Promise<WebhookDeadLetterDelivery[]>

Get dead-lettered deliveries after all retry attempts are exhausted.

interface WebhookDeadLetterDelivery {
  id: string;
  messageId: string;
  endpointId: string;
  tenantId: string;
  eventType: string;
  payload: Record<string, unknown>;
  finalAttemptId: string;
  finalAttemptNumber: number;
  statusCode: number | null;
  response: string | null;
  failedAt: string;
  deadLetteredAt: string;
}

retryMessage(messageId, endpointId): Promise<void>

Manually retry delivery to a specific endpoint.

rotateSecret(endpointId): Promise<string>

Rotate the signing secret. Returns new secret.

verifySignature(payload, signature, secret): Promise<boolean>

Verify a webhook signature. Throws on invalid signature.

close(): Promise<void>

Graceful shutdown.

Event Types

enum WebhookEventType {
  // User events
  USER_CREATED = "user.created",
  USER_UPDATED = "user.updated",
  USER_DELETED = "user.deleted",

  // Invoice events
  INVOICE_PAID = "invoice.paid",
  INVOICE_FAILED = "invoice.failed",
  INVOICE_UPDATED = "invoice.updated",

  // Subscription events
  SUBSCRIPTION_CREATED = "subscription.created",
  SUBSCRIPTION_UPDATED = "subscription.updated",
  SUBSCRIPTION_CANCELLED = "subscription.cancelled",

  // Organization events
  ORG_CREATED = "org.created",
  ORG_UPDATED = "org.updated",
  ORG_DELETED = "org.deleted",
}

To add custom events, extend the enum or use string literals:

await webhooks.sendEvent({
  eventType: "custom.myevent",
  payload: { ...data },
  tenantId: "org_123",
});

Signing & Verification

Standard Format

Webhooks are signed using HMAC-SHA256. The signature is included in the Webhook-Signature header:

Webhook-Signature: whsec_{secret}.{timestamp}.{signature}

Where:

  • secret — base64-encoded signing secret (32 bytes)
  • timestamp — Unix timestamp (seconds since epoch)
  • signature — base64-encoded HMAC-SHA256 hash

Verification

import { verifyPayload, parseWebhookSignatureHeader } from "@nebutra/webhooks";

export async function POST(req: Request) {
  const payload = await req.text();
  const headerValue = req.headers.get("Webhook-Signature");

  const parsed = parseWebhookSignatureHeader(headerValue);
  if (!parsed) {
    return new Response("Invalid signature format", { status: 400 });
  }

  try {
    await webhooks.verifySignature(payload, headerValue, endpoint.secret);
    // Process webhook
  } catch (error) {
    return new Response("Unauthorized", { status: 401 });
  }
}

Replay Attack Protection

Verification includes timestamp validation. Signatures older than 5 minutes (configurable) are rejected:

verifyPayload(payload, signature, secret, timestamp, toleranceSec = 300);

Examples

Create & List Endpoints

const webhooks = await getWebhooks();

// Create
const endpoint = await webhooks.createEndpoint("org_123", {
  url: "https://api.example.com/webhooks",
  eventTypes: ["invoice.paid"],
  metadata: { team: "payments" },
});

// List
const endpoints = await webhooks.listEndpoints("org_123");
console.log(endpoints);

// Delete
await webhooks.deleteEndpoint(endpoint.id);

Send & Retry

// Send event
const messageId = await webhooks.sendEvent({
  eventType: "invoice.paid",
  payload: {
    invoiceId: "inv_123",
    amount: 9999,
    currency: "USD",
  },
  tenantId: "org_123",
});

// Check delivery status
const attempts = await webhooks.getDeliveryAttempts(messageId);
for (const attempt of attempts) {
  console.log(
    `Endpoint ${attempt.endpointId}: ${attempt.status} (attempt ${attempt.attemptNumber})`
  );
}

// Manual retry
if (attempts.some((a) => a.status === "failed")) {
  await webhooks.retryMessage(messageId, failedEndpoint.id);
}

Rotate Secrets

const newSecret = await webhooks.rotateSecret(endpoint.id);
console.log(`New secret: ${newSecret}`);

// Store in your DB / notifiable secret manager
// Consumers need to be notified of the rotation

Environment Variables

Svix

SVIX_API_KEY=svix_test_...
WEBHOOK_PROVIDER=svix  # optional, auto-detected

Custom

REDIS_URL=redis://localhost:6379  # optional, for persistence
WEBHOOK_PROVIDER=custom             # optional, auto-detected

Production Checklist

For Svix:

  • Set SVIX_API_KEY in production environment
  • No additional setup required

For Custom:

  • Deploy Redis or use managed Redis (AWS ElastiCache, etc.)
  • Integrate with @nebutra/queue for distributed delivery
  • Inject a persistent dead-letter store
  • Add monitoring/alerting on delivery failures
  • Add replay guard helpers for duplicate valid signatures
  • Consider rate limiting per endpoint

Contributing

When adding new features:

  1. Update types in src/types.ts
  2. Implement in both providers (src/providers/svix.ts, src/providers/custom.ts)
  3. Add tests (if applicable)
  4. Update this README with examples

License

Proprietary — Nebutra-Sailor