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