webhooks

js/webhooks.ts

fino:webhooks — signed inbound verification and durable outbound delivery.

The signature covers webhook-id, webhook-timestamp, and the exact body bytes with HMAC-SHA-256. Outbound work is a normal fino:task task, so enqueueWebhook() delegates persistence, exponential backoff, and dead-letter behavior to fino:jobs.

Delivery is at-least-once. Receivers should retain webhook-id values for their business idempotency window and return success for an already-applied event. The jobs dedupe key prevents duplicate active deliveries but does not replace durable receiver-side idempotency.

import { Jobs } from 'fino:jobs';
import { createWebhookDeliveryTask, enqueueWebhook } from 'fino:webhooks';

const delivery = createWebhookDeliveryTask({ secret: webhookSecret });
await using jobs = await Jobs.open({
  path: './.fino/jobs.db',
  tasks: [delivery],
});
await enqueueWebhook(jobs, {
  id: 'evt_123',
  url: 'https://example.com/hooks',
  body: JSON.stringify({ type: 'created' }),
});

Constants

const webhookHeaders

Header names used by the Fino webhook signature scheme.

Interfaces

interface WebhookSignOptions {

Input accepted by signWebhook().

Properties

id: string

Stable event identifier.

timestamp: number

Unix timestamp in seconds.

body: BufferLike

Exact body bytes or UTF-8 string to sign.

secret: BufferLike

HMAC secret.

interface WebhookVerificationOptions {

Options controlling inbound webhook verification.

Properties

secret?: BufferLike

One active signing secret.

Use secrets instead when rotating keys.

secrets?: BufferLike[]

Active signing secrets accepted during rotation.

Every configured key is evaluated without early success.

toleranceSeconds?: number

Maximum accepted timestamp age or future skew in seconds.

Defaults to five minutes.

now?: () => number

Clock returning Unix time in milliseconds.

replay?: AtomicExpiringStore

Optional generic store with atomic commit and provider-managed expiry.

Signature and timestamp checks complete before the event id is claimed.

interface VerifiedWebhook {

Authenticated inbound webhook data.

Properties

id: string

Stable event identifier from webhook-id.

timestamp: number

Verified Unix timestamp in seconds.

body: Uint8Array

Exact body bytes covered by the signature.

interface WebhookDeliveryInput {

JSON-compatible input persisted for one outbound delivery.

Properties

id: string

Stable event id used for receiver idempotency.

url: string

Absolute HTTP or HTTPS receiver URL.

body: string

Exact UTF-8 body delivered and signed.

headers?: Record<string, string>

Optional application headers.

Signature headers are always replaced by freshly computed values.

interface WebhookDeliveryTaskOptions {

Options used to construct the outbound delivery task.

Properties

secret: BufferLike

HMAC secret retained by the worker and never persisted in job input.

name?: string

Stable task name registered with fino:jobs.

Defaults to webhooks.deliver.

now?: () => number

Clock returning Unix time in milliseconds.

fetch?: WebhookFetch

Fetch implementation used for delivery.

interface EnqueueWebhookOptions {

Queue options for durable outbound delivery.

Properties

task?: string

Registered delivery task name.

Must match createWebhookDeliveryTask({ name }).

queue?: string

Queue name. Defaults to webhooks.

retry?: Partial<JobRetryPolicy>

Retry policy passed through to fino:jobs.

timeoutMs?: number

Per-attempt timeout in milliseconds.

Functions

function signWebhook(options: WebhookSignOptions): Headers

Create the three headers authenticating one webhook body.

The timestamp is supplied by the caller so tests and durable delivery attempts can make time explicit.

async function verifyWebhookRequest( request: Request, options: WebhookVerificationOptions, ): Promise<VerifiedWebhook>

Authenticate one Fetch-compatible webhook request.

This consumes the supplied request body. Middleware verifies a clone so the downstream handler retains its own readable body.

function webhookVerifier(options: WebhookVerificationOptions): Middleware

Create inbound verification middleware for fino:net/http/app.

Successful verification stores VerifiedWebhook on ctx.webhook. Verification failures short-circuit with { error: { code, message } }.

function createWebhookDeliveryTask( options: WebhookDeliveryTaskOptions, ): Task<WebhookDeliveryInput, { status: number }>

Create the task that performs one signed outbound attempt.

Network failures and transient HTTP statuses throw retryable errors. Other non-2xx responses throw NonRetryableJobError so fino:jobs dead-letters them immediately.

function enqueueWebhook( jobs: Pick<Jobs, 'push'>, input: WebhookDeliveryInput, options: EnqueueWebhookOptions = {}, ): Promise<JobRecord>

Persist an outbound webhook in fino:jobs.

The event id is also the active-job dedupe key. Delivery remains at-least-once across crashes, so receivers must make applying that id idempotent.

Types

type WebhookVerificationErrorCode = | 'missing_header' | 'invalid_timestamp' | 'timestamp_outside_tolerance' | 'invalid_signature' | 'replay_detected'

Machine-readable inbound verification failure codes.

type WebhookFetch = (input: string | Request, init?: FetchInit) => Promise<Response>

Minimal fetch shape accepted by outbound delivery for custom transports and deterministic tests.

Classes

class WebhookVerificationError extends Error {

Error raised when an inbound webhook cannot be authenticated.

Readonly Properties

readonly code: WebhookVerificationErrorCode

Machine-readable reason suitable for API responses and metrics.

readonly status: number

Recommended HTTP status for the failure.

Constructors

constructor(code: WebhookVerificationErrorCode, message: string, status: number)

Create an actionable verification error.