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.