nostr-websocket-utils 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +151 -103
  3. package/dist/__mocks__/extendedWsMock.d.ts +35 -0
  4. package/dist/__mocks__/extendedWsMock.js +156 -0
  5. package/dist/__mocks__/logger.d.ts +9 -0
  6. package/dist/__mocks__/logger.js +6 -0
  7. package/dist/__mocks__/mockLogger.d.ts +41 -0
  8. package/dist/__mocks__/mockLogger.js +47 -0
  9. package/dist/__mocks__/mockserver.d.ts +31 -0
  10. package/dist/__mocks__/mockserver.js +39 -0
  11. package/dist/__mocks__/wsMock.d.ts +26 -0
  12. package/dist/__mocks__/wsMock.js +120 -0
  13. package/dist/client.d.ts +105 -0
  14. package/dist/client.js +105 -0
  15. package/dist/core/client.d.ts +94 -0
  16. package/dist/core/client.js +360 -0
  17. package/dist/core/nostr-server.d.ts +27 -0
  18. package/dist/core/nostr-server.js +95 -0
  19. package/dist/core/queue.d.ts +61 -0
  20. package/dist/core/queue.js +108 -0
  21. package/dist/core/server.d.ts +27 -0
  22. package/dist/core/server.js +114 -0
  23. package/dist/crypto/bech32.d.ts +26 -0
  24. package/dist/crypto/bech32.js +163 -0
  25. package/dist/crypto/handlers.d.ts +11 -0
  26. package/dist/crypto/handlers.js +36 -0
  27. package/dist/crypto/index.d.ts +5 -0
  28. package/dist/crypto/index.js +5 -0
  29. package/dist/crypto/schnorr.d.ts +16 -0
  30. package/dist/crypto/schnorr.js +51 -0
  31. package/dist/endpoints/metrics.d.ts +29 -0
  32. package/dist/endpoints/metrics.js +101 -0
  33. package/dist/index.d.ts +11 -6
  34. package/dist/index.js +16 -4
  35. package/dist/nips/index.d.ts +19 -0
  36. package/dist/nips/index.js +34 -0
  37. package/dist/nips/nip-01.d.ts +34 -0
  38. package/dist/nips/nip-01.js +145 -0
  39. package/dist/nips/nip-02.d.ts +83 -0
  40. package/dist/nips/nip-02.js +123 -0
  41. package/dist/nips/nip-04.d.ts +36 -0
  42. package/dist/nips/nip-04.js +105 -0
  43. package/dist/nips/nip-05.d.ts +86 -0
  44. package/dist/nips/nip-05.js +151 -0
  45. package/dist/nips/nip-09.d.ts +92 -0
  46. package/dist/nips/nip-09.js +190 -0
  47. package/dist/nips/nip-11.d.ts +64 -0
  48. package/dist/nips/nip-11.js +154 -0
  49. package/dist/nips/nip-13.d.ts +73 -0
  50. package/dist/nips/nip-13.js +128 -0
  51. package/dist/nips/nip-15.d.ts +83 -0
  52. package/dist/nips/nip-15.js +101 -0
  53. package/dist/nips/nip-16.d.ts +88 -0
  54. package/dist/nips/nip-16.js +150 -0
  55. package/dist/nips/nip-19.d.ts +28 -0
  56. package/dist/nips/nip-19.js +103 -0
  57. package/dist/nips/nip-20.d.ts +59 -0
  58. package/dist/nips/nip-20.js +95 -0
  59. package/dist/nips/nip-22.d.ts +89 -0
  60. package/dist/nips/nip-22.js +142 -0
  61. package/dist/nips/nip-26.d.ts +52 -0
  62. package/dist/nips/nip-26.js +139 -0
  63. package/dist/nips/nip-28.d.ts +103 -0
  64. package/dist/nips/nip-28.js +170 -0
  65. package/dist/nips/nip-33.d.ts +94 -0
  66. package/dist/nips/nip-33.js +133 -0
  67. package/dist/nostr-server.d.ts +23 -0
  68. package/dist/nostr-server.js +44 -0
  69. package/dist/server.d.ts +13 -3
  70. package/dist/server.js +60 -33
  71. package/dist/transport/base.d.ts +54 -0
  72. package/dist/transport/base.js +104 -0
  73. package/dist/transport/websocket.d.ts +22 -0
  74. package/dist/transport/websocket.js +122 -0
  75. package/dist/types/events.d.ts +63 -0
  76. package/dist/types/events.js +5 -0
  77. package/dist/types/filters.d.ts +19 -0
  78. package/dist/types/filters.js +5 -0
  79. package/dist/types/handlers.d.ts +80 -0
  80. package/dist/types/handlers.js +5 -0
  81. package/dist/types/index.d.ts +118 -39
  82. package/dist/types/index.js +21 -1
  83. package/dist/types/logger.d.ts +40 -0
  84. package/dist/types/logger.js +5 -0
  85. package/dist/types/messages.d.ts +135 -0
  86. package/dist/types/messages.js +40 -0
  87. package/dist/types/nostr.d.ts +120 -39
  88. package/dist/types/nostr.js +5 -10
  89. package/dist/types/options.d.ts +154 -0
  90. package/dist/types/options.js +5 -0
  91. package/dist/types/relays.d.ts +26 -0
  92. package/dist/types/relays.js +5 -0
  93. package/dist/types/scoring.d.ts +47 -0
  94. package/dist/types/scoring.js +29 -0
  95. package/dist/types/socket.d.ts +99 -0
  96. package/dist/types/socket.js +5 -0
  97. package/dist/types/transport.d.ts +97 -0
  98. package/dist/types/transport.js +5 -0
  99. package/dist/types/validation.d.ts +50 -0
  100. package/dist/types/validation.js +5 -0
  101. package/dist/types/websocket.d.ts +172 -0
  102. package/dist/types/websocket.js +5 -0
  103. package/dist/utils/http.d.ts +10 -0
  104. package/dist/utils/http.js +24 -0
  105. package/dist/utils/logger.d.ts +11 -2
  106. package/dist/utils/logger.js +18 -13
  107. package/dist/utils/metrics.d.ts +81 -0
  108. package/dist/utils/metrics.js +206 -0
  109. package/dist/utils/rate-limiter.d.ts +85 -0
  110. package/dist/utils/rate-limiter.js +175 -0
  111. package/package.json +18 -21
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @file WebSocket type definitions and extensions
3
+ * @module types/websocket
4
+ */
5
+ export {};
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @file HTTP utility functions
3
+ * @module utils/http
4
+ */
5
+ /**
6
+ * Fetches JSON data from a URL
7
+ * @param url URL to fetch from
8
+ * @returns Parsed JSON data
9
+ */
10
+ export declare function fetchJson<T>(url: string): Promise<T>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @file HTTP utility functions
3
+ * @module utils/http
4
+ */
5
+ import { getLogger } from './logger';
6
+ const logger = getLogger('http');
7
+ /**
8
+ * Fetches JSON data from a URL
9
+ * @param url URL to fetch from
10
+ * @returns Parsed JSON data
11
+ */
12
+ export async function fetchJson(url) {
13
+ try {
14
+ const response = await fetch(url);
15
+ if (!response.ok) {
16
+ throw new Error(`HTTP error! status: ${response.status}`);
17
+ }
18
+ return await response.json();
19
+ }
20
+ catch (error) {
21
+ logger.error({ error, url }, 'Failed to fetch JSON');
22
+ throw error;
23
+ }
24
+ }
@@ -1,2 +1,11 @@
1
- import winston from 'winston';
2
- export declare function getLogger(name?: string): winston.Logger;
1
+ /**
2
+ * @file Logger utility
3
+ * @module utils/logger
4
+ */
5
+ import { Logger } from '../types/logger';
6
+ /**
7
+ * Creates a logger instance for a specific component
8
+ * @param component Component name for the logger
9
+ * @returns Logger instance
10
+ */
11
+ export declare function getLogger(component: string): Logger;
@@ -1,16 +1,21 @@
1
- import winston from 'winston';
2
- const logger = winston.createLogger({
1
+ /**
2
+ * @file Logger utility
3
+ * @module utils/logger
4
+ */
5
+ import pino from 'pino';
6
+ const rootLogger = pino({
3
7
  level: process.env.LOG_LEVEL || 'info',
4
- format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
5
- transports: [
6
- new winston.transports.Console({
7
- format: winston.format.combine(winston.format.colorize(), winston.format.simple())
8
- })
9
- ]
8
+ timestamp: true,
9
+ formatters: {
10
+ level: (label) => ({ level: label }),
11
+ bindings: (bindings) => bindings,
12
+ },
10
13
  });
11
- export function getLogger(name) {
12
- if (name) {
13
- return logger.child({ service: name });
14
- }
15
- return logger;
14
+ /**
15
+ * Creates a logger instance for a specific component
16
+ * @param component Component name for the logger
17
+ * @returns Logger instance
18
+ */
19
+ export function getLogger(component) {
20
+ return rootLogger.child({ component });
16
21
  }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @file Metrics tracking for Nostr WebSocket connections
3
+ * @module metrics
4
+ */
5
+ /// <reference types="node" />
6
+ import { EventEmitter } from 'events';
7
+ export interface RelayMetrics {
8
+ totalConnections: number;
9
+ activeConnections: number;
10
+ connectionErrors: number;
11
+ messagesReceived: number;
12
+ messagesSent: number;
13
+ bytesReceived: number;
14
+ bytesSent: number;
15
+ averageLatency: number;
16
+ maxLatency: number;
17
+ minLatency: number;
18
+ totalErrors: number;
19
+ lastError?: string;
20
+ eventsReceived: number;
21
+ eventsSent: number;
22
+ subscriptions: number;
23
+ uptime: number;
24
+ reliability: number;
25
+ lastSeen: number;
26
+ score: number;
27
+ }
28
+ export declare class RelayMetricsTracker extends EventEmitter {
29
+ private metrics;
30
+ private startTime;
31
+ constructor();
32
+ /**
33
+ * Get metrics for a specific relay
34
+ */
35
+ getRelayMetrics(relayUrl: string): RelayMetrics;
36
+ /**
37
+ * Get metrics for all relays
38
+ */
39
+ getAllMetrics(): Map<string, RelayMetrics>;
40
+ /**
41
+ * Initialize metrics for a new relay
42
+ */
43
+ private initializeMetrics;
44
+ /**
45
+ * Update connection metrics
46
+ */
47
+ trackConnection(relayUrl: string, connected: boolean): void;
48
+ /**
49
+ * Track message metrics
50
+ */
51
+ trackMessage(relayUrl: string, sent: boolean, bytes: number): void;
52
+ /**
53
+ * Track latency
54
+ */
55
+ trackLatency(relayUrl: string, latencyMs: number): void;
56
+ /**
57
+ * Track errors
58
+ */
59
+ trackError(relayUrl: string, error: Error): void;
60
+ /**
61
+ * Track protocol-specific events
62
+ */
63
+ trackProtocolEvent(relayUrl: string, type: 'event' | 'subscription', sent: boolean): void;
64
+ /**
65
+ * Calculate relay score based on metrics
66
+ */
67
+ private calculateScore;
68
+ /**
69
+ * Update scores for all relays
70
+ */
71
+ private updateScores;
72
+ /**
73
+ * Get high-value relays (score > threshold)
74
+ */
75
+ getHighValueRelays(threshold?: number): string[];
76
+ /**
77
+ * Export metrics in Prometheus format
78
+ */
79
+ getPrometheusMetrics(): string;
80
+ }
81
+ export declare const metricsTracker: RelayMetricsTracker;
@@ -0,0 +1,206 @@
1
+ /**
2
+ * @file Metrics tracking for Nostr WebSocket connections
3
+ * @module metrics
4
+ */
5
+ import { EventEmitter } from 'events';
6
+ import { getLogger } from './logger';
7
+ const logger = getLogger('RelayMetrics');
8
+ export class RelayMetricsTracker extends EventEmitter {
9
+ constructor() {
10
+ super();
11
+ this.metrics = new Map();
12
+ this.startTime = Date.now();
13
+ // Periodically calculate scores
14
+ setInterval(() => this.updateScores(), 60000); // Every minute
15
+ }
16
+ /**
17
+ * Get metrics for a specific relay
18
+ */
19
+ getRelayMetrics(relayUrl) {
20
+ if (!this.metrics.has(relayUrl)) {
21
+ this.initializeMetrics(relayUrl);
22
+ }
23
+ return this.metrics.get(relayUrl);
24
+ }
25
+ /**
26
+ * Get metrics for all relays
27
+ */
28
+ getAllMetrics() {
29
+ return this.metrics;
30
+ }
31
+ /**
32
+ * Initialize metrics for a new relay
33
+ */
34
+ initializeMetrics(relayUrl) {
35
+ this.metrics.set(relayUrl, {
36
+ totalConnections: 0,
37
+ activeConnections: 0,
38
+ connectionErrors: 0,
39
+ messagesReceived: 0,
40
+ messagesSent: 0,
41
+ bytesReceived: 0,
42
+ bytesSent: 0,
43
+ averageLatency: 0,
44
+ maxLatency: 0,
45
+ minLatency: Infinity,
46
+ totalErrors: 0,
47
+ eventsReceived: 0,
48
+ eventsSent: 0,
49
+ subscriptions: 0,
50
+ uptime: 0,
51
+ reliability: 1,
52
+ lastSeen: Date.now(),
53
+ score: 100
54
+ });
55
+ }
56
+ /**
57
+ * Update connection metrics
58
+ */
59
+ trackConnection(relayUrl, connected) {
60
+ const metrics = this.getRelayMetrics(relayUrl);
61
+ if (connected) {
62
+ metrics.totalConnections++;
63
+ metrics.activeConnections++;
64
+ metrics.lastSeen = Date.now();
65
+ }
66
+ else {
67
+ metrics.activeConnections = Math.max(0, metrics.activeConnections - 1);
68
+ }
69
+ this.emit('metrics.update', relayUrl, metrics);
70
+ }
71
+ /**
72
+ * Track message metrics
73
+ */
74
+ trackMessage(relayUrl, sent, bytes) {
75
+ const metrics = this.getRelayMetrics(relayUrl);
76
+ if (sent) {
77
+ metrics.messagesSent++;
78
+ metrics.bytesSent += bytes;
79
+ }
80
+ else {
81
+ metrics.messagesReceived++;
82
+ metrics.bytesReceived += bytes;
83
+ }
84
+ metrics.lastSeen = Date.now();
85
+ this.emit('metrics.update', relayUrl, metrics);
86
+ }
87
+ /**
88
+ * Track latency
89
+ */
90
+ trackLatency(relayUrl, latencyMs) {
91
+ const metrics = this.getRelayMetrics(relayUrl);
92
+ metrics.maxLatency = Math.max(metrics.maxLatency, latencyMs);
93
+ metrics.minLatency = Math.min(metrics.minLatency, latencyMs);
94
+ // Exponential moving average for smoothing
95
+ metrics.averageLatency = metrics.averageLatency * 0.9 + latencyMs * 0.1;
96
+ this.emit('metrics.update', relayUrl, metrics);
97
+ }
98
+ /**
99
+ * Track errors
100
+ */
101
+ trackError(relayUrl, error) {
102
+ const metrics = this.getRelayMetrics(relayUrl);
103
+ metrics.totalErrors++;
104
+ metrics.lastError = error.message;
105
+ metrics.connectionErrors++;
106
+ this.emit('metrics.update', relayUrl, metrics);
107
+ }
108
+ /**
109
+ * Track protocol-specific events
110
+ */
111
+ trackProtocolEvent(relayUrl, type, sent) {
112
+ const metrics = this.getRelayMetrics(relayUrl);
113
+ if (type === 'event') {
114
+ sent ? metrics.eventsSent++ : metrics.eventsReceived++;
115
+ }
116
+ else if (type === 'subscription') {
117
+ metrics.subscriptions += sent ? 1 : -1;
118
+ metrics.subscriptions = Math.max(0, metrics.subscriptions);
119
+ }
120
+ this.emit('metrics.update', relayUrl, metrics);
121
+ }
122
+ /**
123
+ * Calculate relay score based on metrics
124
+ */
125
+ calculateScore(metrics) {
126
+ const now = Date.now();
127
+ // Calculate weighted scores for different aspects
128
+ const latencyScore = Math.max(0, 100 - (metrics.averageLatency / 10)); // Lower is better
129
+ const reliabilityScore = metrics.reliability * 100;
130
+ const uptimeScore = Math.min(100, (metrics.uptime / (60 * 60 * 24)) * 100); // Score based on 24h uptime
131
+ const errorScore = Math.max(0, 100 - (metrics.connectionErrors * 5)); // Each error reduces score by 5
132
+ const activityScore = metrics.lastSeen ? Math.max(0, 100 - ((now - metrics.lastSeen) / (60 * 1000))) : 0; // Activity in last hour
133
+ // Weighted average
134
+ return Math.round((latencyScore * 0.2) +
135
+ (reliabilityScore * 0.3) +
136
+ (uptimeScore * 0.2) +
137
+ (errorScore * 0.2) +
138
+ (activityScore * 0.1));
139
+ }
140
+ /**
141
+ * Update scores for all relays
142
+ */
143
+ updateScores() {
144
+ const now = Date.now();
145
+ for (const [url, metrics] of this.metrics.entries()) {
146
+ // Update uptime
147
+ metrics.uptime = (now - this.startTime) / 1000;
148
+ // Update reliability based on successful vs total connections
149
+ metrics.reliability = metrics.totalConnections > 0
150
+ ? 1 - (metrics.connectionErrors / metrics.totalConnections)
151
+ : 1;
152
+ // Calculate composite score
153
+ metrics.score = this.calculateScore(metrics);
154
+ this.emit('metrics.score', url, metrics.score);
155
+ logger.info({ url, score: metrics.score }, 'Updated relay score');
156
+ }
157
+ }
158
+ /**
159
+ * Get high-value relays (score > threshold)
160
+ */
161
+ getHighValueRelays(threshold = 70) {
162
+ return Array.from(this.metrics.entries())
163
+ .filter(([_, metrics]) => metrics.score >= threshold)
164
+ .map(([url]) => url);
165
+ }
166
+ /**
167
+ * Export metrics in Prometheus format
168
+ */
169
+ getPrometheusMetrics() {
170
+ const lines = [];
171
+ // Helper to format metric line
172
+ const formatMetric = (name, value, labels = {}) => {
173
+ const labelStr = Object.entries(labels)
174
+ .map(([k, v]) => `${k}="${v}"`)
175
+ .join(',');
176
+ lines.push(`nostr_${name}${labelStr ? `{${labelStr}}` : ''} ${value}`);
177
+ };
178
+ for (const [url, metrics] of this.metrics.entries()) {
179
+ const labels = { relay: url };
180
+ // Connection metrics
181
+ formatMetric('connections_total', metrics.totalConnections, labels);
182
+ formatMetric('connections_active', metrics.activeConnections, labels);
183
+ formatMetric('connection_errors_total', metrics.connectionErrors, labels);
184
+ // Message metrics
185
+ formatMetric('messages_received_total', metrics.messagesReceived, labels);
186
+ formatMetric('messages_sent_total', metrics.messagesSent, labels);
187
+ formatMetric('bytes_received_total', metrics.bytesReceived, labels);
188
+ formatMetric('bytes_sent_total', metrics.bytesSent, labels);
189
+ // Performance metrics
190
+ formatMetric('latency_average', metrics.averageLatency, labels);
191
+ formatMetric('latency_max', metrics.maxLatency, labels);
192
+ formatMetric('latency_min', metrics.minLatency, labels);
193
+ // Protocol metrics
194
+ formatMetric('events_received_total', metrics.eventsReceived, labels);
195
+ formatMetric('events_sent_total', metrics.eventsSent, labels);
196
+ formatMetric('subscriptions_active', metrics.subscriptions, labels);
197
+ // Scoring metrics
198
+ formatMetric('uptime_seconds', metrics.uptime, labels);
199
+ formatMetric('reliability_score', metrics.reliability, labels);
200
+ formatMetric('composite_score', metrics.score, labels);
201
+ }
202
+ return lines.join('\n') + '\n';
203
+ }
204
+ }
205
+ // Export singleton instance
206
+ export const metricsTracker = new RelayMetricsTracker();
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @file WebSocket Rate Limiter
3
+ * @module utils/rate-limiter
4
+ */
5
+ import type { NostrWSMessage } from '../types/messages';
6
+ import type { Logger } from '../types/logger';
7
+ /**
8
+ * Rate limit configuration for different message types
9
+ */
10
+ export interface RateLimitConfig {
11
+ windowMs: number;
12
+ maxRequests: number;
13
+ blockDurationMs?: number;
14
+ }
15
+ /**
16
+ * Client state for rate limiting
17
+ */
18
+ interface ClientState {
19
+ requests: Map<string, number[]>;
20
+ blockedUntil?: number;
21
+ }
22
+ /**
23
+ * Rate limiter interface
24
+ */
25
+ export interface RateLimiter {
26
+ shouldLimit(clientId: string, message: NostrWSMessage): Promise<boolean>;
27
+ recordRequest(clientId: string, message: NostrWSMessage): void;
28
+ getRemainingRequests(clientId: string, messageType: string): number;
29
+ isBlocked(clientId: string): boolean;
30
+ getClientState(clientId: string): ClientState;
31
+ }
32
+ /**
33
+ * Rate limit rules for different event types
34
+ */
35
+ export declare const DEFAULT_RATE_LIMITS: Record<string, RateLimitConfig>;
36
+ /**
37
+ * Creates a rate limiter
38
+ * @param config - Rate limit configuration
39
+ * @param _logger - Logger instance
40
+ * @returns {RateLimiter} Rate limiter
41
+ */
42
+ export declare function createRateLimiter(config: Record<string, RateLimitConfig> | undefined, _logger: Logger): RateLimiter;
43
+ /**
44
+ * WebSocket connection rate limiter interface
45
+ */
46
+ export interface ConnectionRateLimiter {
47
+ /**
48
+ * Checks if a new connection should be allowed
49
+ * @param clientId - Client identifier
50
+ * @returns {Promise<boolean>} True if connection is allowed
51
+ */
52
+ allowConnection(clientId: string): Promise<boolean>;
53
+ /**
54
+ * Records a connection attempt
55
+ * @param clientId - Client identifier
56
+ * @param successful - Whether connection was successful
57
+ */
58
+ recordConnection(clientId: string, successful: boolean): void;
59
+ }
60
+ /**
61
+ * Creates a connection rate limiter
62
+ * @param config - Connection limit configuration
63
+ * @param logger - Logger instance
64
+ * @returns {ConnectionRateLimiter} Connection rate limiter
65
+ */
66
+ export declare function createConnectionRateLimiter(config: {
67
+ maxConnectionsPerMinute: number;
68
+ maxConcurrentConnections: number;
69
+ blockAfterFailures: number;
70
+ blockDurationMs: number;
71
+ }, logger: Logger): ConnectionRateLimiter;
72
+ /**
73
+ * Rate limiter implementation
74
+ */
75
+ export declare class RateLimiterImpl implements RateLimiter {
76
+ private clients;
77
+ private config;
78
+ constructor(config: RateLimitConfig);
79
+ getClientState(clientId: string): ClientState;
80
+ shouldLimit(clientId: string, message: NostrWSMessage): Promise<boolean>;
81
+ recordRequest(clientId: string, message: NostrWSMessage): void;
82
+ getRemainingRequests(clientId: string, messageType: string): number;
83
+ isBlocked(clientId: string): boolean;
84
+ }
85
+ export {};
@@ -0,0 +1,175 @@
1
+ /**
2
+ * @file WebSocket Rate Limiter
3
+ * @module utils/rate-limiter
4
+ */
5
+ /**
6
+ * Rate limit rules for different event types
7
+ */
8
+ export const DEFAULT_RATE_LIMITS = {
9
+ EVENT: {
10
+ windowMs: 60000, // 1 minute
11
+ maxRequests: 60,
12
+ blockDurationMs: 300000 // 5 minutes
13
+ },
14
+ REQ: {
15
+ windowMs: 60000,
16
+ maxRequests: 30,
17
+ blockDurationMs: 300000
18
+ },
19
+ CLOSE: {
20
+ windowMs: 60000,
21
+ maxRequests: 50
22
+ },
23
+ AUTH: {
24
+ windowMs: 300000, // 5 minutes
25
+ maxRequests: 10,
26
+ blockDurationMs: 900000 // 15 minutes
27
+ }
28
+ };
29
+ /**
30
+ * Creates a rate limiter
31
+ * @param config - Rate limit configuration
32
+ * @param _logger - Logger instance
33
+ * @returns {RateLimiter} Rate limiter
34
+ */
35
+ export function createRateLimiter(config = DEFAULT_RATE_LIMITS, _logger) {
36
+ return new RateLimiterImpl(config.EVENT || {
37
+ windowMs: 60000,
38
+ maxRequests: 100,
39
+ blockDurationMs: 300000
40
+ });
41
+ }
42
+ /**
43
+ * Creates a connection rate limiter
44
+ * @param config - Connection limit configuration
45
+ * @param logger - Logger instance
46
+ * @returns {ConnectionRateLimiter} Connection rate limiter
47
+ */
48
+ export function createConnectionRateLimiter(config, logger) {
49
+ const clients = new Map();
50
+ function cleanOldAttempts(attempts) {
51
+ const now = Date.now();
52
+ return attempts.filter(attempt => now - attempt.timestamp < 60000);
53
+ }
54
+ return {
55
+ async allowConnection(clientId) {
56
+ const now = Date.now();
57
+ const state = clients.get(clientId) || {
58
+ attempts: [],
59
+ currentConnections: 0
60
+ };
61
+ // Check if blocked
62
+ if (state.blockedUntil && now < state.blockedUntil) {
63
+ logger.debug('Connection blocked', {
64
+ clientId,
65
+ until: new Date(state.blockedUntil)
66
+ });
67
+ return false;
68
+ }
69
+ // Clean old attempts
70
+ state.attempts = cleanOldAttempts(state.attempts);
71
+ // Check rate limits
72
+ if (state.attempts.length >= config.maxConnectionsPerMinute) {
73
+ logger.debug('Too many connection attempts', {
74
+ clientId,
75
+ attempts: state.attempts.length
76
+ });
77
+ return false;
78
+ }
79
+ // Check concurrent connections
80
+ if (state.currentConnections >= config.maxConcurrentConnections) {
81
+ logger.debug('Too many concurrent connections', {
82
+ clientId,
83
+ connections: state.currentConnections
84
+ });
85
+ return false;
86
+ }
87
+ // Check failure rate
88
+ const recentFailures = state.attempts.filter(a => !a.successful).length;
89
+ if (recentFailures >= config.blockAfterFailures) {
90
+ state.blockedUntil = now + config.blockDurationMs;
91
+ clients.set(clientId, state);
92
+ logger.debug('Client blocked due to failures', {
93
+ clientId,
94
+ failures: recentFailures,
95
+ until: new Date(state.blockedUntil)
96
+ });
97
+ return false;
98
+ }
99
+ return true;
100
+ },
101
+ recordConnection(clientId, successful) {
102
+ const state = clients.get(clientId) || {
103
+ attempts: [],
104
+ currentConnections: 0
105
+ };
106
+ state.attempts.push({
107
+ timestamp: Date.now(),
108
+ successful
109
+ });
110
+ if (successful) {
111
+ state.currentConnections++;
112
+ }
113
+ clients.set(clientId, state);
114
+ }
115
+ };
116
+ }
117
+ /**
118
+ * Rate limiter implementation
119
+ */
120
+ export class RateLimiterImpl {
121
+ constructor(config) {
122
+ this.clients = new Map();
123
+ this.config = {
124
+ windowMs: config.windowMs || 60000,
125
+ maxRequests: config.maxRequests || 100,
126
+ blockDurationMs: config.blockDurationMs || 300000
127
+ };
128
+ }
129
+ getClientState(clientId) {
130
+ let state = this.clients.get(clientId);
131
+ if (!state) {
132
+ state = { requests: new Map() };
133
+ this.clients.set(clientId, state);
134
+ }
135
+ return state;
136
+ }
137
+ async shouldLimit(clientId, message) {
138
+ const now = Date.now();
139
+ const state = this.getClientState(clientId);
140
+ // Check if client is blocked
141
+ if (state.blockedUntil && state.blockedUntil > now) {
142
+ return true;
143
+ }
144
+ // Get requests for message type
145
+ const requests = state.requests.get(message.type) || [];
146
+ const validRequests = requests.filter(time => time > now - this.config.windowMs);
147
+ // Update requests
148
+ state.requests.set(message.type, validRequests);
149
+ // Check if limit exceeded
150
+ if (validRequests.length >= this.config.maxRequests) {
151
+ state.blockedUntil = now + this.config.blockDurationMs;
152
+ return true;
153
+ }
154
+ // Add new request
155
+ validRequests.push(now);
156
+ return false;
157
+ }
158
+ recordRequest(clientId, message) {
159
+ const state = this.getClientState(clientId);
160
+ const requests = state.requests.get(message.type) || [];
161
+ requests.push(Date.now());
162
+ state.requests.set(message.type, requests);
163
+ }
164
+ getRemainingRequests(clientId, messageType) {
165
+ const state = this.getClientState(clientId);
166
+ const now = Date.now();
167
+ const requests = state.requests.get(messageType) || [];
168
+ const validRequests = requests.filter(time => time > now - this.config.windowMs);
169
+ return Math.max(0, this.config.maxRequests - validRequests.length);
170
+ }
171
+ isBlocked(clientId) {
172
+ const state = this.getClientState(clientId);
173
+ return !!state.blockedUntil && state.blockedUntil > Date.now();
174
+ }
175
+ }