File size: 3,540 Bytes
46252cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/**
 * 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<HookEvent, true> = {
  '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<HookEvent> = 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<string>).has(event);
}

export interface HookContext<T = unknown> {
  event: HookEvent;
  data: T;
  sessionId?: string;
  timestamp: Date;
  source: string; // Which service emitted this
}

export interface HookResult<T = unknown> {
  /**
   * `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<T = unknown> = (ctx: HookContext<T>) => Promise<HookResult<T>>;

export interface HookRegistration {
  id: string;
  pluginId: string;
  event: HookEvent;
  handler: HookHandler;
  priority: number; // Lower = runs first
}