import type { Clock, FetchLike } from "@suaveplan/types"; import type { Notification } from "../types/notification.types.js"; import type { DeliveryChannel, DeliveryResult } from "./delivery-channel.js"; /** Configuration for the webhook channel. */ export interface WebhookChannelConfig { /** Default headers added to every request. */ readonly headers?: Readonly>; /** Optional `FetchLike` implementation (default: `globalThis.fetch`). */ readonly fetchFn?: FetchLike; /** Timeout in milliseconds (default: 10_000). */ readonly timeoutMs?: number; /** Optional clock for the `timestamp` field (default: `Date.now`). */ readonly clock?: Clock; } const defaultClock: Clock = () => Date.now(); /** * Delivers notifications via HTTP webhook POST requests. The * notification's `recipient` field is the destination URL; the body * contains the serialised notification payload. */ export class WebhookChannel implements DeliveryChannel { public readonly name = "webhook"; private readonly config: WebhookChannelConfig; private readonly clock: Clock; private readonly fetchFn: FetchLike; public constructor(config?: WebhookChannelConfig) { this.config = config ?? {}; this.clock = this.config.clock ?? defaultClock; this.fetchFn = this.config.fetchFn ?? globalThis.fetch.bind(globalThis); } public canSend(notification: Notification): boolean { return ( notification.channel === "webhook" && (notification.recipient.startsWith("http://") || notification.recipient.startsWith("https://")) ); } public async send(notification: Notification): Promise { const timeoutMs = this.config.timeoutMs ?? 10_000; const payload = { id: notification.id, subject: notification.subject, body: notification.body, priority: notification.priority, metadata: notification.metadata, createdAt: notification.createdAt, }; try { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); const response = await this.fetchFn(notification.recipient, { method: "POST", headers: { "Content-Type": "application/json", ...this.config.headers, }, body: JSON.stringify(payload), signal: controller.signal, }); clearTimeout(timer); if (!response.ok) { return { success: false, channel: this.name, error: `HTTP ${response.status}: ${response.statusText}`, timestamp: new Date(this.clock()).toISOString(), }; } return { success: true, channel: this.name, messageId: notification.id, timestamp: new Date(this.clock()).toISOString(), }; } catch (err) { return { success: false, channel: this.name, error: err instanceof Error ? err.message : String(err), timestamp: new Date(this.clock()).toISOString(), }; } } }