@samveys/webhooks

Verified incoming deliveries and signed outgoing events

Version
1.0.2Nov 5, 2026
Install
pnpm add @samveys/webhooks
Import
import { sendWebhook, standardSignature, verifyDelivery, webhookEvent, webhookReceiver } from '@samveys/webhooks'

Function (5)

sendWebhook Function

Signs an event per Standard Webhooks and posts it with `@samveys/http-client` in `untrusted` mode: an endpoint URL that points at a loopback, private or metadata address (or redirects there) is refused with SVS_E106 before anything is sent. The body is `{"type":…,"timestamp":"<ISO>","data":…}`; the headers are `webhook-id`, `webhook-timestamp` (epoch seconds) and `webhook-signature` (`v1,<base64 HMAC-SHA256 of id.timestamp.body>`). A non-2xx answer is returned as `ok: false`; queue retries by calling this inside a job.

function sendWebhook(endpoint: WebhookEndpoint, event: OutgoingEvent, options?: SendWebhookOptions): Promise<SendResult>;

Exported from packages/webhooks/src/sender.ts

standardSignature Function

`v1,<base64 HMAC-SHA256 of "<id>.<timestamp>.<body>">` for one secret.

function standardSignature(secret: string, id: string, timestamp: number | string, body: Uint8Array | string): string;

Exported from packages/webhooks/src/verify.ts

verifyDelivery Function

Checks a delivery's signature (and, where the scheme signs one, its timestamp) against the raw body. Returns the delivery id. Throws SVS_E108 for any problem, always with the same public message.

function verifyDelivery(input: VerifyInput): WebhookEvent;

Exported from packages/webhooks/src/verify.ts

webhookEvent Function

The verified delivery stored by {@link webhookReceiver} when no `process` callback is configured.

function webhookEvent(ctx: Pick<HttpContext, 'state'>): WebhookEvent;

Exported from packages/webhooks/src/receiver.ts

webhookReceiver Function

Route middleware that accepts a webhook only when it is genuine and new. 1. Reads the raw body from a clone (before the kernel parses it) and verifies the signature with a constant-time comparison, plus the 5-minute timestamp window where the scheme signs one. 2. Remembers the delivery id in the cache (`add(id, …, ttl)`); a second delivery with the same id is rejected. 3. Hands the event to `process`, or stores it for the handler (read it with {@link webhookEvent}). Anything wrong answers `401 SVS_E108` with a fixed body. The reason goes to `error.meta` only. If `process` throws, the id is forgotten again so the sender's retry is accepted.

function webhookReceiver(options: WebhookReceiverOptions): Middleware;

Exported from packages/webhooks/src/receiver.ts

Interface (7)

OutgoingEvent Interface

No documentation comment.

interface OutgoingEvent {
    /** Event id, used as `webhook-id`. Default: a random UUID. */
    readonly id?: string;
    readonly type: string;
    readonly data: unknown;
}

Exported from packages/webhooks/src/contracts.ts

SendResult Interface

No documentation comment.

interface SendResult {
    readonly id: string;
    readonly status: number;
    readonly ok: boolean;
}

Exported from packages/webhooks/src/contracts.ts

SendWebhookOptions Interface

No documentation comment.

interface SendWebhookOptions {
    /** Client used for delivery; it is always called in `untrusted` mode. Default: a fresh client. */
    readonly client?: HttpClient;
    readonly now?: () => Date;

Exported from packages/webhooks/src/contracts.ts

VerifyInput Interface

No documentation comment.

interface VerifyInput {
    readonly driver: WebhookDriver;
    /** `whsec_<base64>` (standard), shared secret (github, stripe), server key (midtrans). A list allows rotation. */
    readonly secret: string | readonly string[];
    readonly headers: Headers;
    /** The raw request body: the exact bytes that were signed. */
    readonly rawBody: Uint8Array | string;
    /** Largest accepted clock difference in seconds for drivers that sign a timestamp. Default 300. */
    readonly toleranceSeconds?: number;
    /** Clock override (tests). */
    readonly now?: () => Date;

Exported from packages/webhooks/src/contracts.ts

WebhookEndpoint Interface

Where an outgoing event goes.

interface WebhookEndpoint {
    readonly url: string;
    /** Signing secret, `whsec_<base64>`. */
    readonly secret: string;
}

Exported from packages/webhooks/src/contracts.ts

WebhookEvent Interface

A delivery whose signature, age and uniqueness were checked.

interface WebhookEvent {
    /** Delivery id: `webhook-id`, `X-GitHub-Delivery`, the Stripe event id, or the Midtrans transaction key. */
    readonly id: string;
    readonly driver: WebhookDriver;
    /** The parsed JSON body, or `undefined` when the body is not JSON. */
    readonly payload: unknown;
    /** The body exactly as received (the bytes that were signed). */
    readonly rawBody: string;
    readonly headers: Headers;
}

Exported from packages/webhooks/src/contracts.ts

WebhookReceiverOptions Interface

No documentation comment.

interface WebhookReceiverOptions {
    readonly driver: WebhookDriver;
    readonly secret: string | readonly string[];
    /** Where seen delivery ids are remembered (`add(id, …, ttl)`); omit to disable replay protection (not advised). */
    readonly cache?: Pick<CacheStore, 'add' | 'forget'>;
    /** How long a delivery id is remembered. Default 24 hours. */
    readonly replayTtlMs?: number;
    readonly toleranceSeconds?: number;
    /**
     * Called with each verified, first-seen delivery. Use `queue.dispatch(...)` inside it so the
  …
Show the whole declaration (14 lines)
interface WebhookReceiverOptions {
    readonly driver: WebhookDriver;
    readonly secret: string | readonly string[];
    /** Where seen delivery ids are remembered (`add(id, …, ttl)`); omit to disable replay protection (not advised). */
    readonly cache?: Pick<CacheStore, 'add' | 'forget'>;
    /** How long a delivery id is remembered. Default 24 hours. */
    readonly replayTtlMs?: number;
    readonly toleranceSeconds?: number;
    /**
     * Called with each verified, first-seen delivery. Use `queue.dispatch(...)` inside it so the
     * HTTP response is not held up. When set, the middleware answers `200 {"received":true}` itself;
     * when omitted the route handler runs and reads the event with {@link webhookEvent}.
     */
    readonly process?: (event: WebhookEvent) => void | Promise<void>;

Exported from packages/webhooks/src/contracts.ts

Type (1)

WebhookDriver Type

Built-in verification drivers.

type WebhookDriver = 'standard' | 'github' | 'stripe' | 'midtrans';

Exported from packages/webhooks/src/contracts.ts