@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,1056 @@
1
+ /**
2
+ * WebSocket client implementation for Teneo Protocol SDK
3
+ * Handles connection, authentication, and message management with Zod validation
4
+ */
5
+
6
+ import WebSocket from "ws";
7
+ import { EventEmitter } from "eventemitter3";
8
+ import { privateKeyToAccount, type PrivateKeyAccount } from "viem/accounts";
9
+ import { v4 as uuidv4 } from "uuid";
10
+ import { z } from "zod";
11
+ import {
12
+ SDKConfig,
13
+ SDKConfigSchema,
14
+ ConnectionState,
15
+ ConnectionStateSchema,
16
+ AuthenticationState,
17
+ AuthenticationStateSchema,
18
+ BaseMessage,
19
+ BaseMessageSchema,
20
+ createRequestChallenge,
21
+ createCheckCachedAuth,
22
+ createPing,
23
+ safeParseMessage,
24
+ Logger
25
+ } from "../types";
26
+ import {
27
+ SDKEvents,
28
+ ConnectionError,
29
+ AuthenticationError,
30
+ TimeoutError,
31
+ ValidationError,
32
+ MessageError,
33
+ SignatureVerificationError
34
+ } from "../types/events";
35
+ import { TIMEOUTS, RETRY, LIMITS } from "../constants";
36
+ import { MessageHandlerRegistry } from "../handlers/message-handler-registry";
37
+ import { getDefaultHandlers } from "../handlers/message-handlers";
38
+ import { HandlerContext } from "../handlers/message-handlers/types";
39
+ import { createPinoLogger } from "../utils/logger";
40
+ import { TokenBucketRateLimiter } from "../utils/rate-limiter";
41
+ import { SignatureVerifier } from "../utils/signature-verifier";
42
+ import { SecurePrivateKey } from "../utils/secure-private-key";
43
+ import { RetryPolicy } from "../utils/retry-policy";
44
+ import { DeduplicationCache } from "../utils/deduplication-cache";
45
+
46
+ export class WebSocketClient extends EventEmitter<SDKEvents> {
47
+ private ws?: WebSocket;
48
+ private config: SDKConfig;
49
+ private logger: Logger;
50
+ private account?: PrivateKeyAccount;
51
+ private secureKey?: SecurePrivateKey;
52
+ private ownsSecureKey: boolean = false; // Track if we created the SecurePrivateKey
53
+ private handlerRegistry: MessageHandlerRegistry;
54
+ private rateLimiter?: TokenBucketRateLimiter;
55
+ private signatureVerifier?: SignatureVerifier;
56
+ private deduplicationCache?: DeduplicationCache;
57
+ private reconnectPolicy: RetryPolicy;
58
+ private roomManager?: any; // Reference to RoomManager for handler context
59
+
60
+ private connectionState: ConnectionState = {
61
+ connected: false,
62
+ authenticated: false,
63
+ reconnecting: false,
64
+ reconnectAttempts: 0
65
+ };
66
+
67
+ private authState: AuthenticationState = {
68
+ authenticated: false
69
+ };
70
+
71
+ private messageQueue: BaseMessage[] = [];
72
+ private pendingMessages = new Map<
73
+ string,
74
+ {
75
+ resolve: (value: any) => void;
76
+ reject: (error: Error) => void;
77
+ timeout: NodeJS.Timeout;
78
+ }
79
+ >();
80
+
81
+ private reconnectTimer?: NodeJS.Timeout;
82
+ private pingTimer?: NodeJS.Timeout;
83
+ private connectionTimer?: NodeJS.Timeout;
84
+
85
+ constructor(config: SDKConfig) {
86
+ super();
87
+ // Validate configuration with Zod
88
+ this.config = SDKConfigSchema.parse(config);
89
+ this.logger = this.config.logger || this.createDefaultLogger();
90
+
91
+ // Initialize message handler registry
92
+ this.handlerRegistry = new MessageHandlerRegistry(this.logger);
93
+
94
+ if (config.privateKey) {
95
+ try {
96
+ // Check if privateKey is already a SecurePrivateKey instance (SEC-3)
97
+ if (typeof config.privateKey === 'object' && 'use' in config.privateKey) {
98
+ // Use the provided SecurePrivateKey directly
99
+ this.secureKey = config.privateKey;
100
+ this.ownsSecureKey = false; // User provided it, we don't own it
101
+
102
+ // Create account using the secure key
103
+ this.account = this.secureKey.use((key) =>
104
+ privateKeyToAccount(key as `0x${string}`)
105
+ );
106
+ } else {
107
+ // privateKey is a plain string - encrypt it immediately
108
+ const privateKeyString = config.privateKey as string;
109
+
110
+ // Ensure the private key starts with 0x
111
+ const privateKey = privateKeyString.startsWith("0x")
112
+ ? (privateKeyString as `0x${string}`)
113
+ : (`0x${privateKeyString}` as `0x${string}`);
114
+
115
+ // Encrypt the private key immediately (SEC-3)
116
+ this.secureKey = new SecurePrivateKey(privateKey);
117
+ this.ownsSecureKey = true; // We created it, we own it
118
+
119
+ // Create account using the secure key
120
+ this.account = this.secureKey.use((key) =>
121
+ privateKeyToAccount(key as `0x${string}`)
122
+ );
123
+ }
124
+
125
+ if (
126
+ config.walletAddress &&
127
+ this.account.address.toLowerCase() !== config.walletAddress.toLowerCase()
128
+ ) {
129
+ throw new Error("Private key does not match provided wallet address");
130
+ }
131
+
132
+ // Remove plaintext private key from config to prevent exposure
133
+ // Note: We modify a copy to avoid mutating the original config object
134
+ this.config = { ...config, privateKey: undefined };
135
+ } catch (error) {
136
+ // Clean up secure key if initialization fails (only if we created it)
137
+ if (this.secureKey && this.ownsSecureKey) {
138
+ this.secureKey.destroy();
139
+ this.secureKey = undefined;
140
+ }
141
+ throw new AuthenticationError("Invalid private key", error);
142
+ }
143
+ }
144
+
145
+ // Register all default message handlers
146
+ this.handlerRegistry.registerAll(getDefaultHandlers(config.clientType || "user"));
147
+
148
+ // Initialize rate limiter if configured (CB-2)
149
+ if (this.config.maxMessagesPerSecond) {
150
+ // Burst capacity = 2x rate (allows temporary spikes)
151
+ const burstCapacity = this.config.maxMessagesPerSecond * 2;
152
+ this.rateLimiter = new TokenBucketRateLimiter(
153
+ this.config.maxMessagesPerSecond,
154
+ burstCapacity
155
+ );
156
+ this.logger.info("Rate limiter initialized", {
157
+ rate: this.config.maxMessagesPerSecond,
158
+ burst: burstCapacity
159
+ });
160
+ }
161
+
162
+ // Initialize signature verifier if configured (SEC-2)
163
+ if (this.config.validateSignatures) {
164
+ this.signatureVerifier = new SignatureVerifier({
165
+ trustedAddresses: this.config.trustedAgentAddresses as any[],
166
+ requireSignaturesFor: this.config.requireSignaturesFor,
167
+ strictMode: this.config.strictSignatureValidation
168
+ });
169
+ this.logger.info("Signature verifier initialized", {
170
+ strictMode: this.config.strictSignatureValidation,
171
+ trustedAddressCount: this.config.trustedAgentAddresses?.length || 0,
172
+ requiredTypes: this.config.requireSignaturesFor?.length || 0
173
+ });
174
+ }
175
+
176
+ // Initialize reconnection retry policy (REL-3)
177
+ if (this.config.reconnectStrategy) {
178
+ // User provided custom strategy
179
+ this.reconnectPolicy = new RetryPolicy(this.config.reconnectStrategy);
180
+ this.logger.info("Custom reconnection strategy configured", {
181
+ type: this.config.reconnectStrategy.type,
182
+ baseDelay: this.config.reconnectStrategy.baseDelay,
183
+ maxDelay: this.config.reconnectStrategy.maxDelay,
184
+ maxAttempts: this.config.reconnectStrategy.maxAttempts
185
+ });
186
+ } else {
187
+ // Use default exponential backoff matching previous hardcoded behavior
188
+ this.reconnectPolicy = RetryPolicy.exponential(
189
+ this.config.reconnectDelay || 5000,
190
+ RETRY.MAX_RECONNECT_DELAY,
191
+ this.config.maxReconnectAttempts || 10,
192
+ true // jitter enabled by default
193
+ );
194
+ }
195
+
196
+ // Initialize message deduplication cache if configured (CB-4)
197
+ if (this.config.enableMessageDeduplication !== false) {
198
+ // Default to enabled if not explicitly disabled
199
+ const ttl = this.config.messageDedupeTtl || 60000; // 1 minute default
200
+ const maxSize = this.config.messageDedupMaxSize || 10000; // 10k default
201
+ this.deduplicationCache = new DeduplicationCache(ttl, maxSize);
202
+ this.logger.info("Message deduplication enabled", {
203
+ ttl,
204
+ maxSize
205
+ });
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Set the RoomManager instance for handler context
211
+ * Called by TeneoSDK after initialization
212
+ */
213
+ public setRoomManager(roomManager: any): void {
214
+ this.roomManager = roomManager;
215
+ }
216
+
217
+ /**
218
+ * Establishes a WebSocket connection to the Teneo server.
219
+ * Handles connection timeout, authentication challenge-response flow,
220
+ * and automatic message queue processing after successful connection.
221
+ * Emits 'connection:open', 'auth:challenge', 'auth:success', and 'ready' events.
222
+ *
223
+ * @returns Promise that resolves when connection and authentication are complete
224
+ * @throws {TimeoutError} If connection times out (default: 30 seconds)
225
+ * @throws {ConnectionError} If WebSocket connection fails
226
+ * @throws {AuthenticationError} If authentication fails or times out
227
+ *
228
+ * @example
229
+ * ```typescript
230
+ * const wsClient = new WebSocketClient(config);
231
+ * await wsClient.connect();
232
+ * console.log('Connected and authenticated');
233
+ * ```
234
+ */
235
+ public async connect(): Promise<void> {
236
+ return new Promise((resolve, reject) => {
237
+ this.logger.info("Connecting to WebSocket server", {
238
+ url: this.config.wsUrl
239
+ });
240
+
241
+ // Clear any existing connection
242
+ this.disconnect();
243
+
244
+ // Build connection URL with webhook parameter
245
+ let url = this.config.wsUrl;
246
+ if (this.config.webhookUrl) {
247
+ const separator = url.includes("?") ? "&" : "?";
248
+ url += `${separator}webhookUrl=${encodeURIComponent(this.config.webhookUrl)}`;
249
+ }
250
+
251
+ // Create WebSocket connection
252
+ this.ws = new WebSocket(url, {
253
+ headers: this.config.webhookHeaders,
254
+ handshakeTimeout: this.config.connectionTimeout || TIMEOUTS.CONNECTION_TIMEOUT,
255
+ maxPayload: this.config.maxMessageSize || LIMITS.MAX_MESSAGE_SIZE
256
+ });
257
+
258
+ // Set connection timeout
259
+ this.connectionTimer = setTimeout(() => {
260
+ this.ws?.close();
261
+ reject(new TimeoutError("Connection timeout", { url }));
262
+ }, this.config.connectionTimeout || TIMEOUTS.CONNECTION_TIMEOUT);
263
+
264
+ // Handle connection open
265
+ this.ws.on("open", async () => {
266
+ clearTimeout(this.connectionTimer);
267
+ this.logger.info("WebSocket connection established");
268
+
269
+ this.updateConnectionState({
270
+ connected: true,
271
+ reconnecting: false,
272
+ reconnectAttempts: 0,
273
+ lastConnectedAt: new Date()
274
+ });
275
+
276
+ this.emit("connection:open");
277
+ this.startPingInterval();
278
+
279
+ try {
280
+ await this.authenticate();
281
+ this.processMessageQueue();
282
+ resolve();
283
+ } catch (error) {
284
+ reject(error);
285
+ }
286
+ });
287
+
288
+ // Handle messages
289
+ this.ws.on("message", (data) => {
290
+ try {
291
+ const rawMessage = JSON.parse(data.toString());
292
+ // Validate message with Zod
293
+ const parseResult = safeParseMessage(rawMessage);
294
+
295
+ if (parseResult.success) {
296
+ this.handleMessage(parseResult.data as BaseMessage);
297
+ } else {
298
+ this.logger.error("Invalid message format", parseResult.error);
299
+ this.emit(
300
+ "message:error",
301
+ new ValidationError("Invalid message format", parseResult.error),
302
+ rawMessage
303
+ );
304
+ }
305
+ } catch (error) {
306
+ this.logger.error("Failed to parse message", error);
307
+ this.emit("message:error", new MessageError("Failed to parse message", error));
308
+ }
309
+ });
310
+
311
+ // Handle errors
312
+ this.ws.on("error", (error) => {
313
+ clearTimeout(this.connectionTimer);
314
+ this.logger.error("WebSocket error", error);
315
+ this.emit("connection:error", error as Error);
316
+
317
+ if (!this.connectionState.connected) {
318
+ reject(new ConnectionError("Failed to connect", error));
319
+ }
320
+ });
321
+
322
+ // Handle close
323
+ this.ws.on("close", (code, reason) => {
324
+ clearTimeout(this.connectionTimer);
325
+ this.logger.info("WebSocket connection closed", {
326
+ code,
327
+ reason: reason.toString()
328
+ });
329
+
330
+ this.updateConnectionState({
331
+ connected: false,
332
+ authenticated: false,
333
+ lastDisconnectedAt: new Date(),
334
+ lastError: new Error(`Connection closed: ${reason}`)
335
+ });
336
+
337
+ this.updateAuthState({ authenticated: false });
338
+ this.emit("connection:close", code, reason.toString());
339
+
340
+ this.stopPingInterval();
341
+ this.handleReconnection();
342
+ });
343
+
344
+ // Handle pong
345
+ this.ws.on("pong", () => {
346
+ this.logger.debug("Received pong");
347
+ });
348
+ });
349
+ }
350
+
351
+ /**
352
+ * Disconnects from the WebSocket server and cleans up all resources.
353
+ * Stops reconnection attempts, clears all timers, rejects pending messages,
354
+ * and updates connection state. Emits 'disconnect' event.
355
+ *
356
+ * @example
357
+ * ```typescript
358
+ * wsClient.disconnect();
359
+ * console.log('Disconnected from server');
360
+ * ```
361
+ */
362
+ public disconnect(): void {
363
+ this.logger.info("Disconnecting from WebSocket server");
364
+
365
+ // Clear all timers
366
+ if (this.reconnectTimer) {
367
+ clearTimeout(this.reconnectTimer);
368
+ this.reconnectTimer = undefined;
369
+ }
370
+
371
+ if (this.pingTimer) {
372
+ clearInterval(this.pingTimer);
373
+ this.pingTimer = undefined;
374
+ }
375
+
376
+ if (this.connectionTimer) {
377
+ clearTimeout(this.connectionTimer);
378
+ this.connectionTimer = undefined;
379
+ }
380
+
381
+ // Clear pending messages
382
+ for (const [, pending] of this.pendingMessages) {
383
+ clearTimeout(pending.timeout);
384
+ pending.reject(new Error("Connection closed"));
385
+ }
386
+ this.pendingMessages.clear();
387
+
388
+ // Close WebSocket
389
+ if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
390
+ this.ws.close(1000, "Client disconnect");
391
+ this.ws = undefined;
392
+ }
393
+
394
+ // Clean up secure key (SEC-3) - only if we created it
395
+ if (this.secureKey && this.ownsSecureKey) {
396
+ this.secureKey.destroy();
397
+ this.secureKey = undefined;
398
+ this.ownsSecureKey = false;
399
+ }
400
+
401
+ // Clear deduplication cache (CB-4)
402
+ if (this.deduplicationCache) {
403
+ this.deduplicationCache.clear();
404
+ }
405
+
406
+ // Update state
407
+ this.updateConnectionState({
408
+ connected: false,
409
+ authenticated: false,
410
+ reconnecting: false
411
+ });
412
+
413
+ this.updateAuthState({ authenticated: false });
414
+ this.emit("disconnect");
415
+ }
416
+
417
+ /**
418
+ * Sends a message to the WebSocket server with validation and queueing support.
419
+ * Validates message with Zod schema, enforces size limits, and queues messages
420
+ * during reconnection. Adds timestamp if not present. Emits 'message:sent' event.
421
+ *
422
+ * @param message - The message to send
423
+ * @returns Promise that resolves when message is sent successfully
424
+ * @throws {ValidationError} If message fails Zod validation
425
+ * @throws {MessageError} If message size exceeds limit
426
+ * @throws {ConnectionError} If not connected and reconnection is disabled
427
+ *
428
+ * @example
429
+ * ```typescript
430
+ * const message = createUserMessage('Hello', 'general', walletAddress);
431
+ * await wsClient.sendMessage(message);
432
+ * console.log('Message sent');
433
+ * ```
434
+ */
435
+ public async sendMessage(message: BaseMessage): Promise<void> {
436
+ // Validate outgoing message with Zod
437
+ let validatedMessage: BaseMessage;
438
+ try {
439
+ validatedMessage = BaseMessageSchema.parse(message);
440
+ } catch (error) {
441
+ this.logger.error("Failed to validate message", error);
442
+ if (error instanceof z.ZodError) {
443
+ throw new ValidationError("Invalid message format", error);
444
+ }
445
+ throw new MessageError("Failed to validate message", error);
446
+ }
447
+
448
+ // Check connection
449
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
450
+ if (this.config.reconnect && this.connectionState.reconnecting) {
451
+ // Queue message if reconnecting
452
+ this.messageQueue.push(validatedMessage);
453
+ this.logger.debug("Message queued for reconnection", {
454
+ type: validatedMessage.type
455
+ });
456
+ return;
457
+ } else {
458
+ throw new ConnectionError("WebSocket is not connected");
459
+ }
460
+ }
461
+
462
+ // Add timestamp if not present
463
+ if (!validatedMessage.timestamp) {
464
+ validatedMessage.timestamp = new Date().toISOString();
465
+ }
466
+
467
+ // Apply rate limiting if configured (CB-2)
468
+ if (this.rateLimiter) {
469
+ try {
470
+ await this.rateLimiter.consume();
471
+ } catch (error) {
472
+ this.logger.warn("Rate limit exceeded, waiting for token", {
473
+ type: validatedMessage.type
474
+ });
475
+ throw new MessageError("Rate limit exceeded", error);
476
+ }
477
+ }
478
+
479
+ // Prepare message data
480
+ const data = JSON.stringify(validatedMessage);
481
+
482
+ if (data.length > (this.config.maxMessageSize || LIMITS.MAX_MESSAGE_SIZE)) {
483
+ throw new Error("Message size exceeds maximum allowed");
484
+ }
485
+
486
+ // Send message
487
+ return new Promise((resolve, reject) => {
488
+ this.ws!.send(data, (error) => {
489
+ if (error) {
490
+ this.logger.error("Failed to send message", error);
491
+ reject(error);
492
+ } else {
493
+ this.logger.debug("Message sent", { type: validatedMessage.type });
494
+ this.emit("message:sent", validatedMessage);
495
+ resolve();
496
+ }
497
+ });
498
+ });
499
+ }
500
+
501
+ /**
502
+ * Sends a message and waits for a response with the same message ID.
503
+ * Implements request-response pattern over WebSocket with timeout support.
504
+ * Message is automatically assigned a unique ID for correlation.
505
+ *
506
+ * @template T - Expected response type
507
+ * @param message - The message to send
508
+ * @param timeout - Optional timeout in milliseconds (default: from config or 60000)
509
+ * @returns Promise that resolves with the response message
510
+ * @throws {TimeoutError} If response is not received within timeout
511
+ * @throws {ValidationError} If message fails validation
512
+ * @throws {MessageError} If message sending fails
513
+ *
514
+ * @example
515
+ * ```typescript
516
+ * const requestMessage = createRequestChallenge('user', walletAddress);
517
+ * const response = await wsClient.sendMessageWithResponse(requestMessage, 30000);
518
+ * console.log('Response received:', response);
519
+ * ```
520
+ */
521
+ public async sendMessageWithResponse<T = any>(
522
+ message: BaseMessage,
523
+ timeout?: number
524
+ ): Promise<T> {
525
+ return new Promise((resolve, reject) => {
526
+ const messageId = uuidv4();
527
+ const messageWithId = { ...message, id: messageId };
528
+
529
+ // Set timeout
530
+ const timeoutMs = timeout || this.config.messageTimeout || TIMEOUTS.DEFAULT_MESSAGE_TIMEOUT;
531
+ const timeoutHandle = setTimeout(() => {
532
+ this.pendingMessages.delete(messageId);
533
+ reject(
534
+ new TimeoutError(`Message timeout after ${timeoutMs}ms`, {
535
+ messageId
536
+ })
537
+ );
538
+ }, timeoutMs);
539
+
540
+ // Store pending message
541
+ this.pendingMessages.set(messageId, {
542
+ resolve,
543
+ reject,
544
+ timeout: timeoutHandle
545
+ });
546
+
547
+ // Send message
548
+ this.sendMessage(messageWithId).catch((error) => {
549
+ this.pendingMessages.delete(messageId);
550
+ clearTimeout(timeoutHandle);
551
+ reject(error);
552
+ });
553
+ });
554
+ }
555
+
556
+ /**
557
+ * Authenticate with the server
558
+ */
559
+ private async authenticate(): Promise<void> {
560
+ if (!this.account && !this.config.walletAddress) {
561
+ this.logger.info("No authentication configured, continuing without auth");
562
+ this.updateAuthState({ authenticated: false });
563
+ this.emit("ready");
564
+ return;
565
+ }
566
+
567
+ try {
568
+ // Check for cached authentication first
569
+ if (this.config.walletAddress) {
570
+ this.logger.debug("Checking cached authentication");
571
+ await this.sendMessage(createCheckCachedAuth(this.config.walletAddress));
572
+
573
+ // Wait briefly for cached auth response
574
+ await new Promise((resolve) => setTimeout(resolve, TIMEOUTS.CACHED_AUTH_WAIT));
575
+
576
+ if (this.authState.authenticated) {
577
+ this.logger.info("Using cached authentication");
578
+ return;
579
+ }
580
+ }
581
+
582
+ // Request challenge
583
+ this.logger.debug("Requesting authentication challenge");
584
+ await this.sendMessage(
585
+ createRequestChallenge(
586
+ this.config.clientType || "user",
587
+ this.account?.address || this.config.walletAddress
588
+ )
589
+ );
590
+
591
+ // Wait for authentication to complete
592
+ await new Promise<void>((resolve, reject) => {
593
+ let timeout: NodeJS.Timeout | undefined;
594
+ const pollTimeouts: NodeJS.Timeout[] = [];
595
+
596
+ // Centralized cleanup function - guarantees cleanup in all scenarios
597
+ const cleanup = () => {
598
+ if (timeout) {
599
+ clearTimeout(timeout);
600
+ timeout = undefined;
601
+ }
602
+ // Clear all polling timeouts
603
+ pollTimeouts.forEach((t) => clearTimeout(t));
604
+ pollTimeouts.length = 0;
605
+ };
606
+
607
+ // Set main authentication timeout
608
+ timeout = setTimeout(() => {
609
+ cleanup();
610
+ reject(new AuthenticationError("Authentication timeout"));
611
+ }, TIMEOUTS.AUTH_TIMEOUT);
612
+
613
+ const checkAuth = () => {
614
+ if (this.authState.authenticated) {
615
+ cleanup();
616
+ resolve();
617
+ } else if (this.connectionState.lastError) {
618
+ cleanup();
619
+ reject(this.connectionState.lastError);
620
+ } else {
621
+ // Store polling timeout for cleanup
622
+ const pollTimeout = setTimeout(checkAuth, TIMEOUTS.AUTH_POLL_INTERVAL);
623
+ pollTimeouts.push(pollTimeout);
624
+ }
625
+ };
626
+
627
+ checkAuth();
628
+ });
629
+ } catch (error) {
630
+ this.logger.error("Authentication failed", error);
631
+ throw new AuthenticationError("Failed to authenticate", error);
632
+ }
633
+ }
634
+
635
+ /**
636
+ * Create handler context with all dependencies
637
+ */
638
+ private createHandlerContext(): HandlerContext {
639
+ return {
640
+ emit: (event: string, ...args: any[]) => this.emit(event as any, ...args),
641
+ sendWebhook: async () => {
642
+ // Webhooks are handled by WebhookHandler in TeneoSDK
643
+ // Handlers shouldn't call webhooks directly - they emit events
644
+ // which are then forwarded to webhooks by MessageRouter
645
+ },
646
+ logger: this.logger,
647
+ getConnectionState: () => this.getConnectionState(),
648
+ getAuthState: () => this.getAuthState(),
649
+ updateConnectionState: (update: any) => this.updateConnectionState(update),
650
+ updateAuthState: (update: any) => this.updateAuthState(update),
651
+ roomManager: this.roomManager,
652
+ account: this.account,
653
+ sendMessage: (message: BaseMessage) => this.sendMessage(message)
654
+ };
655
+ }
656
+
657
+ /**
658
+ * Handle incoming messages using the handler registry
659
+ */
660
+ private async handleMessage(message: BaseMessage): Promise<void> {
661
+ this.logger.debug("Received message", { type: message.type });
662
+ this.emit("message:received", message);
663
+
664
+ // Check for duplicate messages (CB-4)
665
+ if (this.deduplicationCache && message.id) {
666
+ if (this.deduplicationCache.has(message.id)) {
667
+ this.logger.debug("Duplicate message detected and skipped", {
668
+ type: message.type,
669
+ id: message.id
670
+ });
671
+ this.emit("message:duplicate", message);
672
+ return;
673
+ }
674
+ // Add to deduplication cache
675
+ this.deduplicationCache.add(message.id);
676
+ }
677
+
678
+ // Check for pending message response
679
+ if (message.id && this.pendingMessages.has(message.id)) {
680
+ const pending = this.pendingMessages.get(message.id)!;
681
+ clearTimeout(pending.timeout);
682
+ this.pendingMessages.delete(message.id);
683
+ pending.resolve(message);
684
+ return;
685
+ }
686
+
687
+ // Verify signature if enabled (SEC-2)
688
+ const shouldProcess = await this.verifyMessageSignature(message);
689
+ if (!shouldProcess) {
690
+ this.logger.debug("Message rejected by signature verification", {
691
+ type: message.type
692
+ });
693
+ return;
694
+ }
695
+
696
+ // Delegate to handler registry
697
+ const context = this.createHandlerContext();
698
+ this.handlerRegistry.handle(message, context).catch((error) => {
699
+ this.logger.error("Error in message handler", error);
700
+ this.emit("message:error", error, message);
701
+ });
702
+ }
703
+
704
+ /**
705
+ * Verify message signature if signature verification is enabled (SEC-2)
706
+ * Returns true if message should be processed, false if it should be rejected
707
+ */
708
+ private async verifyMessageSignature(message: BaseMessage): Promise<boolean> {
709
+ // Skip verification if disabled
710
+ if (!this.signatureVerifier) {
711
+ return true;
712
+ }
713
+
714
+ try {
715
+ const result = await this.signatureVerifier.verify(message);
716
+
717
+ if (result.signatureMissing) {
718
+ // Signature is missing
719
+ const isRequired = this.signatureVerifier.isSignatureRequired(message.type);
720
+ this.emit("signature:missing", message.type, isRequired);
721
+
722
+ if (!result.valid) {
723
+ // Signature required but missing - reject message
724
+ this.logger.warn("Message rejected: signature required but missing", {
725
+ type: message.type,
726
+ from: message.from
727
+ });
728
+ const error = new SignatureVerificationError(
729
+ `Signature required for message type '${message.type}'`,
730
+ {
731
+ messageType: message.type,
732
+ reason: "Signature missing"
733
+ }
734
+ );
735
+ this.emit("message:error", error, message);
736
+ return false;
737
+ } else {
738
+ // Signature not required - allow message
739
+ this.logger.debug("Message accepted without signature", {
740
+ type: message.type
741
+ });
742
+ return true;
743
+ }
744
+ }
745
+
746
+ if (!result.valid) {
747
+ // Signature is invalid
748
+ this.logger.warn("Message rejected: invalid signature", {
749
+ type: message.type,
750
+ from: message.from,
751
+ reason: result.reason
752
+ });
753
+ this.emit(
754
+ "signature:failed",
755
+ message.type,
756
+ result.reason || "Invalid signature",
757
+ result.recoveredAddress
758
+ );
759
+
760
+ const error = new SignatureVerificationError(
761
+ `Signature verification failed for message type '${message.type}': ${result.reason}`,
762
+ {
763
+ messageType: message.type,
764
+ recoveredAddress: result.recoveredAddress,
765
+ reason: result.reason
766
+ }
767
+ );
768
+ this.emit("message:error", error, message);
769
+ return false;
770
+ }
771
+
772
+ // Signature is valid
773
+ this.logger.debug("Message signature verified", {
774
+ type: message.type,
775
+ address: result.recoveredAddress,
776
+ isTrusted: result.isTrusted
777
+ });
778
+ this.emit("signature:verified", message.type, result.recoveredAddress!);
779
+ return true;
780
+ } catch (error) {
781
+ this.logger.error("Signature verification error", error);
782
+ const verificationError = new SignatureVerificationError(
783
+ `Signature verification error: ${error instanceof Error ? error.message : String(error)}`,
784
+ {
785
+ messageType: message.type,
786
+ reason: error instanceof Error ? error.message : String(error)
787
+ }
788
+ );
789
+ this.emit("message:error", verificationError, message);
790
+ return false;
791
+ }
792
+ }
793
+
794
+ /**
795
+ * Handle reconnection logic with configurable retry strategy (REL-3)
796
+ */
797
+ private handleReconnection(): void {
798
+ if (!this.config.reconnect || this.connectionState.reconnecting) {
799
+ return;
800
+ }
801
+
802
+ // Check if we should retry using the retry policy
803
+ if (!this.reconnectPolicy.shouldRetry(this.connectionState.reconnectAttempts + 1)) {
804
+ this.logger.error("Max reconnection attempts reached");
805
+ this.emit("error", new ConnectionError("Max reconnection attempts reached"));
806
+ return;
807
+ }
808
+
809
+ this.updateConnectionState({
810
+ reconnecting: true,
811
+ reconnectAttempts: this.connectionState.reconnectAttempts + 1
812
+ });
813
+
814
+ const delay = this.calculateReconnectDelay();
815
+ this.logger.info(
816
+ `Reconnecting in ${delay}ms (attempt ${this.connectionState.reconnectAttempts})`
817
+ );
818
+ this.emit("connection:reconnecting", this.connectionState.reconnectAttempts);
819
+
820
+ this.reconnectTimer = setTimeout(async () => {
821
+ try {
822
+ await this.connect();
823
+ this.emit("connection:reconnected");
824
+ } catch (error) {
825
+ this.logger.error("Reconnection failed", error);
826
+ this.handleReconnection();
827
+ }
828
+ }, delay);
829
+ }
830
+
831
+ /**
832
+ * Calculate reconnection delay using retry policy (REL-3)
833
+ */
834
+ private calculateReconnectDelay(): number {
835
+ return this.reconnectPolicy.calculateDelay(this.connectionState.reconnectAttempts);
836
+ }
837
+
838
+ /**
839
+ * Start ping interval to keep connection alive
840
+ */
841
+ private startPingInterval(): void {
842
+ this.stopPingInterval();
843
+
844
+ this.pingTimer = setInterval(() => {
845
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
846
+ this.sendMessage(createPing()).catch((error) => {
847
+ this.logger.error("Failed to send ping", error);
848
+ });
849
+ }
850
+ }, TIMEOUTS.PING_INTERVAL);
851
+ }
852
+
853
+ /**
854
+ * Stop ping interval
855
+ */
856
+ private stopPingInterval(): void {
857
+ if (this.pingTimer) {
858
+ clearInterval(this.pingTimer);
859
+ this.pingTimer = undefined;
860
+ }
861
+ }
862
+
863
+ /**
864
+ * Process queued messages after reconnection
865
+ */
866
+ private processMessageQueue(): void {
867
+ if (this.messageQueue.length === 0) {
868
+ return;
869
+ }
870
+
871
+ this.logger.info(`Processing ${this.messageQueue.length} queued messages`);
872
+ const queue = [...this.messageQueue];
873
+ this.messageQueue = [];
874
+
875
+ for (const message of queue) {
876
+ this.sendMessage(message).catch((error) => {
877
+ this.logger.error("Failed to send queued message", error);
878
+ this.emit("message:error", error as Error, message);
879
+ });
880
+ }
881
+ }
882
+
883
+ /**
884
+ * Update connection state with validation
885
+ */
886
+ private updateConnectionState(update: Partial<ConnectionState>): void {
887
+ const newState = { ...this.connectionState, ...update };
888
+ // Validate the new state
889
+ this.connectionState = ConnectionStateSchema.parse(newState);
890
+ this.emit("connection:state", this.connectionState);
891
+ }
892
+
893
+ /**
894
+ * Update authentication state with validation
895
+ */
896
+ private updateAuthState(update: Partial<AuthenticationState>): void {
897
+ const newState = { ...this.authState, ...update };
898
+ // Validate the new state
899
+ this.authState = AuthenticationStateSchema.parse(newState);
900
+ this.emit("auth:state", this.authState);
901
+ }
902
+
903
+ /**
904
+ * Create default logger using pino
905
+ */
906
+ private createDefaultLogger(): Logger {
907
+ return createPinoLogger(this.config.logLevel || "info", "WebSocketClient");
908
+ }
909
+
910
+ // Getters
911
+ /**
912
+ * Quick check for whether the WebSocket connection is currently active.
913
+ * This getter provides immediate connection status without full state details.
914
+ *
915
+ * @returns True if WebSocket is connected, false otherwise
916
+ *
917
+ * @example
918
+ * ```typescript
919
+ * if (wsClient.isConnected) {
920
+ * await wsClient.sendMessage(message);
921
+ * }
922
+ * ```
923
+ */
924
+ public get isConnected(): boolean {
925
+ return this.connectionState.connected;
926
+ }
927
+
928
+ /**
929
+ * Quick check for whether authentication is complete.
930
+ * This getter provides immediate authentication status without full state details.
931
+ *
932
+ * @returns True if authenticated, false otherwise
933
+ *
934
+ * @example
935
+ * ```typescript
936
+ * if (wsClient.isAuthenticated) {
937
+ * console.log('Ready to communicate with agents');
938
+ * }
939
+ * ```
940
+ */
941
+ public get isAuthenticated(): boolean {
942
+ return this.authState.authenticated;
943
+ }
944
+
945
+ /**
946
+ * Gets a copy of the current connection state including detailed status information.
947
+ * Returns a shallow copy to prevent external modification of internal state.
948
+ *
949
+ * @returns Copy of connection state with connection status, reconnection info, and timestamps
950
+ *
951
+ * @example
952
+ * ```typescript
953
+ * const state = wsClient.getConnectionState();
954
+ * console.log(`Connected: ${state.connected}`);
955
+ * console.log(`Reconnecting: ${state.reconnecting}`);
956
+ * console.log(`Attempts: ${state.reconnectAttempts}`);
957
+ * ```
958
+ */
959
+ public getConnectionState(): ConnectionState {
960
+ return { ...this.connectionState };
961
+ }
962
+
963
+ /**
964
+ * Gets a copy of the current authentication state including wallet and room information.
965
+ * Returns a shallow copy to prevent external modification of internal state.
966
+ *
967
+ * @returns Copy of authentication state with wallet address, challenge, and room access
968
+ *
969
+ * @example
970
+ * ```typescript
971
+ * const authState = wsClient.getAuthState();
972
+ * console.log(`Authenticated: ${authState.authenticated}`);
973
+ * console.log(`Wallet: ${authState.walletAddress}`);
974
+ * console.log(`Rooms: ${authState.rooms?.length}`);
975
+ * ```
976
+ */
977
+ public getAuthState(): AuthenticationState {
978
+ return { ...this.authState };
979
+ }
980
+
981
+ /**
982
+ * Gets the current rate limiter status including available tokens and configuration.
983
+ * Useful for monitoring rate limiting behavior and detecting potential throttling.
984
+ * Returns undefined if rate limiting is not configured.
985
+ *
986
+ * @returns Rate limiter status object, or undefined if not configured
987
+ * @returns {number} returns.availableTokens - Tokens currently available for consumption
988
+ * @returns {number} returns.tokensPerSecond - Configured rate limit (operations per second)
989
+ * @returns {number} returns.maxBurst - Maximum burst capacity
990
+ *
991
+ * @example
992
+ * ```typescript
993
+ * const status = wsClient.getRateLimiterStatus();
994
+ * if (status) {
995
+ * console.log(`Available: ${status.availableTokens}/${status.maxBurst}`);
996
+ * console.log(`Rate: ${status.tokensPerSecond}/sec`);
997
+ * }
998
+ * ```
999
+ */
1000
+ public getRateLimiterStatus():
1001
+ | {
1002
+ availableTokens: number;
1003
+ tokensPerSecond: number;
1004
+ maxBurst: number;
1005
+ }
1006
+ | undefined {
1007
+ if (!this.rateLimiter) {
1008
+ return undefined;
1009
+ }
1010
+
1011
+ const config = this.rateLimiter.getConfig();
1012
+ return {
1013
+ availableTokens: this.rateLimiter.getAvailableTokens(),
1014
+ tokensPerSecond: config.tokensPerSecond,
1015
+ maxBurst: config.maxBurst
1016
+ };
1017
+ }
1018
+
1019
+ /**
1020
+ * Gets the current message deduplication cache status (CB-4).
1021
+ * Useful for monitoring deduplication behavior and cache health.
1022
+ * Returns undefined if deduplication is not configured.
1023
+ *
1024
+ * @returns Deduplication cache status object, or undefined if not configured
1025
+ * @returns {number} returns.cacheSize - Number of message IDs currently cached
1026
+ * @returns {number} returns.ttl - Time-to-live for cache entries in milliseconds
1027
+ * @returns {number} returns.maxSize - Maximum cache size
1028
+ *
1029
+ * @example
1030
+ * ```typescript
1031
+ * const status = wsClient.getDeduplicationStatus();
1032
+ * if (status) {
1033
+ * console.log(`Cache: ${status.cacheSize}/${status.maxSize}`);
1034
+ * console.log(`TTL: ${status.ttl}ms`);
1035
+ * }
1036
+ * ```
1037
+ */
1038
+ public getDeduplicationStatus():
1039
+ | {
1040
+ cacheSize: number;
1041
+ ttl: number;
1042
+ maxSize: number;
1043
+ }
1044
+ | undefined {
1045
+ if (!this.deduplicationCache) {
1046
+ return undefined;
1047
+ }
1048
+
1049
+ const config = this.deduplicationCache.getConfig();
1050
+ return {
1051
+ cacheSize: this.deduplicationCache.size(),
1052
+ ttl: config.ttl,
1053
+ maxSize: config.maxSize
1054
+ };
1055
+ }
1056
+ }