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.
- package/LICENSE +1 -1
- package/README.md +151 -103
- package/dist/__mocks__/extendedWsMock.d.ts +35 -0
- package/dist/__mocks__/extendedWsMock.js +156 -0
- package/dist/__mocks__/logger.d.ts +9 -0
- package/dist/__mocks__/logger.js +6 -0
- package/dist/__mocks__/mockLogger.d.ts +41 -0
- package/dist/__mocks__/mockLogger.js +47 -0
- package/dist/__mocks__/mockserver.d.ts +31 -0
- package/dist/__mocks__/mockserver.js +39 -0
- package/dist/__mocks__/wsMock.d.ts +26 -0
- package/dist/__mocks__/wsMock.js +120 -0
- package/dist/client.d.ts +105 -0
- package/dist/client.js +105 -0
- package/dist/core/client.d.ts +94 -0
- package/dist/core/client.js +360 -0
- package/dist/core/nostr-server.d.ts +27 -0
- package/dist/core/nostr-server.js +95 -0
- package/dist/core/queue.d.ts +61 -0
- package/dist/core/queue.js +108 -0
- package/dist/core/server.d.ts +27 -0
- package/dist/core/server.js +114 -0
- package/dist/crypto/bech32.d.ts +26 -0
- package/dist/crypto/bech32.js +163 -0
- package/dist/crypto/handlers.d.ts +11 -0
- package/dist/crypto/handlers.js +36 -0
- package/dist/crypto/index.d.ts +5 -0
- package/dist/crypto/index.js +5 -0
- package/dist/crypto/schnorr.d.ts +16 -0
- package/dist/crypto/schnorr.js +51 -0
- package/dist/endpoints/metrics.d.ts +29 -0
- package/dist/endpoints/metrics.js +101 -0
- package/dist/index.d.ts +11 -6
- package/dist/index.js +16 -4
- package/dist/nips/index.d.ts +19 -0
- package/dist/nips/index.js +34 -0
- package/dist/nips/nip-01.d.ts +34 -0
- package/dist/nips/nip-01.js +145 -0
- package/dist/nips/nip-02.d.ts +83 -0
- package/dist/nips/nip-02.js +123 -0
- package/dist/nips/nip-04.d.ts +36 -0
- package/dist/nips/nip-04.js +105 -0
- package/dist/nips/nip-05.d.ts +86 -0
- package/dist/nips/nip-05.js +151 -0
- package/dist/nips/nip-09.d.ts +92 -0
- package/dist/nips/nip-09.js +190 -0
- package/dist/nips/nip-11.d.ts +64 -0
- package/dist/nips/nip-11.js +154 -0
- package/dist/nips/nip-13.d.ts +73 -0
- package/dist/nips/nip-13.js +128 -0
- package/dist/nips/nip-15.d.ts +83 -0
- package/dist/nips/nip-15.js +101 -0
- package/dist/nips/nip-16.d.ts +88 -0
- package/dist/nips/nip-16.js +150 -0
- package/dist/nips/nip-19.d.ts +28 -0
- package/dist/nips/nip-19.js +103 -0
- package/dist/nips/nip-20.d.ts +59 -0
- package/dist/nips/nip-20.js +95 -0
- package/dist/nips/nip-22.d.ts +89 -0
- package/dist/nips/nip-22.js +142 -0
- package/dist/nips/nip-26.d.ts +52 -0
- package/dist/nips/nip-26.js +139 -0
- package/dist/nips/nip-28.d.ts +103 -0
- package/dist/nips/nip-28.js +170 -0
- package/dist/nips/nip-33.d.ts +94 -0
- package/dist/nips/nip-33.js +133 -0
- package/dist/nostr-server.d.ts +23 -0
- package/dist/nostr-server.js +44 -0
- package/dist/server.d.ts +13 -3
- package/dist/server.js +60 -33
- package/dist/transport/base.d.ts +54 -0
- package/dist/transport/base.js +104 -0
- package/dist/transport/websocket.d.ts +22 -0
- package/dist/transport/websocket.js +122 -0
- package/dist/types/events.d.ts +63 -0
- package/dist/types/events.js +5 -0
- package/dist/types/filters.d.ts +19 -0
- package/dist/types/filters.js +5 -0
- package/dist/types/handlers.d.ts +80 -0
- package/dist/types/handlers.js +5 -0
- package/dist/types/index.d.ts +118 -39
- package/dist/types/index.js +21 -1
- package/dist/types/logger.d.ts +40 -0
- package/dist/types/logger.js +5 -0
- package/dist/types/messages.d.ts +135 -0
- package/dist/types/messages.js +40 -0
- package/dist/types/nostr.d.ts +120 -39
- package/dist/types/nostr.js +5 -10
- package/dist/types/options.d.ts +154 -0
- package/dist/types/options.js +5 -0
- package/dist/types/relays.d.ts +26 -0
- package/dist/types/relays.js +5 -0
- package/dist/types/scoring.d.ts +47 -0
- package/dist/types/scoring.js +29 -0
- package/dist/types/socket.d.ts +99 -0
- package/dist/types/socket.js +5 -0
- package/dist/types/transport.d.ts +97 -0
- package/dist/types/transport.js +5 -0
- package/dist/types/validation.d.ts +50 -0
- package/dist/types/validation.js +5 -0
- package/dist/types/websocket.d.ts +172 -0
- package/dist/types/websocket.js +5 -0
- package/dist/utils/http.d.ts +10 -0
- package/dist/utils/http.js +24 -0
- package/dist/utils/logger.d.ts +11 -2
- package/dist/utils/logger.js +18 -13
- package/dist/utils/metrics.d.ts +81 -0
- package/dist/utils/metrics.js +206 -0
- package/dist/utils/rate-limiter.d.ts +85 -0
- package/dist/utils/rate-limiter.js +175 -0
- package/package.json +18 -21
|
@@ -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
|
+
}
|
package/dist/utils/logger.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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;
|
package/dist/utils/logger.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
]
|
|
8
|
+
timestamp: true,
|
|
9
|
+
formatters: {
|
|
10
|
+
level: (label) => ({ level: label }),
|
|
11
|
+
bindings: (bindings) => bindings,
|
|
12
|
+
},
|
|
10
13
|
});
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
+
}
|