import { type JsonFileStore, type OperationAuditEntry, type OperationAuditStoreOptions, type RecordOperationAuditInput } from "@listmonk-ops/common"; import type { OperationEventSource, OperationResourceKind } from "@listmonk-ops/operations/specs"; import { z } from "zod"; export declare const OUTBOUND_WEBHOOK_STORE_VERSION = 2; export declare const OUTBOUND_WEBHOOK_EVENT_SCHEMA_VERSION = 1; export declare const DEFAULT_OUTBOUND_WEBHOOK_TIMEOUT_MS = 10000; export declare const DEFAULT_OUTBOUND_WEBHOOK_MAX_ATTEMPTS = 6; export declare const DEFAULT_OUTBOUND_WEBHOOK_RETRY_DELAY_MS = 30000; export declare const MAX_OUTBOUND_WEBHOOK_RETRY_DELAY_MS = 3600000; export declare const DEFAULT_OUTBOUND_WEBHOOK_LEASE_MS = 90000; export declare const DEFAULT_OUTBOUND_WEBHOOK_STORE_LIMIT = 5000; export declare const DEFAULT_OUTBOUND_WEBHOOK_CONCURRENCY = 5; export declare const DEFAULT_OUTBOUND_WEBHOOK_CIRCUIT_FAILURE_THRESHOLD = 5; export declare const DEFAULT_OUTBOUND_WEBHOOK_CIRCUIT_COOLDOWN_MS = 300000; export declare const DEFAULT_OUTBOUND_WEBHOOK_WORKER_HEARTBEAT_STALE_MS = 90000; export declare const DEFAULT_OUTBOUND_WEBHOOK_WORKER_RETENTION_MS: number; export declare const OUTBOUND_WEBHOOK_SECRET_REF_PATTERN: RegExp; export declare const OUTBOUND_WEBHOOK_EVENT_TYPES: readonly ["operation.started", "operation.blocked", "operation.succeeded", "operation.failed", "campaign.scheduled", "campaign.started", "campaign.paused", "campaign.cancelled", "campaign.finished", "subscriber.created", "subscriber.updated", "subscriber.blocklisted", "subscriber.unsubscribed", "delivery.delivered", "delivery.bounced", "delivery.complained", "delivery.delayed", "delivery.rejected", "abtest.started", "abtest.ready-for-analysis", "abtest.winner-selected", "abtest.inconclusive", "abtest.failed", "sequence.created", "sequence.revised", "sequence.enrolled", "sequence.paused", "sequence.resumed", "sequence.reconciled", "sequence.deleted", "webhook.test"]; export declare const OUTBOUND_WEBHOOK_EVENT_FAMILY_FILTERS: readonly ["operation.*", "campaign.*", "subscriber.*", "delivery.*", "abtest.*", "sequence.*", "webhook.*"]; export declare const OUTBOUND_WEBHOOK_EVENT_FILTERS: readonly ["*", "operation.started", "operation.blocked", "operation.succeeded", "operation.failed", "campaign.scheduled", "campaign.started", "campaign.paused", "campaign.cancelled", "campaign.finished", "subscriber.created", "subscriber.updated", "subscriber.blocklisted", "subscriber.unsubscribed", "delivery.delivered", "delivery.bounced", "delivery.complained", "delivery.delayed", "delivery.rejected", "abtest.started", "abtest.ready-for-analysis", "abtest.winner-selected", "abtest.inconclusive", "abtest.failed", "sequence.created", "sequence.revised", "sequence.enrolled", "sequence.paused", "sequence.resumed", "sequence.reconciled", "sequence.deleted", "webhook.test", "operation.*", "campaign.*", "subscriber.*", "delivery.*", "abtest.*", "sequence.*", "webhook.*"]; export type OutboundWebhookEventType = (typeof OUTBOUND_WEBHOOK_EVENT_TYPES)[number]; export type OutboundWebhookEventFilter = (typeof OUTBOUND_WEBHOOK_EVENT_FILTERS)[number]; export type OutboundWebhookEventSource = OperationEventSource; export type OutboundWebhookSubjectKind = Extract; export type OutboundWebhookDeliveryStatus = "pending" | "delivering" | "retry" | "succeeded" | "exhausted"; export type OutboundWebhookSubject = Readonly<{ kind: OutboundWebhookSubjectKind; key: string; }>; export type OutboundWebhookEvent = Readonly<{ id: string; type: OutboundWebhookEventType; schemaVersion: typeof OUTBOUND_WEBHOOK_EVENT_SCHEMA_VERSION; occurredAt: string; source: OutboundWebhookEventSource; correlationId?: string | undefined; subject?: OutboundWebhookSubject | undefined; data: Readonly>; }>; export type OutboundWebhookEndpoint = Readonly<{ id: string; name: string; url: string; secretRef: string; eventFilters: readonly string[]; enabled: boolean; timeoutMs: number; maxAttempts: number; circuitFailureThreshold: number; circuitCooldownMs: number; createdAt: string; updatedAt: string; }>; export type OutboundWebhookEndpointRuntime = Readonly<{ endpointId: string; consecutiveFailures: number; circuitState: "closed" | "open"; circuitOpenedAt?: string | undefined; circuitOpenUntil?: string | undefined; lastFailureAt?: string | undefined; lastSuccessAt?: string | undefined; }>; export type OutboundWebhookWorkerStatus = "running" | "stopped" | "failed"; export type OutboundWebhookWorker = Readonly<{ id: string; status: OutboundWebhookWorkerStatus; startedAt: string; heartbeatAt: string; stoppedAt?: string | undefined; lastError?: string | undefined; lastTick?: Readonly<{ claimed: number; succeeded: number; retried: number; exhausted: number; completedAt: string; }> | undefined; }>; export type OutboundWebhookDelivery = Readonly<{ id: string; eventId: string; endpointId: string; event: OutboundWebhookEvent; status: OutboundWebhookDeliveryStatus; attemptCount: number; manualRetryCount: number; nextAttemptAt: string; lastAttemptAt?: string | undefined; completedAt?: string | undefined; statusCode?: number | undefined; lastError?: string | undefined; leaseToken?: string | undefined; leaseExpiresAt?: string | undefined; }>; export type OutboundWebhookStore = Readonly<{ version: typeof OUTBOUND_WEBHOOK_STORE_VERSION; endpoints: readonly OutboundWebhookEndpoint[]; deliveries: readonly OutboundWebhookDelivery[]; endpointRuntime: readonly OutboundWebhookEndpointRuntime[]; workers: readonly OutboundWebhookWorker[]; /** * Server-generated high-entropy key for deriving keyed webhook.test * event ids. Independent of every endpoint signing credential so a * delivery-log reader cannot turn the derivation into a signing-key * oracle. Generated lazily on the first keyed probe. */ probeIdKey?: string; }>; export type OutboundWebhookStoreOptions = Readonly<{ path?: string; limit?: number; repository?: OutboundWebhookRepository; }>; export type OutboundWebhookMutationOptions = OutboundWebhookStoreOptions & Readonly<{ now?: Date; }>; export type CreateOutboundWebhookEndpointInput = Readonly<{ name: string; url: string; secretRef: string; eventFilters: readonly string[]; enabled?: boolean; timeoutMs?: number; maxAttempts?: number; circuitFailureThreshold?: number; circuitCooldownMs?: number; }>; export type UpdateOutboundWebhookEndpointInput = Readonly<{ name?: string; url?: string; secretRef?: string; eventFilters?: readonly string[]; enabled?: boolean; timeoutMs?: number; maxAttempts?: number; circuitFailureThreshold?: number; circuitCooldownMs?: number; }>; export type CreateOutboundWebhookEventInput = Readonly<{ id?: string; type: OutboundWebhookEventType; occurredAt?: string; source: OutboundWebhookEventSource; correlationId?: string; subject?: OutboundWebhookSubject; data?: Readonly>; }>; export type EnqueueOutboundWebhookResult = Readonly<{ event: OutboundWebhookEvent; matchedEndpoints: number; queuedDeliveries: number; duplicateDeliveries: number; deliveryIds: readonly string[]; }>; export type OutboundWebhookDeliveryListOptions = Readonly<{ endpointId?: string; status?: OutboundWebhookDeliveryStatus; eventType?: OutboundWebhookEventType; /** Resolves a single delivery by its originating event without a paginated scan. */ eventId?: string; /** Resolves a single delivery by id without a paginated scan. */ deliveryId?: string; /** Restricts the listing to exactly these deliveries before pagination. */ deliveryIds?: readonly string[]; limit?: number; }>; export type DispatchOutboundWebhooksOptions = Readonly<{ store?: OutboundWebhookStoreOptions; limit?: number; now?: Date; fetcher?: typeof fetch; resolveSecret?: (secretRef: string) => string | undefined; leaseMs?: number; baseRetryDelayMs?: number; concurrency?: number; deliveryIds?: readonly string[]; bypassCircuitBreaker?: boolean; /** * Echoed claim set from a prior dispatch: each delivery id paired with * its attempt count at that claim. Recovery claims exactly these * deliveries, and only while their current attempt count still matches * — a delivery anyone has attempted since the echo is skipped, so the * retry cannot deliver past the originally claimed position. */ recoveryClaims?: readonly Readonly<{ id: string; attemptCount: number; }>[]; }>; export type OutboundWebhookDispatchErrorCode = "endpoint_unavailable" | "delivery_unavailable" | "signing_secret_unavailable" | "url_policy_blocked" | "http_rejected" | "lease_conflict" | "delivery_state_conflict" | "delivery_failed"; type ReplayDeadLetterError = Readonly<{ deliveryId: string; errorCode: OutboundWebhookDispatchErrorCode; }>; export type OutboundWebhookDispatchResultEntry = Readonly<{ deliveryId: string; endpointId: string; status: Extract; statusCode?: number | undefined; errorCode?: OutboundWebhookDispatchErrorCode | undefined; }> | Readonly<{ deliveryId: string; endpointId: string; status: "skipped"; errorCode: OutboundWebhookDispatchErrorCode; }>; export type DispatchOutboundWebhooksResult = Readonly<{ claimed: number; succeeded: number; retried: number; exhausted: number; skipped: number; results: readonly OutboundWebhookDispatchResultEntry[]; /** Echoed ids of the exact deliveries this dispatch claimed. */ claimedIds: readonly string[]; /** Echoed claim positions: delivery id plus its attempt count at claim. */ claimSteps: readonly Readonly<{ id: string; attemptCount: number; }>[]; /** Recovery passes only: size of the echoed claim set. */ requested?: number; /** Recovery passes only: skipped members still at their claimed attempt under a lease, backoff, or open circuit — retryable later. */ pendingIds?: readonly string[]; /** Recovery passes only: echoed members that already moved on. */ alreadyDone?: number; }>; export type ClaimedOutboundWebhookDelivery = Readonly<{ delivery: OutboundWebhookDelivery; endpoint?: OutboundWebhookEndpoint | undefined; }>; export type CompleteOutboundWebhookDeliveryResult = Readonly<{ success: boolean; retryable: boolean; statusCode?: number | undefined; error?: unknown; }>; export type ReconcileOutboundWebhooksOptions = Readonly<{ now?: Date; limit?: number; dryRun?: boolean; }>; export type ReconcileOutboundWebhooksResult = Readonly<{ /** Echoed ids of the deliveries the scan considered. */ scannedIds: readonly string[]; scanned: number; recovered: number; exhausted: number; unchanged: number; dryRun: boolean; }>; export type PruneOutboundWebhooksOptions = Readonly<{ before: Date; ids?: readonly string[]; limit?: number; dryRun?: boolean; }>; export type ResolvedPruneOutboundWebhooksOptions = Readonly<{ before: Date; ids: readonly string[] | undefined; limit: number; dryRun: boolean; }>; export type PruneOutboundWebhooksResult = Readonly<{ eligible: number; deleted: number; dryRun: boolean; before: string; ids: readonly string[]; }>; export type ReplayOutboundWebhookDeadLettersOptions = Readonly<{ endpointId?: string; /** Exact dead-letter set reported by a dry run; supplied, exactly that set is replayed. */ deliveryIds?: readonly string[]; /** * Generation binding: replay a delivery only while it is still * exhausted at the echoed pre-request manual retry count. A record a * worker re-exhausted after a replay moved past the echoed generation * and is skipped, so an identical echoed request never starts another * attempt cycle. */ expectedManualRetryCounts?: ReadonlyMap; limit?: number; dryRun?: boolean; now?: Date; }>; export type ReplayOutboundWebhookDeadLettersResult = Readonly<{ eligible: number; replayed: number; failed: number; dryRun: boolean; deliveryIds: readonly string[]; /** * Echoed dead-letter generations: each candidate delivery paired with * its manual retry count as observed before any replay moved it. */ replayedGenerations: readonly Readonly<{ id: string; manualRetryCount: number; }>[]; errors: readonly ReplayDeadLetterError[]; }>; export type OutboundWebhookRuntimeHealth = Readonly<{ store: "file" | "postgres"; schemaVersion: number; healthy: boolean; checkedAt: string; endpoints: Readonly<{ total: number; enabled: number; circuitOpen: number; }>; deliveries: Readonly & { due: number; deadLetter: number; oldestDueAt?: string | undefined; }>; workers: Readonly<{ running: number; stale: number; stopped: number; failed: number; lastHeartbeatAt?: string | undefined; }>; }>; export type UpsertOutboundWebhookWorkerInput = Readonly<{ id: string; status: OutboundWebhookWorkerStatus; startedAt: string; heartbeatAt: string; stoppedAt?: string; lastError?: string; lastTick?: OutboundWebhookWorker["lastTick"]; }>; /** * Durable persistence contract shared by the file-backed runtime and the * Postgres implementation. Implementations own atomicity and lease fencing; * the transport, retry, signature, and redaction policy stays in this module. */ export interface OutboundWebhookRepository { readonly kind: "file" | "postgres"; /** * Resolve the server-generated probe id key for keyed webhook.test * derivations, creating it on first use. Independent of endpoint * signing credentials by design. */ getOrCreateProbeIdKey(): Promise; listEndpoints(): Promise; getEndpoint(id: string): Promise; createEndpoint(endpoint: OutboundWebhookEndpoint): Promise; updateEndpoint(id: string, input: UpdateOutboundWebhookEndpointInput, now: Date): Promise; deleteEndpoint(id: string, now: Date): Promise; enqueue(event: OutboundWebhookEvent, options: Readonly<{ endpointIds?: readonly string[]; bypassEventFilters?: boolean; convergeEventIds?: readonly string[]; limit: number; now: Date; }>): Promise; listDeliveries(options: OutboundWebhookDeliveryListOptions): Promise; retryDelivery(id: string, now: Date, expectedManualRetryCount?: number): Promise; /** Atomically transitions still-exhausted deliveries back to pending. */ replayDeadLetters(options: { endpointId?: string; deliveryIds?: readonly string[]; expectedManualRetryCounts?: ReadonlyMap; limit: number; dryRun: boolean; now: Date; }): Promise; claimDeliveries(options: Readonly<{ limit: number; now: Date; leaseMs: number; deliveryIds?: readonly string[]; excludeDeliveryIds?: readonly string[]; bypassCircuitBreaker?: boolean; /** Recovery binding: only claim a targeted delivery while its current attempt count matches. */ expectedAttemptCounts?: ReadonlyMap; }>): Promise; completeDelivery(claimed: OutboundWebhookDelivery, result: CompleteOutboundWebhookDeliveryResult, endpoint: OutboundWebhookEndpoint | undefined, options: Readonly<{ now: Date; baseRetryDelayMs: number; }>): Promise; reconcile(options: Required & { deliveryIds?: readonly string[]; }): Promise; prune(options: ResolvedPruneOutboundWebhooksOptions): Promise; getRuntimeHealth(options: Readonly<{ now: Date; workerStaleMs: number; }>): Promise; upsertWorker(worker: UpsertOutboundWebhookWorkerInput): Promise; resetEndpointCircuit(endpointId: string, now: Date): Promise; close?(): Promise; } export declare const outboundWebhookEventFilterSchema: z.ZodPipe>; export declare class OutboundWebhookNotFoundError extends Error { constructor(kind: "endpoint" | "delivery", id: string); } export declare class OutboundWebhookConflictError extends Error { readonly code: "conflict" | "endpoint_unavailable" | "delivery_state_conflict" | "lease_conflict" | "signing_secret_unavailable"; constructor(message: string, code?: "conflict" | "endpoint_unavailable" | "delivery_state_conflict" | "lease_conflict" | "signing_secret_unavailable"); } export declare class OutboundWebhookSignatureError extends Error { constructor(message: string); } export declare function mergeOutboundWebhookEndpointUpdate(previous: OutboundWebhookEndpoint, input: UpdateOutboundWebhookEndpointInput, now: Date): OutboundWebhookEndpoint; export declare function parseOutboundWebhookEndpoint(value: unknown): OutboundWebhookEndpoint; export declare function parseOutboundWebhookEvent(value: unknown): OutboundWebhookEvent; export declare function parseOutboundWebhookDelivery(value: unknown): OutboundWebhookDelivery; /** Overrides follow the shared home-anchored path rule, like other stores. */ export declare function getOutboundWebhookStorePath(): string; export declare function createOutboundWebhookStore(path?: string): JsonFileStore; /** * Resolve the server-generated key that derives keyed webhook.test event * ids, creating it on first use. The key is independent of every endpoint * signing credential — keying the derivation to a signing secret would * hand delivery-log readers a known-message/tag oracle for recovering that * secret and forging webhook signatures. */ export declare function ensureOutboundWebhookProbeIdKey(options?: OutboundWebhookStoreOptions): Promise; export declare function createFileOutboundWebhookRepository(options?: Omit): OutboundWebhookRepository; export declare function normalizeOutboundWebhookEndpointUrl(value: string): string; export declare function normalizeEventFilters(filters: readonly string[]): readonly string[]; export declare function isSupportedEventFilter(filter: string): boolean; export declare function matchesOutboundWebhookEvent(filters: readonly string[], eventType: OutboundWebhookEventType): boolean; export declare function redactOutboundWebhookData(data: Readonly>): Readonly>; export declare function createOutboundWebhookEvent(input: CreateOutboundWebhookEventInput): OutboundWebhookEvent; export declare function listOutboundWebhookEndpoints(options?: OutboundWebhookStoreOptions): Promise; export declare function getOutboundWebhookEndpoint(id: string, options?: OutboundWebhookStoreOptions): Promise; export declare function createOutboundWebhookEndpoint(input: CreateOutboundWebhookEndpointInput, options?: OutboundWebhookStoreOptions): Promise; export declare function updateOutboundWebhookEndpoint(id: string, input: UpdateOutboundWebhookEndpointInput, options?: OutboundWebhookMutationOptions): Promise; export declare function deleteOutboundWebhookEndpoint(id: string, options?: OutboundWebhookMutationOptions): Promise; export declare function enqueueOutboundWebhookEvent(input: CreateOutboundWebhookEventInput, options?: OutboundWebhookStoreOptions & { endpointIds?: readonly string[]; bypassEventFilters?: boolean; /** * Alternate event ids that identify the same logical request (for * example a legacy derivation during a rolling upgrade). Checked in * the same store transaction as the enqueue, so a delivery for any * of them collapses this enqueue instead of creating a concurrent * duplicate. */ convergeEventIds?: readonly string[]; now?: Date; }): Promise; /** * Projects the existing privacy-preserving operation audit metadata into the * typed event envelope. The helper avoids creating an empty outbox file when * no enabled endpoint subscribes to operation lifecycle events. */ export declare function enqueueOperationLifecycleEvent(input: Required> & RecordOperationAuditInput, options?: OutboundWebhookStoreOptions): Promise; /** * Persists the canonical metadata-only audit event first, then best-effort * projects the same execution into the outbound event outbox. A webhook store * failure must never replace an operation result or invite a duplicate retry. */ export declare function recordOperationAuditWithLifecycle(input: RecordOperationAuditInput, options?: Readonly<{ audit?: OperationAuditStoreOptions; webhook?: OutboundWebhookStoreOptions; onLifecycleError?: (error: unknown) => void; }>): Promise; export declare function listOutboundWebhookDeliveries(options?: OutboundWebhookDeliveryListOptions & OutboundWebhookStoreOptions): Promise; export type RetryOutboundWebhookDeliveryResult = Readonly<{ delivery: OutboundWebhookDelivery; retried: boolean; retryGeneration: number; }>; export declare function retryOutboundWebhookDelivery(id: string, options?: OutboundWebhookMutationOptions & { /** * Generation binding: only retry when the delivery's current * manual retry count still matches this echoed pre-request value. * A delivery a dispatcher already completed and returned to retry * after the original request is skipped, not given another cycle. */ expectedManualRetryCount?: number; }): Promise; export declare function replayOutboundWebhookDeadLetters(options?: ReplayOutboundWebhookDeadLettersOptions & OutboundWebhookStoreOptions): Promise; /** * Recover deliveries left in `delivering` after a worker crash. Active * endpoints are returned to retry; missing or disabled endpoints are exhausted. * Lease ownership is always cleared so an old worker cannot commit afterward. */ export declare function reconcileOutboundWebhookDeliveries(options?: ReconcileOutboundWebhooksOptions & OutboundWebhookStoreOptions & { /** Recovery binding: restrict the expired-lease scan to exactly these deliveries. */ deliveryIds?: readonly string[]; }): Promise; /** * Remove bounded terminal delivery history older than `before`. Active and * retryable records are never eligible, even when their timestamps are old. * Without `ids`, the oldest eligible batch up to `limit` is selected; with * `ids`, exactly that set is matched against the same terminal/cutoff * criteria (stale or non-terminal ids are silently skipped) and `limit` * and oldest-first ordering do not apply. */ export declare function pruneOutboundWebhookDeliveries(options: PruneOutboundWebhooksOptions & OutboundWebhookStoreOptions): Promise; export declare function truncateOutboundWebhookError(error: unknown): string; export declare function outboundWebhookLeaseReconciliationError(canRetry: boolean, endpointEnabled: boolean): string; export declare function outboundWebhookRetryDelayMs(attemptCount: number, baseDelayMs: number): number; export declare function resetOutboundWebhookEndpointCircuit(endpointId: string, options?: OutboundWebhookMutationOptions): Promise; export declare function upsertOutboundWebhookWorker(input: UpsertOutboundWebhookWorkerInput, options?: OutboundWebhookStoreOptions): Promise; export declare function getOutboundWebhookRuntimeHealth(options?: OutboundWebhookStoreOptions & { now?: Date; workerStaleMs?: number; }): Promise; export declare function summarizeOutboundWebhookRuntimeHealth(store: "file" | "postgres", endpoints: readonly OutboundWebhookEndpoint[], deliveries: readonly OutboundWebhookDelivery[], endpointRuntime: readonly OutboundWebhookEndpointRuntime[], workers: readonly OutboundWebhookWorker[], now: Date, workerStaleMs: number): OutboundWebhookRuntimeHealth; export declare function signOutboundWebhookPayload(secret: string, timestamp: string, body: string): string; export declare function verifyOutboundWebhookSignature(input: { secret: string; timestamp: string; body: string; signature: string; now?: Date; toleranceMs?: number; }): boolean; /** * A dispatch that failed after claiming deliveries. The claimed positions * ride along as the recovery handle: adapters serialize * {@link WebhookDispatchFailureError.toStructuredDetails} into structured * error details so an ambiguous retry can echo exactly that set as * recovery_set instead of claiming new due work. */ export declare class WebhookDispatchFailureError extends AggregateError { readonly claimSteps: readonly Readonly<{ id: string; attemptCount: number; }>[]; constructor(failures: readonly unknown[], message: string, claimSteps: readonly Readonly<{ id: string; attemptCount: number; }>[]); /** Recovery handle for adapters: the echoed claim set this failure leaves. */ toStructuredDetails(): Record; } export declare function dispatchOutboundWebhooks(options?: DispatchOutboundWebhooksOptions): Promise; export {};