@teneo-protocol/sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/.dockerignore +14 -0
  2. package/.env.test.example +14 -0
  3. package/.eslintrc.json +26 -0
  4. package/.github/workflows/claude-code-review.yml +78 -0
  5. package/.github/workflows/claude-reviewer.yml +64 -0
  6. package/.github/workflows/publish-npm.yml +38 -0
  7. package/.github/workflows/push-to-main.yml +23 -0
  8. package/.node-version +1 -0
  9. package/.prettierrc +11 -0
  10. package/Dockerfile +25 -0
  11. package/LICENCE +661 -0
  12. package/README.md +709 -0
  13. package/dist/constants.d.ts +42 -0
  14. package/dist/constants.d.ts.map +1 -0
  15. package/dist/constants.js +45 -0
  16. package/dist/constants.js.map +1 -0
  17. package/dist/core/websocket-client.d.ts +261 -0
  18. package/dist/core/websocket-client.d.ts.map +1 -0
  19. package/dist/core/websocket-client.js +875 -0
  20. package/dist/core/websocket-client.js.map +1 -0
  21. package/dist/formatters/response-formatter.d.ts +354 -0
  22. package/dist/formatters/response-formatter.d.ts.map +1 -0
  23. package/dist/formatters/response-formatter.js +575 -0
  24. package/dist/formatters/response-formatter.js.map +1 -0
  25. package/dist/handlers/message-handler-registry.d.ts +155 -0
  26. package/dist/handlers/message-handler-registry.d.ts.map +1 -0
  27. package/dist/handlers/message-handler-registry.js +216 -0
  28. package/dist/handlers/message-handler-registry.js.map +1 -0
  29. package/dist/handlers/message-handlers/agent-selected-handler.d.ts +112 -0
  30. package/dist/handlers/message-handlers/agent-selected-handler.d.ts.map +1 -0
  31. package/dist/handlers/message-handlers/agent-selected-handler.js +40 -0
  32. package/dist/handlers/message-handlers/agent-selected-handler.js.map +1 -0
  33. package/dist/handlers/message-handlers/agents-list-handler.d.ts +14 -0
  34. package/dist/handlers/message-handlers/agents-list-handler.d.ts.map +1 -0
  35. package/dist/handlers/message-handlers/agents-list-handler.js +25 -0
  36. package/dist/handlers/message-handlers/agents-list-handler.js.map +1 -0
  37. package/dist/handlers/message-handlers/auth-error-handler.d.ts +71 -0
  38. package/dist/handlers/message-handlers/auth-error-handler.d.ts.map +1 -0
  39. package/dist/handlers/message-handlers/auth-error-handler.js +30 -0
  40. package/dist/handlers/message-handlers/auth-error-handler.js.map +1 -0
  41. package/dist/handlers/message-handlers/auth-message-handler.d.ts +18 -0
  42. package/dist/handlers/message-handlers/auth-message-handler.d.ts.map +1 -0
  43. package/dist/handlers/message-handlers/auth-message-handler.js +60 -0
  44. package/dist/handlers/message-handlers/auth-message-handler.js.map +1 -0
  45. package/dist/handlers/message-handlers/auth-required-handler.d.ts +76 -0
  46. package/dist/handlers/message-handlers/auth-required-handler.d.ts.map +1 -0
  47. package/dist/handlers/message-handlers/auth-required-handler.js +23 -0
  48. package/dist/handlers/message-handlers/auth-required-handler.js.map +1 -0
  49. package/dist/handlers/message-handlers/auth-success-handler.d.ts +18 -0
  50. package/dist/handlers/message-handlers/auth-success-handler.d.ts.map +1 -0
  51. package/dist/handlers/message-handlers/auth-success-handler.js +51 -0
  52. package/dist/handlers/message-handlers/auth-success-handler.js.map +1 -0
  53. package/dist/handlers/message-handlers/base-handler.d.ts +55 -0
  54. package/dist/handlers/message-handlers/base-handler.d.ts.map +1 -0
  55. package/dist/handlers/message-handlers/base-handler.js +83 -0
  56. package/dist/handlers/message-handlers/base-handler.js.map +1 -0
  57. package/dist/handlers/message-handlers/challenge-handler.d.ts +73 -0
  58. package/dist/handlers/message-handlers/challenge-handler.d.ts.map +1 -0
  59. package/dist/handlers/message-handlers/challenge-handler.js +47 -0
  60. package/dist/handlers/message-handlers/challenge-handler.js.map +1 -0
  61. package/dist/handlers/message-handlers/error-message-handler.d.ts +76 -0
  62. package/dist/handlers/message-handlers/error-message-handler.d.ts.map +1 -0
  63. package/dist/handlers/message-handlers/error-message-handler.js +29 -0
  64. package/dist/handlers/message-handlers/error-message-handler.js.map +1 -0
  65. package/dist/handlers/message-handlers/index.d.ts +28 -0
  66. package/dist/handlers/message-handlers/index.d.ts.map +1 -0
  67. package/dist/handlers/message-handlers/index.js +100 -0
  68. package/dist/handlers/message-handlers/index.js.map +1 -0
  69. package/dist/handlers/message-handlers/list-rooms-response-handler.d.ts +122 -0
  70. package/dist/handlers/message-handlers/list-rooms-response-handler.d.ts.map +1 -0
  71. package/dist/handlers/message-handlers/list-rooms-response-handler.js +30 -0
  72. package/dist/handlers/message-handlers/list-rooms-response-handler.js.map +1 -0
  73. package/dist/handlers/message-handlers/ping-pong-handler.d.ts +104 -0
  74. package/dist/handlers/message-handlers/ping-pong-handler.d.ts.map +1 -0
  75. package/dist/handlers/message-handlers/ping-pong-handler.js +36 -0
  76. package/dist/handlers/message-handlers/ping-pong-handler.js.map +1 -0
  77. package/dist/handlers/message-handlers/regular-message-handler.d.ts +56 -0
  78. package/dist/handlers/message-handlers/regular-message-handler.d.ts.map +1 -0
  79. package/dist/handlers/message-handlers/regular-message-handler.js +59 -0
  80. package/dist/handlers/message-handlers/regular-message-handler.js.map +1 -0
  81. package/dist/handlers/message-handlers/subscribe-response-handler.d.ts +81 -0
  82. package/dist/handlers/message-handlers/subscribe-response-handler.d.ts.map +1 -0
  83. package/dist/handlers/message-handlers/subscribe-response-handler.js +48 -0
  84. package/dist/handlers/message-handlers/subscribe-response-handler.js.map +1 -0
  85. package/dist/handlers/message-handlers/task-response-handler.d.ts +14 -0
  86. package/dist/handlers/message-handlers/task-response-handler.d.ts.map +1 -0
  87. package/dist/handlers/message-handlers/task-response-handler.js +44 -0
  88. package/dist/handlers/message-handlers/task-response-handler.js.map +1 -0
  89. package/dist/handlers/message-handlers/types.d.ts +51 -0
  90. package/dist/handlers/message-handlers/types.d.ts.map +1 -0
  91. package/dist/handlers/message-handlers/types.js +7 -0
  92. package/dist/handlers/message-handlers/types.js.map +1 -0
  93. package/dist/handlers/message-handlers/unsubscribe-response-handler.d.ts +81 -0
  94. package/dist/handlers/message-handlers/unsubscribe-response-handler.d.ts.map +1 -0
  95. package/dist/handlers/message-handlers/unsubscribe-response-handler.js +48 -0
  96. package/dist/handlers/message-handlers/unsubscribe-response-handler.js.map +1 -0
  97. package/dist/handlers/webhook-handler.d.ts +202 -0
  98. package/dist/handlers/webhook-handler.d.ts.map +1 -0
  99. package/dist/handlers/webhook-handler.js +511 -0
  100. package/dist/handlers/webhook-handler.js.map +1 -0
  101. package/dist/index.d.ts +71 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +217 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/managers/agent-registry.d.ts +173 -0
  106. package/dist/managers/agent-registry.d.ts.map +1 -0
  107. package/dist/managers/agent-registry.js +310 -0
  108. package/dist/managers/agent-registry.js.map +1 -0
  109. package/dist/managers/connection-manager.d.ts +134 -0
  110. package/dist/managers/connection-manager.d.ts.map +1 -0
  111. package/dist/managers/connection-manager.js +176 -0
  112. package/dist/managers/connection-manager.js.map +1 -0
  113. package/dist/managers/index.d.ts +9 -0
  114. package/dist/managers/index.d.ts.map +1 -0
  115. package/dist/managers/index.js +16 -0
  116. package/dist/managers/index.js.map +1 -0
  117. package/dist/managers/message-router.d.ts +112 -0
  118. package/dist/managers/message-router.d.ts.map +1 -0
  119. package/dist/managers/message-router.js +260 -0
  120. package/dist/managers/message-router.js.map +1 -0
  121. package/dist/managers/room-manager.d.ts +165 -0
  122. package/dist/managers/room-manager.d.ts.map +1 -0
  123. package/dist/managers/room-manager.js +227 -0
  124. package/dist/managers/room-manager.js.map +1 -0
  125. package/dist/teneo-sdk.d.ts +703 -0
  126. package/dist/teneo-sdk.d.ts.map +1 -0
  127. package/dist/teneo-sdk.js +907 -0
  128. package/dist/teneo-sdk.js.map +1 -0
  129. package/dist/types/config.d.ts +1047 -0
  130. package/dist/types/config.d.ts.map +1 -0
  131. package/dist/types/config.js +720 -0
  132. package/dist/types/config.js.map +1 -0
  133. package/dist/types/error-codes.d.ts +29 -0
  134. package/dist/types/error-codes.d.ts.map +1 -0
  135. package/dist/types/error-codes.js +41 -0
  136. package/dist/types/error-codes.js.map +1 -0
  137. package/dist/types/events.d.ts +616 -0
  138. package/dist/types/events.d.ts.map +1 -0
  139. package/dist/types/events.js +261 -0
  140. package/dist/types/events.js.map +1 -0
  141. package/dist/types/health.d.ts +40 -0
  142. package/dist/types/health.d.ts.map +1 -0
  143. package/dist/types/health.js +6 -0
  144. package/dist/types/health.js.map +1 -0
  145. package/dist/types/index.d.ts +10 -0
  146. package/dist/types/index.d.ts.map +1 -0
  147. package/dist/types/index.js +123 -0
  148. package/dist/types/index.js.map +1 -0
  149. package/dist/types/messages.d.ts +3734 -0
  150. package/dist/types/messages.d.ts.map +1 -0
  151. package/dist/types/messages.js +482 -0
  152. package/dist/types/messages.js.map +1 -0
  153. package/dist/types/validation.d.ts +81 -0
  154. package/dist/types/validation.d.ts.map +1 -0
  155. package/dist/types/validation.js +115 -0
  156. package/dist/types/validation.js.map +1 -0
  157. package/dist/utils/bounded-queue.d.ts +127 -0
  158. package/dist/utils/bounded-queue.d.ts.map +1 -0
  159. package/dist/utils/bounded-queue.js +181 -0
  160. package/dist/utils/bounded-queue.js.map +1 -0
  161. package/dist/utils/circuit-breaker.d.ts +141 -0
  162. package/dist/utils/circuit-breaker.d.ts.map +1 -0
  163. package/dist/utils/circuit-breaker.js +215 -0
  164. package/dist/utils/circuit-breaker.js.map +1 -0
  165. package/dist/utils/deduplication-cache.d.ts +110 -0
  166. package/dist/utils/deduplication-cache.d.ts.map +1 -0
  167. package/dist/utils/deduplication-cache.js +177 -0
  168. package/dist/utils/deduplication-cache.js.map +1 -0
  169. package/dist/utils/event-waiter.d.ts +101 -0
  170. package/dist/utils/event-waiter.d.ts.map +1 -0
  171. package/dist/utils/event-waiter.js +118 -0
  172. package/dist/utils/event-waiter.js.map +1 -0
  173. package/dist/utils/index.d.ts +51 -0
  174. package/dist/utils/index.d.ts.map +1 -0
  175. package/dist/utils/index.js +72 -0
  176. package/dist/utils/index.js.map +1 -0
  177. package/dist/utils/logger.d.ts +22 -0
  178. package/dist/utils/logger.d.ts.map +1 -0
  179. package/dist/utils/logger.js +91 -0
  180. package/dist/utils/logger.js.map +1 -0
  181. package/dist/utils/rate-limiter.d.ts +122 -0
  182. package/dist/utils/rate-limiter.d.ts.map +1 -0
  183. package/dist/utils/rate-limiter.js +190 -0
  184. package/dist/utils/rate-limiter.js.map +1 -0
  185. package/dist/utils/retry-policy.d.ts +191 -0
  186. package/dist/utils/retry-policy.d.ts.map +1 -0
  187. package/dist/utils/retry-policy.js +225 -0
  188. package/dist/utils/retry-policy.js.map +1 -0
  189. package/dist/utils/secure-private-key.d.ts +113 -0
  190. package/dist/utils/secure-private-key.d.ts.map +1 -0
  191. package/dist/utils/secure-private-key.js +188 -0
  192. package/dist/utils/secure-private-key.js.map +1 -0
  193. package/dist/utils/signature-verifier.d.ts +143 -0
  194. package/dist/utils/signature-verifier.d.ts.map +1 -0
  195. package/dist/utils/signature-verifier.js +238 -0
  196. package/dist/utils/signature-verifier.js.map +1 -0
  197. package/dist/utils/ssrf-validator.d.ts +36 -0
  198. package/dist/utils/ssrf-validator.d.ts.map +1 -0
  199. package/dist/utils/ssrf-validator.js +195 -0
  200. package/dist/utils/ssrf-validator.js.map +1 -0
  201. package/examples/.env.example +17 -0
  202. package/examples/basic-usage.ts +211 -0
  203. package/examples/production-dashboard/.env.example +153 -0
  204. package/examples/production-dashboard/package.json +39 -0
  205. package/examples/production-dashboard/public/dashboard.html +642 -0
  206. package/examples/production-dashboard/server.ts +753 -0
  207. package/examples/webhook-integration.ts +239 -0
  208. package/examples/x-influencer-battle-redesign.html +1065 -0
  209. package/examples/x-influencer-battle-server.ts +217 -0
  210. package/examples/x-influencer-battle.html +787 -0
  211. package/package.json +65 -0
  212. package/src/constants.ts +43 -0
  213. package/src/core/websocket-client.test.ts +512 -0
  214. package/src/core/websocket-client.ts +1056 -0
  215. package/src/formatters/response-formatter.test.ts +571 -0
  216. package/src/formatters/response-formatter.ts +677 -0
  217. package/src/handlers/message-handler-registry.ts +239 -0
  218. package/src/handlers/message-handlers/agent-selected-handler.ts +40 -0
  219. package/src/handlers/message-handlers/agents-list-handler.ts +26 -0
  220. package/src/handlers/message-handlers/auth-error-handler.ts +31 -0
  221. package/src/handlers/message-handlers/auth-message-handler.ts +66 -0
  222. package/src/handlers/message-handlers/auth-required-handler.ts +23 -0
  223. package/src/handlers/message-handlers/auth-success-handler.ts +57 -0
  224. package/src/handlers/message-handlers/base-handler.ts +101 -0
  225. package/src/handlers/message-handlers/challenge-handler.ts +57 -0
  226. package/src/handlers/message-handlers/error-message-handler.ts +27 -0
  227. package/src/handlers/message-handlers/index.ts +77 -0
  228. package/src/handlers/message-handlers/list-rooms-response-handler.ts +28 -0
  229. package/src/handlers/message-handlers/ping-pong-handler.ts +30 -0
  230. package/src/handlers/message-handlers/regular-message-handler.ts +65 -0
  231. package/src/handlers/message-handlers/subscribe-response-handler.ts +47 -0
  232. package/src/handlers/message-handlers/task-response-handler.ts +45 -0
  233. package/src/handlers/message-handlers/types.ts +77 -0
  234. package/src/handlers/message-handlers/unsubscribe-response-handler.ts +47 -0
  235. package/src/handlers/webhook-handler.test.ts +789 -0
  236. package/src/handlers/webhook-handler.ts +576 -0
  237. package/src/index.ts +269 -0
  238. package/src/managers/agent-registry.test.ts +466 -0
  239. package/src/managers/agent-registry.ts +347 -0
  240. package/src/managers/connection-manager.ts +195 -0
  241. package/src/managers/index.ts +9 -0
  242. package/src/managers/message-router.ts +349 -0
  243. package/src/managers/room-manager.ts +248 -0
  244. package/src/teneo-sdk.ts +1022 -0
  245. package/src/types/config.test.ts +325 -0
  246. package/src/types/config.ts +799 -0
  247. package/src/types/error-codes.ts +44 -0
  248. package/src/types/events.test.ts +302 -0
  249. package/src/types/events.ts +382 -0
  250. package/src/types/health.ts +46 -0
  251. package/src/types/index.ts +199 -0
  252. package/src/types/messages.test.ts +660 -0
  253. package/src/types/messages.ts +570 -0
  254. package/src/types/validation.ts +123 -0
  255. package/src/utils/bounded-queue.test.ts +356 -0
  256. package/src/utils/bounded-queue.ts +205 -0
  257. package/src/utils/circuit-breaker.test.ts +394 -0
  258. package/src/utils/circuit-breaker.ts +262 -0
  259. package/src/utils/deduplication-cache.test.ts +380 -0
  260. package/src/utils/deduplication-cache.ts +198 -0
  261. package/src/utils/event-waiter.test.ts +381 -0
  262. package/src/utils/event-waiter.ts +172 -0
  263. package/src/utils/index.ts +74 -0
  264. package/src/utils/logger.ts +87 -0
  265. package/src/utils/rate-limiter.test.ts +341 -0
  266. package/src/utils/rate-limiter.ts +211 -0
  267. package/src/utils/retry-policy.test.ts +558 -0
  268. package/src/utils/retry-policy.ts +272 -0
  269. package/src/utils/secure-private-key.test.ts +356 -0
  270. package/src/utils/secure-private-key.ts +205 -0
  271. package/src/utils/signature-verifier.test.ts +464 -0
  272. package/src/utils/signature-verifier.ts +298 -0
  273. package/src/utils/ssrf-validator.test.ts +372 -0
  274. package/src/utils/ssrf-validator.ts +224 -0
  275. package/tests/integration/real-server.test.ts +740 -0
  276. package/tests/integration/websocket.test.ts +381 -0
  277. package/tests/integration-setup.ts +16 -0
  278. package/tests/setup.ts +34 -0
  279. package/tsconfig.json +32 -0
  280. package/vitest.config.ts +42 -0
  281. package/vitest.integration.config.ts +23 -0
@@ -0,0 +1,576 @@
1
+ /**
2
+ * Webhook handler for Teneo Protocol SDK
3
+ * Manages webhook delivery with retries and error handling using Zod validation
4
+ */
5
+
6
+ import fetch, { RequestInit } from "node-fetch";
7
+ import { EventEmitter } from "eventemitter3";
8
+ import { z } from "zod";
9
+ import { validateWebhookUrl as validateSSRF } from "../utils/ssrf-validator";
10
+ import { BoundedQueue } from "../utils/bounded-queue";
11
+ import { CircuitBreaker, CircuitBreakerError } from "../utils/circuit-breaker";
12
+ import { RetryPolicy } from "../utils/retry-policy";
13
+ import {
14
+ WebhookConfig,
15
+ WebhookConfigSchema,
16
+ WebhookEventType,
17
+ WebhookEventTypeSchema,
18
+ WebhookPayload,
19
+ WebhookPayloadSchema,
20
+ BaseMessage,
21
+ BaseMessageSchema,
22
+ Logger,
23
+ LoggerSchema,
24
+ SDKConfig,
25
+ SDKConfigSchema
26
+ } from "../types";
27
+ import { SDKEvents, WebhookError, ValidationError } from "../types/events";
28
+ import { TIMEOUTS, RETRY } from "../constants";
29
+
30
+ interface WebhookQueueItem {
31
+ payload: WebhookPayload;
32
+ attempts: number;
33
+ lastAttempt?: Date;
34
+ nextRetry?: Date;
35
+ error?: Error;
36
+ }
37
+
38
+ export class WebhookHandler extends EventEmitter<SDKEvents> {
39
+ private readonly config: SDKConfig;
40
+ private webhookConfig?: WebhookConfig;
41
+ private readonly logger: Logger;
42
+ private queue: BoundedQueue<WebhookQueueItem>;
43
+ private circuitBreaker: CircuitBreaker;
44
+ private retryPolicy: RetryPolicy;
45
+ private isProcessing = false;
46
+ private processTimer?: NodeJS.Timeout;
47
+ private isDestroyed = false;
48
+
49
+ constructor(config: SDKConfig, logger: Logger) {
50
+ super();
51
+ // Validate config and logger with Zod
52
+ this.config = SDKConfigSchema.parse(config);
53
+ this.logger = LoggerSchema.parse(logger);
54
+
55
+ // Initialize bounded queue to prevent unbounded memory growth (CB-1)
56
+ // Max 1000 webhooks, drop oldest on overflow
57
+ this.queue = new BoundedQueue<WebhookQueueItem>(1000, "drop-oldest");
58
+
59
+ // Initialize circuit breaker for fault tolerance (CB-3)
60
+ // Opens after 5 failures, closes after 2 successes, 60s timeout
61
+ this.circuitBreaker = new CircuitBreaker({
62
+ failureThreshold: 5,
63
+ successThreshold: 2,
64
+ timeout: 60000,
65
+ windowSize: 60000
66
+ });
67
+
68
+ // Initialize webhook retry policy (REL-3)
69
+ if (this.config.webhookRetryStrategy) {
70
+ // User provided custom strategy
71
+ this.retryPolicy = new RetryPolicy(this.config.webhookRetryStrategy);
72
+ this.logger.info("Custom webhook retry strategy configured", {
73
+ type: this.config.webhookRetryStrategy.type,
74
+ baseDelay: this.config.webhookRetryStrategy.baseDelay,
75
+ maxDelay: this.config.webhookRetryStrategy.maxDelay,
76
+ maxAttempts: this.config.webhookRetryStrategy.maxAttempts
77
+ });
78
+ } else {
79
+ // Use default exponential backoff matching previous hardcoded behavior
80
+ // Previous: RETRY.BASE_DELAY (1000ms) * 2^(attempt-1), max RETRY.MAX_WEBHOOK_DELAY (30000ms)
81
+ // Default retries: 3
82
+ this.retryPolicy = RetryPolicy.exponential(
83
+ RETRY.BASE_DELAY, // 1000ms base delay
84
+ RETRY.MAX_WEBHOOK_DELAY, // 30000ms max delay
85
+ this.config.webhookRetries ?? 3,
86
+ false // jitter disabled by default for webhooks
87
+ );
88
+ }
89
+
90
+ if (this.config.webhookUrl) {
91
+ // Create and validate webhook configuration
92
+ this.webhookConfig = WebhookConfigSchema.parse({
93
+ url: this.config.webhookUrl,
94
+ headers: this.config.webhookHeaders,
95
+ retries: this.config.webhookRetries ?? 3,
96
+ timeout: this.config.webhookTimeout ?? TIMEOUTS.WEBHOOK_TIMEOUT,
97
+ events: ["message", "task_response", "agent_selected", "error"]
98
+ });
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Configures or updates webhook settings with runtime validation.
104
+ * Validates the webhook URL for security (HTTPS requirement except localhost)
105
+ * and sets up retry logic, timeout, and event filtering.
106
+ *
107
+ * @param config - Webhook configuration object
108
+ * @param config.url - Webhook endpoint URL (must be HTTPS unless localhost)
109
+ * @param config.headers - Optional HTTP headers to include with requests
110
+ * @param config.retries - Maximum number of retry attempts for failed deliveries (default: 3)
111
+ * @param config.timeout - Request timeout in milliseconds (default: 30000)
112
+ * @param config.events - Array of event types to send webhooks for (default: all events)
113
+ * @throws {WebhookError} If URL is invalid, insecure, or points to private IP ranges
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * webhookHandler.configure({
118
+ * url: 'https://api.example.com/webhooks',
119
+ * headers: { 'Authorization': 'Bearer token' },
120
+ * retries: 5,
121
+ * timeout: 60000,
122
+ * events: ['message', 'agent_selected', 'error']
123
+ * });
124
+ * ```
125
+ */
126
+ public configure(config: WebhookConfig): void {
127
+ // Validate configuration with Zod
128
+ const validatedConfig = WebhookConfigSchema.parse(config);
129
+
130
+ this.validateWebhookUrl(validatedConfig.url);
131
+
132
+ this.webhookConfig = {
133
+ ...validatedConfig,
134
+ retries: validatedConfig.retries ?? 3,
135
+ timeout: validatedConfig.timeout ?? TIMEOUTS.WEBHOOK_TIMEOUT,
136
+ events: validatedConfig.events ?? ["message", "task_response", "agent_selected", "error"]
137
+ };
138
+
139
+ this.logger.info("Webhook configured", {
140
+ url: validatedConfig.url,
141
+ events: this.webhookConfig.events
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Sends a webhook for a specific event type with payload validation.
147
+ * Validates event type and payload with Zod schemas before queueing.
148
+ * Webhooks are queued and delivered asynchronously with retry logic.
149
+ * Emits 'webhook:sent', 'webhook:success', or 'webhook:error' events.
150
+ *
151
+ * @param eventType - Type of event to send ('message', 'task_response', 'agent_selected', or 'error')
152
+ * @param data - Event-specific data payload (validated with Zod)
153
+ * @param metadata - Optional metadata to include with the event
154
+ * @returns Promise that resolves when webhook is queued (not when delivered)
155
+ * @throws {ValidationError} If eventType or data fail Zod validation
156
+ *
157
+ * @example
158
+ * ```typescript
159
+ * await webhookHandler.sendWebhook('agent_selected', {
160
+ * agent_id: 'weather-agent',
161
+ * agent_name: 'Weather Agent',
162
+ * capabilities: ['weather-forecast']
163
+ * }, {
164
+ * agentId: 'weather-agent',
165
+ * taskId: 'task-123'
166
+ * });
167
+ * ```
168
+ */
169
+ public async sendWebhook(eventType: WebhookEventType, data: any, metadata?: any): Promise<void> {
170
+ if (!this.webhookConfig || this.isDestroyed) {
171
+ return;
172
+ }
173
+
174
+ try {
175
+ // Validate event type
176
+ const validatedEventType = WebhookEventTypeSchema.parse(eventType);
177
+
178
+ // Check if this event type should trigger a webhook
179
+ if (this.webhookConfig.events && !this.webhookConfig.events.includes(validatedEventType)) {
180
+ return;
181
+ }
182
+
183
+ // Create and validate payload
184
+ const payload = WebhookPayloadSchema.parse({
185
+ event: validatedEventType,
186
+ timestamp: new Date().toISOString(),
187
+ data,
188
+ metadata
189
+ });
190
+
191
+ // Add to queue (bounded - will drop oldest if full)
192
+ const pushed = this.queue.push({
193
+ payload,
194
+ attempts: 0
195
+ });
196
+
197
+ if (!pushed) {
198
+ this.logger.warn("Webhook queue full - oldest webhook dropped", {
199
+ queueSize: this.queue.size()
200
+ });
201
+ }
202
+
203
+ // Process queue
204
+ this.processQueue();
205
+ } catch (error) {
206
+ if (error instanceof z.ZodError) {
207
+ this.logger.error("Invalid webhook payload", error);
208
+ throw new ValidationError("Invalid webhook payload", error);
209
+ }
210
+ throw error;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Sends a webhook specifically for message events with automatic event type detection.
216
+ * Filters out system messages (ping, pong, auth) and maps message types to webhook events.
217
+ * Validates message with Zod schema before sending.
218
+ *
219
+ * @param message - The message to send as a webhook
220
+ * @returns Promise that resolves when webhook is queued
221
+ * @throws {ValidationError} If message fails Zod validation
222
+ *
223
+ * @example
224
+ * ```typescript
225
+ * const userMessage = createUserMessage('Hello', 'general', walletAddress);
226
+ * await webhookHandler.sendMessageWebhook(userMessage);
227
+ * // Webhook will be sent with event type 'message'
228
+ * ```
229
+ */
230
+ public async sendMessageWebhook(message: BaseMessage): Promise<void> {
231
+ if (!message) return;
232
+
233
+ // Skip system messages that shouldn't be sent as webhooks
234
+ const skipMessageTypes = [
235
+ "agents",
236
+ "auth",
237
+ "auth_required",
238
+ "pong",
239
+ "ping",
240
+ "auth_success",
241
+ "auth_error",
242
+ "challenge",
243
+ "request_challenge"
244
+ ];
245
+ if (skipMessageTypes.includes(message.type)) {
246
+ return;
247
+ }
248
+
249
+ try {
250
+ // Validate message with Zod
251
+ const validatedMessage = BaseMessageSchema.parse(message);
252
+
253
+ let eventType: WebhookEventType;
254
+ switch (validatedMessage.type) {
255
+ case "task_response":
256
+ eventType = "task_response";
257
+ break;
258
+ case "agent_selected":
259
+ eventType = "agent_selected";
260
+ break;
261
+ case "error":
262
+ eventType = "error";
263
+ break;
264
+ default:
265
+ eventType = "message";
266
+ }
267
+
268
+ const metadata = {
269
+ messageType: validatedMessage.type,
270
+ from: validatedMessage.from,
271
+ to: validatedMessage.to,
272
+ room: validatedMessage.room,
273
+ taskId: validatedMessage.task_id
274
+ };
275
+
276
+ await this.sendWebhook(eventType, validatedMessage, metadata);
277
+ } catch (error) {
278
+ if (error instanceof z.ZodError) {
279
+ this.logger.error("Invalid message for webhook", error);
280
+ throw new ValidationError("Invalid message for webhook", error);
281
+ }
282
+ throw error;
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Process webhook queue
288
+ */
289
+ private async processQueue(): Promise<void> {
290
+ if (this.isProcessing || this.queue.isEmpty() || this.isDestroyed) {
291
+ return;
292
+ }
293
+
294
+ this.isProcessing = true;
295
+
296
+ while (!this.queue.isEmpty() && !this.isDestroyed) {
297
+ const item = this.queue.peek();
298
+ if (!item) break; // Queue empty
299
+
300
+ // Check if we should retry
301
+ if (item.nextRetry && item.nextRetry > new Date()) {
302
+ // Wait until retry time
303
+ const delay = item.nextRetry.getTime() - Date.now();
304
+ this.processTimer = setTimeout(() => {
305
+ this.isProcessing = false;
306
+ this.processQueue();
307
+ }, delay);
308
+ return;
309
+ }
310
+
311
+ try {
312
+ // Use circuit breaker for fault tolerance (CB-3)
313
+ await this.circuitBreaker.execute(async () => {
314
+ await this.deliverWebhook(item);
315
+ });
316
+ // Success - remove from queue
317
+ this.queue.shift();
318
+ } catch (error) {
319
+ // Check if circuit breaker is open
320
+ if (error instanceof CircuitBreakerError) {
321
+ this.logger.warn("Circuit breaker is OPEN, pausing webhook delivery", {
322
+ state: error.state
323
+ });
324
+ // Don't retry immediately when circuit is open
325
+ // Queue will be retried when circuit closes
326
+ this.isProcessing = false;
327
+ return;
328
+ }
329
+ // Handle retry logic
330
+ item.attempts++;
331
+ item.lastAttempt = new Date();
332
+ item.error = error as Error;
333
+
334
+ // Check if we should retry using the retry policy (REL-3)
335
+ if (!this.retryPolicy.shouldRetry(item.attempts)) {
336
+ // Max retries reached
337
+ this.logger.error("Webhook delivery failed after max retries", {
338
+ url: this.webhookConfig?.url,
339
+ attempts: item.attempts,
340
+ error: error
341
+ });
342
+ this.emit("webhook:error", error as Error, this.webhookConfig?.url ?? "");
343
+ this.queue.shift();
344
+ } else {
345
+ // Schedule retry using configured strategy
346
+ const retryDelay = this.retryPolicy.calculateDelay(item.attempts);
347
+ item.nextRetry = new Date(Date.now() + retryDelay);
348
+
349
+ this.logger.warn(`Webhook delivery failed, retrying in ${retryDelay}ms`, {
350
+ url: this.webhookConfig?.url,
351
+ attempt: item.attempts,
352
+ error: error
353
+ });
354
+ this.emit("webhook:retry", item.attempts, this.webhookConfig?.url ?? "");
355
+
356
+ // Move to end of queue
357
+ const failedItem = this.queue.shift();
358
+ if (failedItem) {
359
+ this.queue.push(failedItem);
360
+ }
361
+ }
362
+ }
363
+ }
364
+
365
+ this.isProcessing = false;
366
+ }
367
+
368
+ /**
369
+ * Deliver a webhook
370
+ */
371
+ private async deliverWebhook(item: WebhookQueueItem): Promise<void> {
372
+ if (!this.webhookConfig) {
373
+ throw new Error("Webhook not configured");
374
+ }
375
+
376
+ const controller = new AbortController();
377
+ const timeout = setTimeout(() => {
378
+ controller.abort();
379
+ }, this.webhookConfig.timeout);
380
+
381
+ try {
382
+ this.logger.debug("Delivering webhook", { url: this.webhookConfig.url });
383
+
384
+ const options: RequestInit = {
385
+ method: "POST",
386
+ headers: {
387
+ "Content-Type": "application/json",
388
+ ...this.webhookConfig.headers
389
+ },
390
+ body: JSON.stringify(item.payload),
391
+ signal: controller.signal as any
392
+ };
393
+
394
+ const response = await fetch(this.webhookConfig.url, options);
395
+
396
+ if (!response.ok) {
397
+ throw new WebhookError(`Webhook returned status ${response.status}`, {
398
+ status: response.status,
399
+ statusText: response.statusText
400
+ });
401
+ }
402
+
403
+ const responseData = await response.text();
404
+ this.logger.debug("Webhook delivered successfully", {
405
+ url: this.webhookConfig.url,
406
+ status: response.status
407
+ });
408
+
409
+ this.emit("webhook:sent", item.payload, this.webhookConfig.url);
410
+ this.emit("webhook:success", responseData, this.webhookConfig.url);
411
+ } catch (error: any) {
412
+ if (error.name === "AbortError") {
413
+ throw new WebhookError("Webhook timeout", {
414
+ timeout: this.webhookConfig.timeout
415
+ });
416
+ }
417
+ throw new WebhookError(`Webhook delivery failed: ${error.message}`, error);
418
+ } finally {
419
+ clearTimeout(timeout);
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Validate webhook URL for SSRF vulnerabilities
425
+ * Uses comprehensive SSRF validator to block:
426
+ * - Private IP ranges (RFC1918)
427
+ * - Cloud metadata endpoints (AWS, GCP, Azure)
428
+ * - Kubernetes service discovery
429
+ * - Dangerous internal ports
430
+ * - Localhost (unless allowInsecureWebhooks is enabled)
431
+ */
432
+ private validateWebhookUrl(url: string): void {
433
+ try {
434
+ // Use comprehensive SSRF validator
435
+ // allowInsecureWebhooks controls whether localhost/HTTP is allowed
436
+ validateSSRF(url, this.config.allowInsecureWebhooks ?? false);
437
+ } catch (error: any) {
438
+ throw new WebhookError(`Webhook URL validation failed: ${error.message}`, { url });
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Gets the current status of the webhook delivery queue.
444
+ * Useful for monitoring webhook health and detecting delivery issues.
445
+ *
446
+ * @returns Object containing queue statistics
447
+ * @returns {number} returns.pending - Total number of webhooks in queue (including failed)
448
+ * @returns {boolean} returns.processing - Whether queue is currently being processed
449
+ * @returns {number} returns.failed - Number of webhooks that have failed and are awaiting retry
450
+ *
451
+ * @example
452
+ * ```typescript
453
+ * const status = webhookHandler.getQueueStatus();
454
+ * console.log(`Queue: ${status.pending} pending, ${status.failed} failed`);
455
+ * if (status.failed > 10) {
456
+ * console.warn('High number of failed webhooks detected');
457
+ * }
458
+ * ```
459
+ */
460
+ public getQueueStatus(): {
461
+ pending: number;
462
+ processing: boolean;
463
+ failed: number;
464
+ circuitState: string;
465
+ } {
466
+ const circuitState = this.circuitBreaker.getState();
467
+ return {
468
+ pending: this.queue.size(),
469
+ processing: this.isProcessing,
470
+ failed: this.queue.toArray().filter((item) => item.error).length,
471
+ circuitState: circuitState.state
472
+ };
473
+ }
474
+
475
+ /**
476
+ * Clears all pending and failed webhooks from the delivery queue.
477
+ * Warning: This permanently discards all queued webhooks.
478
+ * Use this to recover from queue issues or when webhooks are no longer relevant.
479
+ *
480
+ * @example
481
+ * ```typescript
482
+ * webhookHandler.clearQueue();
483
+ * console.log('All pending webhooks cleared');
484
+ * ```
485
+ */
486
+ public clearQueue(): void {
487
+ this.queue.clear();
488
+ this.logger.info("Webhook queue cleared");
489
+ }
490
+
491
+ /**
492
+ * Retries all failed webhooks in the queue immediately.
493
+ * Resets attempt counters and error states for failed webhooks.
494
+ * Useful for recovering from temporary network or endpoint issues.
495
+ *
496
+ * @example
497
+ * ```typescript
498
+ * const status = webhookHandler.getQueueStatus();
499
+ * if (status.failed > 0) {
500
+ * webhookHandler.retryFailed();
501
+ * console.log(`Retrying ${status.failed} failed webhooks`);
502
+ * }
503
+ * ```
504
+ */
505
+ public retryFailed(): void {
506
+ const failed = this.queue.toArray().filter((item) => item.error);
507
+ for (const item of failed) {
508
+ item.attempts = 0;
509
+ item.error = undefined;
510
+ item.nextRetry = undefined;
511
+ }
512
+ this.processQueue();
513
+ }
514
+
515
+ /**
516
+ * Destroys the webhook handler and cleans up resources.
517
+ * Stops queue processing, clears all timers, removes event listeners,
518
+ * and discards all pending webhooks. After destruction, the handler cannot be reused.
519
+ *
520
+ * @example
521
+ * ```typescript
522
+ * webhookHandler.destroy();
523
+ * console.log('Webhook handler destroyed');
524
+ * ```
525
+ */
526
+ public destroy(): void {
527
+ this.isDestroyed = true;
528
+ this.isProcessing = false;
529
+ if (this.processTimer) {
530
+ clearTimeout(this.processTimer);
531
+ this.processTimer = undefined;
532
+ }
533
+ this.clearQueue();
534
+ this.removeAllListeners();
535
+ }
536
+
537
+ /**
538
+ * Quick check for whether a webhook URL has been configured.
539
+ * Returns true if configure() has been called with a valid URL.
540
+ *
541
+ * @returns True if webhook is configured, false otherwise
542
+ *
543
+ * @example
544
+ * ```typescript
545
+ * if (webhookHandler.isConfigured) {
546
+ * console.log('Webhooks are enabled');
547
+ * } else {
548
+ * console.log('Webhooks not configured');
549
+ * }
550
+ * ```
551
+ */
552
+ public get isConfigured(): boolean {
553
+ return !!this.webhookConfig;
554
+ }
555
+
556
+ /**
557
+ * Gets the current webhook configuration including URL, headers, and settings.
558
+ * Returns a defensive copy to prevent external modification of internal state.
559
+ * Returns undefined if webhook has not been configured.
560
+ *
561
+ * @returns Copy of current webhook configuration, or undefined if not configured
562
+ *
563
+ * @example
564
+ * ```typescript
565
+ * const config = webhookHandler.getConfig();
566
+ * if (config) {
567
+ * console.log(`Webhook URL: ${config.url}`);
568
+ * console.log(`Max retries: ${config.retries}`);
569
+ * console.log(`Timeout: ${config.timeout}ms`);
570
+ * }
571
+ * ```
572
+ */
573
+ public getConfig(): WebhookConfig | undefined {
574
+ return this.webhookConfig ? { ...this.webhookConfig } : undefined;
575
+ }
576
+ }