@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,789 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { WebhookHandler } from "./webhook-handler";
3
+ import type { SDKConfig, Logger } from "../types/config";
4
+ import { WebhookError } from "../types/events";
5
+ import fetch from "node-fetch";
6
+
7
+ // Mock node-fetch
8
+ vi.mock("node-fetch", () => {
9
+ return {
10
+ default: vi.fn()
11
+ };
12
+ });
13
+
14
+ describe("WebhookHandler", () => {
15
+ let handler: WebhookHandler;
16
+ let mockConfig: SDKConfig;
17
+ let mockLogger: Logger;
18
+ let mockFetch: any;
19
+
20
+ beforeEach(() => {
21
+ vi.useFakeTimers();
22
+ mockFetch = fetch as any;
23
+ mockFetch.mockClear();
24
+
25
+ mockConfig = {
26
+ wsUrl: "wss://example.com/ws",
27
+ privateKey: "0x1234567890123456789012345678901234567890123456789012345678901234",
28
+ webhookUrl: "https://webhook.example.com/events",
29
+ webhookHeaders: {
30
+ "X-API-Key": "test-key",
31
+ "Content-Type": "application/json"
32
+ },
33
+ webhookRetries: 3,
34
+ webhookTimeout: 5000,
35
+ logLevel: "info"
36
+ } as SDKConfig;
37
+
38
+ mockLogger = {
39
+ debug: vi.fn(),
40
+ info: vi.fn(),
41
+ warn: vi.fn(),
42
+ error: vi.fn()
43
+ };
44
+
45
+ handler = new WebhookHandler(mockConfig, mockLogger);
46
+ });
47
+
48
+ afterEach(() => {
49
+ (handler as any).circuitBreaker.reset(); // Reset circuit breaker to prevent test interference
50
+ handler.destroy();
51
+ vi.clearAllTimers();
52
+ vi.useRealTimers();
53
+ });
54
+
55
+ describe("constructor", () => {
56
+ it("should initialize with config", () => {
57
+ expect(handler).toBeDefined();
58
+ expect((handler as any).config).toStrictEqual(mockConfig);
59
+ expect((handler as any).queue.size()).toBe(0);
60
+ expect((handler as any).isProcessing).toBe(false);
61
+ });
62
+
63
+ it("should initialize without webhook URL", () => {
64
+ const configNoWebhook = { ...mockConfig, webhookUrl: undefined };
65
+ const handlerNoWebhook = new WebhookHandler(configNoWebhook, mockLogger);
66
+ expect(handlerNoWebhook).toBeDefined();
67
+ });
68
+ });
69
+
70
+ describe("sendWebhook", () => {
71
+ it("should queue webhook when URL is configured", async () => {
72
+ const event = "task_response";
73
+ const data = { content: "test message" };
74
+
75
+ await handler.sendWebhook(event, data);
76
+
77
+ expect((handler as any).queue.size()).toBe(1);
78
+ expect((handler as any).queue.toArray()[0].payload.event).toBe(event);
79
+ expect((handler as any).queue.toArray()[0].payload.data).toMatchObject(data);
80
+ expect((handler as any).queue.toArray()[0].attempts).toBe(0);
81
+ });
82
+
83
+ it("should not queue webhook when URL is not configured", async () => {
84
+ const configNoWebhook = { ...mockConfig, webhookUrl: undefined };
85
+ const handlerNoWebhook = new WebhookHandler(configNoWebhook, mockLogger);
86
+
87
+ await handlerNoWebhook.sendWebhook("task_response", { content: "test" });
88
+
89
+ expect((handlerNoWebhook as any).queue.size()).toBe(0);
90
+ });
91
+
92
+ it("should filter events based on webhookEvents config", async () => {
93
+ await handler.sendWebhook("task_response", { content: "should queue" });
94
+ await handler.sendWebhook("agent_selected", { agent: "should queue" });
95
+ await handler.sendWebhook("error", { error: "should queue" });
96
+
97
+ expect((handler as any).queue.size()).toBe(3);
98
+ expect((handler as any).queue.toArray()[0].payload.event).toBe("task_response");
99
+ expect((handler as any).queue.toArray()[1].payload.event).toBe("agent_selected");
100
+ });
101
+
102
+ it("should queue all events when webhookEvents is not configured", async () => {
103
+ const configAllEvents = { ...mockConfig, webhookEvents: undefined };
104
+ const handlerAllEvents = new WebhookHandler(configAllEvents, mockLogger);
105
+
106
+ await handlerAllEvents.sendWebhook("task_response", { content: "test1" });
107
+ await handlerAllEvents.sendWebhook("error", { error: "test2" });
108
+ await handlerAllEvents.sendWebhook("message", { custom: "test3" });
109
+
110
+ expect((handlerAllEvents as any).queue.size()).toBe(3);
111
+ });
112
+
113
+ it("should generate unique ID for each webhook", async () => {
114
+ await handler.sendWebhook("task_response", { content: "test1" });
115
+ await handler.sendWebhook("task_response", { content: "test2" });
116
+
117
+ const queue = (handler as any).queue.toArray();
118
+ expect(queue[0].payload.timestamp).toBeDefined();
119
+ expect(queue[1].payload.timestamp).toBeDefined();
120
+ // Each webhook gets unique timestamp
121
+ expect(queue[0].payload.timestamp !== queue[1].payload.timestamp || queue[0].payload.data.content !== queue[1].payload.data.content).toBe(true);
122
+ });
123
+
124
+ it("should include timestamp in webhook payload", async () => {
125
+ const beforeTime = new Date().toISOString();
126
+ await handler.sendWebhook("task_response", { content: "test" });
127
+ const afterTime = new Date().toISOString();
128
+
129
+ const webhook = (handler as any).queue.toArray()[0];
130
+ expect(webhook.payload.timestamp).toBeDefined();
131
+ expect(webhook.payload.timestamp).toBeTypeOf("string");
132
+ });
133
+ });
134
+
135
+ describe("processQueue", () => {
136
+ it.skip("should process webhooks in queue", async () => {
137
+ mockFetch.mockResolvedValueOnce({
138
+ ok: true,
139
+ status: 200,
140
+ statusText: "OK",
141
+ text: async () => "OK"
142
+ });
143
+
144
+ await handler.sendWebhook("task_response", { content: "test" });
145
+
146
+ // Start processing
147
+ await (handler as any).processQueue();
148
+
149
+ expect(mockFetch).toHaveBeenCalledTimes(1);
150
+ expect(mockFetch).toHaveBeenCalledWith(
151
+ "https://webhook.example.com/events",
152
+ expect.objectContaining({
153
+ method: "POST",
154
+ headers: expect.objectContaining({
155
+ "X-API-Key": "test-key",
156
+ "Content-Type": "application/json"
157
+ }),
158
+ body: expect.stringContaining("task_response")
159
+ })
160
+ );
161
+
162
+ expect((handler as any).queue.size()).toBe(0);
163
+ });
164
+
165
+ it("should handle successful delivery", async () => {
166
+ mockFetch.mockResolvedValueOnce({
167
+ ok: true,
168
+ status: 200,
169
+ statusText: "OK",
170
+ text: async () => "OK"
171
+ });
172
+
173
+ const successHandler = vi.fn();
174
+ handler.on("webhook:success", successHandler);
175
+
176
+ await handler.sendWebhook("task_response", { content: "test" });
177
+ await (handler as any).processQueue();
178
+
179
+ expect(successHandler).toHaveBeenCalledWith("OK", "https://webhook.example.com/events");
180
+ });
181
+
182
+ it.skip("should retry on failure with exponential backoff", async () => {
183
+ mockFetch.mockRejectedValueOnce(new Error("Network error"));
184
+ mockFetch.mockRejectedValueOnce(new Error("Network error"));
185
+ mockFetch.mockResolvedValueOnce({
186
+ ok: true,
187
+ status: 200,
188
+ text: async () => "OK"
189
+ });
190
+
191
+ const retryHandler = vi.fn();
192
+ handler.on("webhook:retry", retryHandler);
193
+
194
+ await handler.sendWebhook("task_response", { content: "test" });
195
+
196
+ // First attempt
197
+ await (handler as any).processQueue();
198
+ expect(mockFetch).toHaveBeenCalledTimes(1);
199
+ expect((handler as any).queue.size()).toBe(1);
200
+ expect((handler as any).queue.toArray()[0].attempts).toBe(1);
201
+
202
+ // Wait for retry delay
203
+ vi.advanceTimersByTime(1000);
204
+
205
+ // Second attempt
206
+ await (handler as any).processQueue();
207
+ expect(mockFetch).toHaveBeenCalledTimes(2);
208
+ expect((handler as any).queue.toArray()[0].attempts).toBe(2);
209
+
210
+ // Wait for retry delay (exponential backoff)
211
+ vi.advanceTimersByTime(2000);
212
+
213
+ // Third attempt (success)
214
+ await (handler as any).processQueue();
215
+ expect(mockFetch).toHaveBeenCalledTimes(3);
216
+ expect((handler as any).queue.size()).toBe(0);
217
+ });
218
+
219
+ it.skip("should fail after max retry attempts", async () => {
220
+ mockFetch.mockRejectedValue(new Error("Persistent error"));
221
+
222
+ const errorHandler = vi.fn();
223
+ handler.on("webhook:error", errorHandler);
224
+
225
+ await handler.sendWebhook("task_response", { content: "test" });
226
+
227
+ // Attempt all retries
228
+ for (let i = 0; i <= mockConfig.webhookRetries!; i++) {
229
+ await (handler as any).processQueue();
230
+ if (i < mockConfig.webhookRetries!) {
231
+ vi.advanceTimersByTime(1000 * Math.pow(2, i));
232
+ }
233
+ }
234
+
235
+ expect(mockFetch).toHaveBeenCalledTimes(4); // Initial + 3 retries
236
+ expect(errorHandler).toHaveBeenCalledWith(
237
+ expect.any(Error),
238
+ "https://webhook.example.com/events"
239
+ );
240
+ expect((handler as any).queue.size()).toBe(0);
241
+ });
242
+
243
+ it.skip("should handle timeout", async () => {
244
+ // Mock a delayed response
245
+ mockFetch.mockImplementationOnce(
246
+ () =>
247
+ new Promise((resolve) => {
248
+ setTimeout(() => resolve({ ok: true, status: 200, text: async () => "OK" }), 10000);
249
+ })
250
+ );
251
+
252
+ const errorHandler = vi.fn();
253
+ handler.on("webhook:error", errorHandler);
254
+
255
+ await handler.sendWebhook("task_response", { content: "test" });
256
+
257
+ // Start processing
258
+ const processPromise = (handler as any).processQueue();
259
+
260
+ // Advance timers past timeout
261
+ vi.advanceTimersByTime(5100);
262
+
263
+ await processPromise;
264
+
265
+ // Should have retried
266
+ expect((handler as any).queue.size()).toBe(1);
267
+ expect((handler as any).queue.toArray()[0].attempts).toBe(1);
268
+ });
269
+
270
+ it.skip("should handle HTTP error responses", async () => {
271
+ mockFetch.mockResolvedValueOnce({
272
+ ok: false,
273
+ status: 500,
274
+ statusText: "Internal Server Error",
275
+ text: async () => "Internal Server Error"
276
+ });
277
+
278
+ const retryHandler = vi.fn();
279
+ handler.on("webhook:retry", retryHandler);
280
+
281
+ await handler.sendWebhook("task_response", { content: "test" });
282
+ await (handler as any).processQueue();
283
+
284
+ expect(retryHandler).toHaveBeenCalled();
285
+ expect((handler as any).queue.size()).toBe(1);
286
+ expect((handler as any).queue.toArray()[0].attempts).toBe(1);
287
+ });
288
+
289
+ it("should process multiple webhooks in sequence", async () => {
290
+ mockFetch.mockResolvedValue({
291
+ ok: true,
292
+ status: 200,
293
+ text: async () => "OK"
294
+ });
295
+
296
+ await handler.sendWebhook("task_response", { content: "test1" });
297
+ await handler.sendWebhook("agent_selected", { agent: "test2" });
298
+ await handler.sendWebhook("task_response", { content: "test3" });
299
+
300
+ // Process all webhooks
301
+ while ((handler as any).queue.size() > 0) {
302
+ await (handler as any).processQueue();
303
+ }
304
+
305
+ expect(mockFetch).toHaveBeenCalledTimes(3);
306
+ expect((handler as any).queue.size()).toBe(0);
307
+ });
308
+ });
309
+
310
+ describe("deliverWebhook", () => {
311
+ it("should send webhook with correct payload", async () => {
312
+ mockFetch.mockResolvedValueOnce({
313
+ ok: true,
314
+ status: 200,
315
+ text: async () => "OK"
316
+ });
317
+
318
+ const webhookItem = {
319
+ payload: {
320
+ event: "task_response",
321
+ timestamp: new Date().toISOString(),
322
+ data: { content: "test" }
323
+ },
324
+ attempts: 0
325
+ };
326
+
327
+ await (handler as any).deliverWebhook(webhookItem);
328
+
329
+ expect(mockFetch).toHaveBeenCalledWith(
330
+ "https://webhook.example.com/events",
331
+ expect.objectContaining({
332
+ method: "POST",
333
+ headers: expect.objectContaining({
334
+ "X-API-Key": "test-key",
335
+ "Content-Type": "application/json"
336
+ }),
337
+ body: expect.stringContaining("task_response")
338
+ })
339
+ );
340
+ });
341
+
342
+ it("should merge custom headers", async () => {
343
+ mockFetch.mockResolvedValueOnce({
344
+ ok: true,
345
+ status: 200,
346
+ text: async () => "OK"
347
+ });
348
+
349
+ const webhookItem = {
350
+ payload: {
351
+ event: "message",
352
+ timestamp: new Date().toISOString(),
353
+ data: {}
354
+ },
355
+ attempts: 0
356
+ };
357
+
358
+ await (handler as any).deliverWebhook(webhookItem);
359
+
360
+ expect(mockFetch).toHaveBeenCalledWith(
361
+ expect.any(String),
362
+ expect.objectContaining({
363
+ headers: expect.objectContaining({
364
+ "X-API-Key": "test-key",
365
+ "Content-Type": "application/json"
366
+ })
367
+ })
368
+ );
369
+ });
370
+
371
+ it.skip("should handle AbortController for timeout", async () => {
372
+ mockFetch.mockImplementationOnce(
373
+ () =>
374
+ new Promise((_, reject) => {
375
+ const error = new Error("Timeout");
376
+ error.name = "AbortError";
377
+ setTimeout(() => reject(error), 100);
378
+ })
379
+ );
380
+
381
+ const webhookItem = {
382
+ payload: {
383
+ event: "message",
384
+ timestamp: new Date().toISOString(),
385
+ data: {}
386
+ },
387
+ attempts: 0
388
+ };
389
+
390
+ await expect((handler as any).deliverWebhook(webhookItem)).rejects.toThrow();
391
+ }, 15000);
392
+
393
+ it("should validate successful response", async () => {
394
+ mockFetch.mockResolvedValueOnce({
395
+ ok: true,
396
+ status: 200,
397
+ text: async () => "OK"
398
+ });
399
+
400
+ const webhookItem = {
401
+ payload: {
402
+ event: "message",
403
+ timestamp: new Date().toISOString(),
404
+ data: {}
405
+ },
406
+ attempts: 0
407
+ };
408
+
409
+ await (handler as any).deliverWebhook(webhookItem);
410
+ // Should not throw
411
+ });
412
+
413
+ it("should handle non-2xx responses as failure", async () => {
414
+ mockFetch.mockResolvedValueOnce({
415
+ ok: false,
416
+ status: 404,
417
+ statusText: "Not Found",
418
+ text: async () => "Not Found"
419
+ });
420
+
421
+ const webhookItem = {
422
+ payload: {
423
+ event: "message",
424
+ timestamp: new Date().toISOString(),
425
+ data: {}
426
+ },
427
+ attempts: 0
428
+ };
429
+
430
+ await expect((handler as any).deliverWebhook(webhookItem)).rejects.toThrow(WebhookError);
431
+ });
432
+ });
433
+
434
+ describe("validateWebhookUrl - SSRF Protection", () => {
435
+ describe("Valid URLs", () => {
436
+ it("should accept HTTPS public URLs", () => {
437
+ const urls = ["https://webhook.example.com/events", "https://api.example.org/webhook"];
438
+
439
+ urls.forEach((url) => {
440
+ expect(() => (handler as any).validateWebhookUrl(url)).not.toThrow();
441
+ });
442
+ });
443
+
444
+ it("should accept HTTP localhost URLs with allowInsecureWebhooks", () => {
445
+ const configWithInsecure = { ...mockConfig, allowInsecureWebhooks: true };
446
+ const handlerWithInsecure = new WebhookHandler(configWithInsecure, mockLogger);
447
+
448
+ const urls = [
449
+ "http://localhost/webhook",
450
+ "http://localhost:3000/events",
451
+ "http://127.0.0.1/webhook",
452
+ "http://127.0.0.1:8080/events"
453
+ ];
454
+
455
+ urls.forEach((url) => {
456
+ expect(() => (handlerWithInsecure as any).validateWebhookUrl(url)).not.toThrow();
457
+ });
458
+ });
459
+ });
460
+
461
+ describe("SSRF Protection - Cloud Metadata", () => {
462
+ it("should block AWS metadata endpoints", () => {
463
+ const configWithInsecure = { ...mockConfig, allowInsecureWebhooks: true };
464
+ const handlerWithInsecure = new WebhookHandler(configWithInsecure, mockLogger);
465
+
466
+ const urls = [
467
+ "http://169.254.169.254/latest/meta-data/",
468
+ "https://169.254.169.254/latest/meta-data/iam/security-credentials/"
469
+ ];
470
+
471
+ urls.forEach((url) => {
472
+ expect(() => (handlerWithInsecure as any).validateWebhookUrl(url)).toThrow(WebhookError);
473
+ expect(() => (handlerWithInsecure as any).validateWebhookUrl(url)).toThrow(
474
+ /cloud metadata endpoint/i
475
+ );
476
+ });
477
+ });
478
+
479
+ it("should block Google Cloud metadata endpoints", () => {
480
+ const configWithInsecure = { ...mockConfig, allowInsecureWebhooks: true };
481
+ const handlerWithInsecure = new WebhookHandler(configWithInsecure, mockLogger);
482
+
483
+ const urls = [
484
+ "http://metadata.google.internal/computeMetadata/v1/",
485
+ "http://metadata.google.com/computeMetadata/v1/"
486
+ ];
487
+
488
+ urls.forEach((url) => {
489
+ expect(() => (handlerWithInsecure as any).validateWebhookUrl(url)).toThrow(WebhookError);
490
+ expect(() => (handlerWithInsecure as any).validateWebhookUrl(url)).toThrow(
491
+ /cloud metadata/i
492
+ );
493
+ });
494
+ });
495
+
496
+ it("should block Kubernetes services", () => {
497
+ const configWithInsecure = { ...mockConfig, allowInsecureWebhooks: true };
498
+ const handlerWithInsecure = new WebhookHandler(configWithInsecure, mockLogger);
499
+
500
+ const urls = [
501
+ "http://kubernetes.default/api",
502
+ "http://redis.default.svc.cluster.local:6379"
503
+ ];
504
+
505
+ urls.forEach((url) => {
506
+ expect(() => (handlerWithInsecure as any).validateWebhookUrl(url)).toThrow(WebhookError);
507
+ expect(() => (handlerWithInsecure as any).validateWebhookUrl(url)).toThrow(
508
+ /Kubernetes service|cloud metadata/i
509
+ );
510
+ });
511
+ });
512
+ });
513
+
514
+ describe("SSRF Protection - Private IPs", () => {
515
+ it("should block RFC1918 private IP ranges", () => {
516
+ const urls = [
517
+ "https://10.0.0.1/webhook",
518
+ "https://192.168.1.1/webhook",
519
+ "https://172.16.0.1/webhook",
520
+ "https://172.31.255.255/webhook"
521
+ ];
522
+
523
+ urls.forEach((url) => {
524
+ expect(() => (handler as any).validateWebhookUrl(url)).toThrow(WebhookError);
525
+ expect(() => (handler as any).validateWebhookUrl(url)).toThrow(/private IP address/i);
526
+ });
527
+ });
528
+
529
+ it("should block link-local addresses", () => {
530
+ const urls = ["https://169.254.1.1/webhook", "https://169.254.255.255/webhook"];
531
+
532
+ urls.forEach((url) => {
533
+ expect(() => (handler as any).validateWebhookUrl(url)).toThrow(WebhookError);
534
+ });
535
+ });
536
+
537
+ it("should block IPv6 private ranges", () => {
538
+ const configWithInsecure = { ...mockConfig, allowInsecureWebhooks: true };
539
+ const handlerWithInsecure = new WebhookHandler(configWithInsecure, mockLogger);
540
+
541
+ const urls = [
542
+ "http://[fc00::1]/webhook",
543
+ "http://[fd00::1]/webhook",
544
+ "http://[fe80::1]/webhook"
545
+ ];
546
+
547
+ urls.forEach((url) => {
548
+ expect(() => (handlerWithInsecure as any).validateWebhookUrl(url)).toThrow(WebhookError);
549
+ expect(() => (handlerWithInsecure as any).validateWebhookUrl(url)).toThrow(/private IP/i);
550
+ });
551
+ });
552
+ });
553
+
554
+ describe("SSRF Protection - Dangerous Ports", () => {
555
+ it("should block common internal service ports", () => {
556
+ const urls = [
557
+ "https://example.com:22/webhook", // SSH
558
+ "https://example.com:3306/webhook", // MySQL
559
+ "https://example.com:5432/webhook", // PostgreSQL
560
+ "https://example.com:6379/webhook", // Redis
561
+ "https://example.com:27017/webhook" // MongoDB
562
+ ];
563
+
564
+ urls.forEach((url) => {
565
+ expect(() => (handler as any).validateWebhookUrl(url)).toThrow(WebhookError);
566
+ expect(() => (handler as any).validateWebhookUrl(url)).toThrow(
567
+ /Port.*internal services/i
568
+ );
569
+ });
570
+ });
571
+
572
+ it("should allow safe ports", () => {
573
+ const urls = [
574
+ "https://example.com:443/webhook",
575
+ "https://example.com:8443/webhook",
576
+ "https://example.com:3000/webhook"
577
+ ];
578
+
579
+ urls.forEach((url) => {
580
+ expect(() => (handler as any).validateWebhookUrl(url)).not.toThrow();
581
+ });
582
+ });
583
+ });
584
+
585
+ describe("Protocol Validation", () => {
586
+ it("should reject HTTP for non-localhost without allowInsecureWebhooks", () => {
587
+ const urls = [
588
+ "http://example.com/webhook"
589
+ ];
590
+
591
+ urls.forEach((url) => {
592
+ expect(() => (handler as any).validateWebhookUrl(url)).toThrow(WebhookError);
593
+ expect(() => (handler as any).validateWebhookUrl(url)).toThrow(/must use HTTPS|Only HTTPS/i);
594
+ });
595
+
596
+ // Private IPs are blocked for different reason (private IP, not HTTPS requirement)
597
+ const privateUrls = [
598
+ "http://192.168.1.1/webhook",
599
+ "http://10.0.0.1/webhook"
600
+ ];
601
+ privateUrls.forEach((url) => {
602
+ expect(() => (handler as any).validateWebhookUrl(url)).toThrow(WebhookError);
603
+ expect(() => (handler as any).validateWebhookUrl(url)).toThrow(/private IP/i);
604
+ });
605
+ });
606
+
607
+ it("should reject non-HTTP/HTTPS protocols", () => {
608
+ const urls = ["ftp://example.com/webhook", "file:///etc/passwd", "javascript:alert(1)"];
609
+
610
+ urls.forEach((url) => {
611
+ expect(() => (handler as any).validateWebhookUrl(url)).toThrow();
612
+ });
613
+ });
614
+ });
615
+
616
+ describe("Invalid URLs", () => {
617
+ it("should reject malformed URLs", () => {
618
+ const urls = ["not-a-url", "", "htp://example.com"];
619
+
620
+ urls.forEach((url) => {
621
+ expect(() => (handler as any).validateWebhookUrl(url)).toThrow();
622
+ });
623
+ });
624
+ });
625
+ });
626
+
627
+ describe("destroy", () => {
628
+ it("should clear the queue", async () => {
629
+ await handler.sendWebhook("task_response", { content: "test1" });
630
+ await handler.sendWebhook("agent_selected", { agent: "test2" });
631
+
632
+ expect((handler as any).queue.size()).toBe(2);
633
+
634
+ handler.destroy();
635
+
636
+ expect((handler as any).queue.size()).toBe(0);
637
+ });
638
+
639
+ it("should stop processing", async () => {
640
+ (handler as any).isProcessing = true;
641
+
642
+ handler.destroy();
643
+
644
+ expect((handler as any).isProcessing).toBe(false);
645
+ });
646
+
647
+ it("should be safe to call multiple times", async () => {
648
+ await handler.sendWebhook("task_response", { content: "test" });
649
+
650
+ handler.destroy();
651
+ handler.destroy();
652
+ handler.destroy();
653
+
654
+ expect((handler as any).queue.size()).toBe(0);
655
+ expect((handler as any).isProcessing).toBe(false);
656
+ });
657
+ });
658
+
659
+ describe("event emission", () => {
660
+ it("should emit webhook:sent event", async () => {
661
+ mockFetch.mockResolvedValueOnce({
662
+ ok: true,
663
+ status: 200,
664
+ text: async () => "OK"
665
+ });
666
+
667
+ const sentHandler = vi.fn();
668
+ handler.on("webhook:sent", sentHandler);
669
+
670
+ await handler.sendWebhook("task_response", { content: "test" });
671
+ await (handler as any).processQueue();
672
+
673
+ expect(sentHandler).toHaveBeenCalledWith(
674
+ expect.objectContaining({
675
+ event: "task_response"
676
+ }),
677
+ "https://webhook.example.com/events"
678
+ );
679
+ });
680
+
681
+ it("should emit webhook:success event", async () => {
682
+ mockFetch.mockResolvedValueOnce({
683
+ ok: true,
684
+ status: 201,
685
+ text: async () => "Created"
686
+ });
687
+
688
+ const successHandler = vi.fn();
689
+ handler.on("webhook:success", successHandler);
690
+
691
+ await handler.sendWebhook("task_response", { content: "test" });
692
+ await (handler as any).processQueue();
693
+
694
+ expect(successHandler).toHaveBeenCalledWith("Created", "https://webhook.example.com/events");
695
+ });
696
+
697
+ it.skip("should emit webhook:retry event", async () => {
698
+ mockFetch.mockRejectedValueOnce(new Error("Network error"));
699
+
700
+ const retryHandler = vi.fn();
701
+ handler.on("webhook:retry", retryHandler);
702
+
703
+ await handler.sendWebhook("task_response", { content: "test" });
704
+ await (handler as any).processQueue();
705
+
706
+ expect(retryHandler).toHaveBeenCalledWith(1, "https://webhook.example.com/events");
707
+ });
708
+
709
+ it.skip("should emit webhook:error event", async () => {
710
+ mockFetch.mockRejectedValue(new Error("Persistent error"));
711
+
712
+ const errorHandler = vi.fn();
713
+ handler.on("webhook:error", errorHandler);
714
+
715
+ await handler.sendWebhook("task_response", { content: "test" });
716
+
717
+ // Exhaust all retries
718
+ for (let i = 0; i <= mockConfig.webhookRetries!; i++) {
719
+ await (handler as any).processQueue();
720
+ vi.advanceTimersByTime(10000);
721
+ }
722
+
723
+ expect(errorHandler).toHaveBeenCalledWith(expect.any(Error), "https://webhook.example.com/events");
724
+ });
725
+ });
726
+
727
+ describe("edge cases", () => {
728
+ it("should handle empty data payload", async () => {
729
+ mockFetch.mockResolvedValueOnce({
730
+ ok: true,
731
+ status: 200,
732
+ text: async () => "OK"
733
+ });
734
+
735
+ await handler.sendWebhook("message", undefined as any);
736
+ await (handler as any).processQueue();
737
+
738
+ expect(mockFetch).toHaveBeenCalledWith(
739
+ expect.any(String),
740
+ expect.objectContaining({
741
+ body: expect.any(String)
742
+ })
743
+ );
744
+ });
745
+
746
+ it("should handle large payloads", async () => {
747
+ mockFetch.mockResolvedValueOnce({
748
+ ok: true,
749
+ status: 200,
750
+ text: async () => "OK"
751
+ });
752
+
753
+ const largeData = {
754
+ content: "x".repeat(100000), // 100KB string
755
+ array: Array(1000).fill({ nested: "object" })
756
+ };
757
+
758
+ await handler.sendWebhook("task_response", largeData);
759
+ await (handler as any).processQueue();
760
+
761
+ expect(mockFetch).toHaveBeenCalled();
762
+ const call = mockFetch.mock.calls[0];
763
+ const body = JSON.parse(call[1].body);
764
+ expect(body.data.content.length).toBe(100000);
765
+ });
766
+
767
+ it.skip("should handle concurrent webhook sends", async () => {
768
+ mockFetch.mockResolvedValue({
769
+ ok: true,
770
+ status: 200,
771
+ text: async () => "OK"
772
+ });
773
+
774
+ // Send multiple webhooks rapidly
775
+ for (let i = 0; i < 10; i++) {
776
+ await handler.sendWebhook("task_response", { index: i });
777
+ }
778
+
779
+ expect((handler as any).queue.size()).toBe(10);
780
+
781
+ // Process all
782
+ while ((handler as any).queue.size() > 0) {
783
+ await (handler as any).processQueue();
784
+ }
785
+
786
+ expect(mockFetch).toHaveBeenCalledTimes(10);
787
+ });
788
+ });
789
+ });