@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,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
|
+
}
|