/** * Hook System Interfaces * Central event bus for plugin integration */ export type HookEvent = // Session lifecycle | 'session:created' | 'session:starting' | 'session:ready' | 'session:qr' | 'session:disconnected' | 'session:error' | 'session:deleted' // Message lifecycle | 'message:received' | 'message:sending' | 'message:sent' | 'message:failed' | 'message:ack' | 'message:persisted' // Webhook lifecycle | 'webhook:before' | 'webhook:queued' // After webhook job added to queue (queue mode only) | 'webhook:delivered' // After webhook successfully delivered (direct or queue) | 'webhook:after' // After webhook attempt (direct mode only, deprecated for queue) | 'webhook:error' // After webhook delivery failed (all retries exhausted) // Ingress (inbound provider webhook -> plugin) lifecycle | 'ingress:error'; // After an ingress dispatch failed (all retries exhausted) // Runtime allowlist of every HookEvent. The Record is exhaustively typed, so adding a HookEvent above // without listing it here is a COMPILE error — keeping the runtime set in lockstep with the type. // Used to reject fabricated event names at the sandbox IPC boundary (HookEvent is type-only; the wire // payload is an arbitrary string), bounding host-registry growth to this finite set. const HOOK_EVENT_REGISTRY: Record = { 'session:created': true, 'session:starting': true, 'session:ready': true, 'session:qr': true, 'session:disconnected': true, 'session:error': true, 'session:deleted': true, 'message:received': true, 'message:sending': true, 'message:sent': true, 'message:failed': true, 'message:ack': true, 'message:persisted': true, 'webhook:before': true, 'webhook:queued': true, 'webhook:delivered': true, 'webhook:after': true, 'webhook:error': true, 'ingress:error': true, }; export const KNOWN_HOOK_EVENTS: ReadonlySet = new Set(Object.keys(HOOK_EVENT_REGISTRY) as HookEvent[]); /** Type guard: is `event` one of the known HookEvent values? Narrows an untrusted string to HookEvent. */ export function isKnownHookEvent(event: string): event is HookEvent { return (KNOWN_HOOK_EVENTS as ReadonlySet).has(event); } export interface HookContext { event: HookEvent; data: T; sessionId?: string; timestamp: Date; source: string; // Which service emitted this } export interface HookResult { /** * `false` stops the handler chain: the handlers registered after this one do not run. * * That is its whole meaning on a notification event (`message:received`, `message:sent`, …). The * message has already arrived at, or left, WhatsApp — a handler can keep other plugins from acting on * it, but the gateway still records it and still dispatches it to webhooks and the websocket. Use it * to claim an event ("I answered this, don't let another bot answer too"), not to hide one. * * On a pre-action event it is a veto, because the action has not happened yet: `false` on * `message:sending` blocks the send (core/hooks/sending-gate.ts), and on `webhook:before` it cancels * that delivery. */ continue: boolean; data?: T; // Modified data (optional) error?: Error; // Error to propagate } export type HookHandler = (ctx: HookContext) => Promise>; export interface HookRegistration { id: string; pluginId: string; event: HookEvent; handler: HookHandler; priority: number; // Lower = runs first }