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