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,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file WebSocket server implementation
|
|
3
|
+
* @module core/server
|
|
4
|
+
*/
|
|
5
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
6
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
7
|
+
import { getLogger } from '../utils/logger';
|
|
8
|
+
import { createRateLimiter } from '../utils/rate-limiter';
|
|
9
|
+
const logger = getLogger('NostrWSServer');
|
|
10
|
+
/**
|
|
11
|
+
* NostrWSServer class for handling WebSocket connections
|
|
12
|
+
*/
|
|
13
|
+
export class NostrWSServer {
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.options = {
|
|
16
|
+
...options,
|
|
17
|
+
port: options.port || 8080,
|
|
18
|
+
host: options.host || 'localhost',
|
|
19
|
+
maxConnections: options.maxConnections || 1000,
|
|
20
|
+
pingInterval: options.pingInterval || 30000,
|
|
21
|
+
rateLimits: options.rateLimits
|
|
22
|
+
};
|
|
23
|
+
this.wss = new WebSocketServer({
|
|
24
|
+
port: this.options.port,
|
|
25
|
+
host: this.options.host
|
|
26
|
+
});
|
|
27
|
+
this.setupServer();
|
|
28
|
+
this.startPingInterval();
|
|
29
|
+
if (this.options.rateLimits) {
|
|
30
|
+
this.rateLimiter = createRateLimiter({
|
|
31
|
+
EVENT: this.options.rateLimits
|
|
32
|
+
}, logger);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Set up WebSocket server event handlers
|
|
37
|
+
*/
|
|
38
|
+
setupServer() {
|
|
39
|
+
this.wss.on('connection', async (ws) => {
|
|
40
|
+
const socket = ws;
|
|
41
|
+
socket.clientId = uuidv4();
|
|
42
|
+
socket.subscriptions = new Set();
|
|
43
|
+
socket.isAlive = true;
|
|
44
|
+
logger.info(`Client connected: ${socket.clientId}`);
|
|
45
|
+
if (this.wss.clients.size > this.options.maxConnections) {
|
|
46
|
+
logger.warn(`Max connections (${this.options.maxConnections}) reached`);
|
|
47
|
+
socket.close(1008, 'Max connections reached');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
socket.on('message', async (data) => {
|
|
51
|
+
try {
|
|
52
|
+
const message = JSON.parse(data.toString());
|
|
53
|
+
if (this.rateLimiter && await this.rateLimiter.shouldLimit(socket.clientId, message)) {
|
|
54
|
+
socket.send(JSON.stringify({
|
|
55
|
+
type: 'NOTICE',
|
|
56
|
+
data: { message: 'Rate limit exceeded' }
|
|
57
|
+
}));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
await this.options.onMessage?.(message, socket);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
logger.error('Error processing message:', error);
|
|
64
|
+
this.options.onError?.(error, socket);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
socket.on('error', (error) => {
|
|
68
|
+
logger.error(`Client error (${socket.clientId}):`, error);
|
|
69
|
+
this.options.onError?.(error, socket);
|
|
70
|
+
});
|
|
71
|
+
socket.on('close', () => {
|
|
72
|
+
logger.info(`Client disconnected: ${socket.clientId}`);
|
|
73
|
+
this.options.onClose?.(socket);
|
|
74
|
+
});
|
|
75
|
+
socket.on('pong', () => {
|
|
76
|
+
socket.isAlive = true;
|
|
77
|
+
});
|
|
78
|
+
await this.options.onConnection?.(socket);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Start ping interval to check client connections
|
|
83
|
+
*/
|
|
84
|
+
startPingInterval() {
|
|
85
|
+
if (this.options.pingInterval) {
|
|
86
|
+
this.pingInterval = setInterval(() => {
|
|
87
|
+
this.wss.clients.forEach((socket) => {
|
|
88
|
+
const nostrSocket = socket;
|
|
89
|
+
if (!nostrSocket.isAlive) {
|
|
90
|
+
nostrSocket.terminate();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
nostrSocket.isAlive = false;
|
|
94
|
+
nostrSocket.ping();
|
|
95
|
+
});
|
|
96
|
+
}, this.options.pingInterval);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Stop the server and clean up resources
|
|
101
|
+
*/
|
|
102
|
+
stop() {
|
|
103
|
+
if (this.pingInterval) {
|
|
104
|
+
clearInterval(this.pingInterval);
|
|
105
|
+
}
|
|
106
|
+
this.wss.clients.forEach((socket) => {
|
|
107
|
+
const nostrSocket = socket;
|
|
108
|
+
if (nostrSocket.readyState === WebSocket.OPEN) {
|
|
109
|
+
nostrSocket.close();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
this.wss.close();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Bech32 encoding/decoding utilities
|
|
3
|
+
* @module crypto/bech32
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Encode data to bech32
|
|
7
|
+
*/
|
|
8
|
+
export declare function bech32Encode(hrp: string, data: number[]): string;
|
|
9
|
+
/**
|
|
10
|
+
* Decode bech32 string
|
|
11
|
+
*/
|
|
12
|
+
export declare function bech32Decode(str: string): {
|
|
13
|
+
hrp: string;
|
|
14
|
+
data: number[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Encode hex string to bech32
|
|
18
|
+
*/
|
|
19
|
+
export declare function encodeToBech32(hrp: string, hex: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Decode bech32 to hex string
|
|
22
|
+
*/
|
|
23
|
+
export declare function decodeFromBech32(str: string): {
|
|
24
|
+
prefix: string;
|
|
25
|
+
hex: string;
|
|
26
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Bech32 encoding/decoding utilities
|
|
3
|
+
* @module crypto/bech32
|
|
4
|
+
*/
|
|
5
|
+
const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
|
6
|
+
const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
|
7
|
+
/**
|
|
8
|
+
* Convert 5-bit groups to base32
|
|
9
|
+
*/
|
|
10
|
+
function toChars(data) {
|
|
11
|
+
let result = '';
|
|
12
|
+
for (let i = 0; i < data.length; i++) {
|
|
13
|
+
result += CHARSET.charAt(data[i]);
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Convert string to 5-bit groups
|
|
19
|
+
*/
|
|
20
|
+
function fromChars(str) {
|
|
21
|
+
const result = [];
|
|
22
|
+
for (let i = 0; i < str.length; i++) {
|
|
23
|
+
const index = CHARSET.indexOf(str.charAt(i));
|
|
24
|
+
if (index === -1)
|
|
25
|
+
throw new Error(`Invalid character: ${str.charAt(i)}`);
|
|
26
|
+
result.push(index);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Compute checksum
|
|
32
|
+
*/
|
|
33
|
+
function polymod(values) {
|
|
34
|
+
let chk = 1;
|
|
35
|
+
for (let p = 0; p < values.length; p++) {
|
|
36
|
+
const top = chk >> 25;
|
|
37
|
+
chk = (chk & 0x1ffffff) << 5 ^ values[p];
|
|
38
|
+
for (let i = 0; i < 5; i++) {
|
|
39
|
+
if ((top >> i) & 1) {
|
|
40
|
+
chk ^= GENERATOR[i];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return chk;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Expand human-readable part
|
|
48
|
+
*/
|
|
49
|
+
function expandHRP(hrp) {
|
|
50
|
+
const result = [];
|
|
51
|
+
for (let i = 0; i < hrp.length; i++) {
|
|
52
|
+
result.push(hrp.charCodeAt(i) >> 5);
|
|
53
|
+
}
|
|
54
|
+
result.push(0);
|
|
55
|
+
for (let i = 0; i < hrp.length; i++) {
|
|
56
|
+
result.push(hrp.charCodeAt(i) & 31);
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Verify checksum
|
|
62
|
+
*/
|
|
63
|
+
function verifyChecksum(hrp, data) {
|
|
64
|
+
return polymod(expandHRP(hrp).concat(data)) === 1;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Create checksum
|
|
68
|
+
*/
|
|
69
|
+
function createChecksum(hrp, data) {
|
|
70
|
+
const values = expandHRP(hrp).concat(data).concat([0, 0, 0, 0, 0, 0]);
|
|
71
|
+
const mod = polymod(values) ^ 1;
|
|
72
|
+
const result = [];
|
|
73
|
+
for (let p = 0; p < 6; p++) {
|
|
74
|
+
result.push((mod >> 5 * (5 - p)) & 31);
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Convert data to 5-bit groups
|
|
80
|
+
*/
|
|
81
|
+
function convertBits(data, frombits, tobits, pad) {
|
|
82
|
+
let acc = 0;
|
|
83
|
+
let bits = 0;
|
|
84
|
+
const result = [];
|
|
85
|
+
const maxv = (1 << tobits) - 1;
|
|
86
|
+
for (let i = 0; i < data.length; i++) {
|
|
87
|
+
const value = data[i];
|
|
88
|
+
if (value < 0 || value >> frombits !== 0) {
|
|
89
|
+
throw new Error('Invalid value');
|
|
90
|
+
}
|
|
91
|
+
acc = (acc << frombits) | value;
|
|
92
|
+
bits += frombits;
|
|
93
|
+
while (bits >= tobits) {
|
|
94
|
+
bits -= tobits;
|
|
95
|
+
result.push((acc >> bits) & maxv);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (pad) {
|
|
99
|
+
if (bits > 0) {
|
|
100
|
+
result.push((acc << (tobits - bits)) & maxv);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (bits >= frombits || ((acc << (tobits - bits)) & maxv)) {
|
|
104
|
+
throw new Error('Invalid padding');
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Encode data to bech32
|
|
110
|
+
*/
|
|
111
|
+
export function bech32Encode(hrp, data) {
|
|
112
|
+
const combined = data.concat(createChecksum(hrp, data));
|
|
113
|
+
return hrp + '1' + toChars(combined);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Decode bech32 string
|
|
117
|
+
*/
|
|
118
|
+
export function bech32Decode(str) {
|
|
119
|
+
if (str.length < 8 || str.length > 90) {
|
|
120
|
+
throw new Error('Invalid length');
|
|
121
|
+
}
|
|
122
|
+
let lower = false;
|
|
123
|
+
let upper = false;
|
|
124
|
+
for (let i = 0; i < str.length; i++) {
|
|
125
|
+
if (str.charCodeAt(i) < 33 || str.charCodeAt(i) > 126) {
|
|
126
|
+
throw new Error('Invalid character');
|
|
127
|
+
}
|
|
128
|
+
if (str.charCodeAt(i) >= 97 && str.charCodeAt(i) <= 122)
|
|
129
|
+
lower = true;
|
|
130
|
+
if (str.charCodeAt(i) >= 65 && str.charCodeAt(i) <= 90)
|
|
131
|
+
upper = true;
|
|
132
|
+
}
|
|
133
|
+
if (lower && upper) {
|
|
134
|
+
throw new Error('Mixed case');
|
|
135
|
+
}
|
|
136
|
+
const pos = str.lastIndexOf('1');
|
|
137
|
+
if (pos < 1 || pos + 7 > str.length) {
|
|
138
|
+
throw new Error('Invalid separator position');
|
|
139
|
+
}
|
|
140
|
+
const hrp = str.substring(0, pos).toLowerCase();
|
|
141
|
+
const data = fromChars(str.substring(pos + 1));
|
|
142
|
+
if (!verifyChecksum(hrp, data)) {
|
|
143
|
+
throw new Error('Invalid checksum');
|
|
144
|
+
}
|
|
145
|
+
return { hrp, data: data.slice(0, -6) };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Encode hex string to bech32
|
|
149
|
+
*/
|
|
150
|
+
export function encodeToBech32(hrp, hex) {
|
|
151
|
+
const data = new Uint8Array(hex.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []);
|
|
152
|
+
const words = convertBits(Array.from(data), 8, 5, true);
|
|
153
|
+
return bech32Encode(hrp, words);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Decode bech32 to hex string
|
|
157
|
+
*/
|
|
158
|
+
export function decodeFromBech32(str) {
|
|
159
|
+
const { hrp, data } = bech32Decode(str);
|
|
160
|
+
const bytes = convertBits(data, 5, 8, false);
|
|
161
|
+
const hex = bytes.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
162
|
+
return { prefix: hrp, hex };
|
|
163
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Crypto handlers for Nostr messages
|
|
3
|
+
* @module crypto/handlers
|
|
4
|
+
*/
|
|
5
|
+
import type { NostrWSMessage } from '../types/messages';
|
|
6
|
+
/**
|
|
7
|
+
* Validates a signed message
|
|
8
|
+
* @param message - Message to validate
|
|
9
|
+
* @returns Promise resolving to true if message is valid
|
|
10
|
+
*/
|
|
11
|
+
export declare function validateSignedMessage(message: NostrWSMessage): Promise<boolean>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Crypto handlers for Nostr messages
|
|
3
|
+
* @module crypto/handlers
|
|
4
|
+
*/
|
|
5
|
+
import { getLogger } from '../utils/logger';
|
|
6
|
+
import { MESSAGE_TYPES } from '../types/messages';
|
|
7
|
+
import { validateEvent, verifySignature } from 'nostr-crypto-utils';
|
|
8
|
+
const logger = getLogger('crypto');
|
|
9
|
+
/**
|
|
10
|
+
* Validates a signed message
|
|
11
|
+
* @param message - Message to validate
|
|
12
|
+
* @returns Promise resolving to true if message is valid
|
|
13
|
+
*/
|
|
14
|
+
export async function validateSignedMessage(message) {
|
|
15
|
+
try {
|
|
16
|
+
if (message.type !== MESSAGE_TYPES.EVENT || !message.data) {
|
|
17
|
+
logger.debug('Invalid message format');
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const event = message.data;
|
|
21
|
+
if (!validateEvent(event)) {
|
|
22
|
+
logger.debug('Invalid event format');
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const isValid = await verifySignature(event);
|
|
26
|
+
if (!isValid) {
|
|
27
|
+
logger.debug('Invalid signature');
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
logger.error('Error validating signed message:', error);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Schnorr signature implementation for Nostr
|
|
3
|
+
* @module crypto/schnorr
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Sign a message using Schnorr signature
|
|
7
|
+
*/
|
|
8
|
+
export declare function schnorrSign(message: string, privateKey: Uint8Array): Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Verify a Schnorr signature
|
|
11
|
+
*/
|
|
12
|
+
export declare function schnorrVerify(signature: string, message: string, publicKey: string): Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* Get public key from private key
|
|
15
|
+
*/
|
|
16
|
+
export declare function getPublicKey(privateKey: string): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Schnorr signature implementation for Nostr
|
|
3
|
+
* @module crypto/schnorr
|
|
4
|
+
*/
|
|
5
|
+
import { utils } from '@noble/secp256k1';
|
|
6
|
+
import * as secp from '@noble/secp256k1';
|
|
7
|
+
/**
|
|
8
|
+
* Sign a message using Schnorr signature
|
|
9
|
+
*/
|
|
10
|
+
export async function schnorrSign(message, privateKey) {
|
|
11
|
+
try {
|
|
12
|
+
// Hash the message
|
|
13
|
+
const messageHash = utils.sha256(Buffer.from(message));
|
|
14
|
+
// Sign using Schnorr
|
|
15
|
+
const signature = await secp.sign(messageHash, privateKey, { canonical: true });
|
|
16
|
+
// Convert to hex
|
|
17
|
+
return utils.bytesToHex(signature);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
throw new Error(`Failed to create Schnorr signature: ${error}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Verify a Schnorr signature
|
|
25
|
+
*/
|
|
26
|
+
export async function schnorrVerify(signature, message, publicKey) {
|
|
27
|
+
try {
|
|
28
|
+
// Convert inputs to bytes
|
|
29
|
+
const signatureBytes = utils.hexToBytes(signature);
|
|
30
|
+
const messageHash = utils.sha256(Buffer.from(message));
|
|
31
|
+
const publicKeyBytes = utils.hexToBytes(publicKey);
|
|
32
|
+
// Verify using Schnorr
|
|
33
|
+
return await secp.verify(signatureBytes, messageHash, publicKeyBytes, { canonical: true });
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new Error(`Failed to verify Schnorr signature: ${error}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get public key from private key
|
|
41
|
+
*/
|
|
42
|
+
export function getPublicKey(privateKey) {
|
|
43
|
+
try {
|
|
44
|
+
const publicKey = secp.getPublicKey(privateKey, true);
|
|
45
|
+
return Buffer.from(publicKey).toString('hex');
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
49
|
+
throw new Error(`Failed to get public key: ${errorMessage}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Metrics endpoint for Nostr WebSocket server
|
|
3
|
+
* @module endpoints/metrics
|
|
4
|
+
*/
|
|
5
|
+
export interface MetricsEndpointOptions {
|
|
6
|
+
port?: number;
|
|
7
|
+
host?: string;
|
|
8
|
+
path?: string;
|
|
9
|
+
auth?: {
|
|
10
|
+
username: string;
|
|
11
|
+
password: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export declare class MetricsEndpoint {
|
|
15
|
+
private server;
|
|
16
|
+
private options;
|
|
17
|
+
constructor(options?: MetricsEndpointOptions);
|
|
18
|
+
private checkAuth;
|
|
19
|
+
private handleRequest;
|
|
20
|
+
/**
|
|
21
|
+
* Start the metrics endpoint server
|
|
22
|
+
*/
|
|
23
|
+
start(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Stop the metrics endpoint server
|
|
26
|
+
*/
|
|
27
|
+
stop(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare const metricsEndpoint: MetricsEndpoint;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Metrics endpoint for Nostr WebSocket server
|
|
3
|
+
* @module endpoints/metrics
|
|
4
|
+
*/
|
|
5
|
+
import { createServer } from 'http';
|
|
6
|
+
import { metricsTracker } from '../utils/metrics';
|
|
7
|
+
import { getLogger } from '../utils/logger';
|
|
8
|
+
const logger = getLogger('MetricsEndpoint');
|
|
9
|
+
export class MetricsEndpoint {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.options = {
|
|
12
|
+
port: options.port || 9100,
|
|
13
|
+
host: options.host || 'localhost',
|
|
14
|
+
path: options.path || '/metrics',
|
|
15
|
+
auth: options.auth || { username: '', password: '' }
|
|
16
|
+
};
|
|
17
|
+
this.server = createServer((req, res) => this.handleRequest(req, res));
|
|
18
|
+
}
|
|
19
|
+
checkAuth(req) {
|
|
20
|
+
if (!this.options.auth.username)
|
|
21
|
+
return true;
|
|
22
|
+
const authHeader = req.headers.authorization || '';
|
|
23
|
+
const [type, credentials] = authHeader.split(' ');
|
|
24
|
+
if (type !== 'Basic')
|
|
25
|
+
return false;
|
|
26
|
+
const [username, password] = Buffer.from(credentials, 'base64')
|
|
27
|
+
.toString()
|
|
28
|
+
.split(':');
|
|
29
|
+
return username === this.options.auth.username &&
|
|
30
|
+
password === this.options.auth.password;
|
|
31
|
+
}
|
|
32
|
+
async handleRequest(req, res) {
|
|
33
|
+
try {
|
|
34
|
+
// Only handle GET requests to metrics path
|
|
35
|
+
if (req.method !== 'GET' || req.url !== this.options.path) {
|
|
36
|
+
res.writeHead(404);
|
|
37
|
+
res.end('Not Found');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Check authentication if configured
|
|
41
|
+
if (!this.checkAuth(req)) {
|
|
42
|
+
res.writeHead(401, {
|
|
43
|
+
'WWW-Authenticate': 'Basic realm="Nostr Metrics"'
|
|
44
|
+
});
|
|
45
|
+
res.end('Unauthorized');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Get metrics in Prometheus format
|
|
49
|
+
const metrics = metricsTracker.getPrometheusMetrics();
|
|
50
|
+
// Send response
|
|
51
|
+
res.writeHead(200, {
|
|
52
|
+
'Content-Type': 'text/plain; version=0.0.4',
|
|
53
|
+
'Cache-Control': 'no-cache'
|
|
54
|
+
});
|
|
55
|
+
res.end(metrics);
|
|
56
|
+
logger.info({
|
|
57
|
+
method: req.method,
|
|
58
|
+
path: req.url,
|
|
59
|
+
status: 200,
|
|
60
|
+
metrics: metrics.split('\n').length
|
|
61
|
+
}, 'Metrics request served');
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
logger.error({ error }, 'Error serving metrics');
|
|
65
|
+
res.writeHead(500);
|
|
66
|
+
res.end('Internal Server Error');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Start the metrics endpoint server
|
|
71
|
+
*/
|
|
72
|
+
start() {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
this.server.listen(this.options.port, this.options.host, () => {
|
|
75
|
+
logger.info({
|
|
76
|
+
host: this.options.host,
|
|
77
|
+
port: this.options.port,
|
|
78
|
+
path: this.options.path
|
|
79
|
+
}, 'Metrics endpoint started');
|
|
80
|
+
resolve();
|
|
81
|
+
}).on('error', reject);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Stop the metrics endpoint server
|
|
86
|
+
*/
|
|
87
|
+
stop() {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
this.server.close((err) => {
|
|
90
|
+
if (err)
|
|
91
|
+
reject(err);
|
|
92
|
+
else {
|
|
93
|
+
logger.info('Metrics endpoint stopped');
|
|
94
|
+
resolve();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Export singleton instance with default configuration
|
|
101
|
+
export const metricsEndpoint = new MetricsEndpoint();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file Main entry point for the nostr-websocket-utils library
|
|
3
|
+
* @module nostr-websocket-utils
|
|
4
|
+
*/
|
|
5
|
+
export { NostrWSClient } from './core/client.js';
|
|
6
|
+
export { NostrWSServer } from './core/server.js';
|
|
7
|
+
export { NostrWSServer as NostrServer } from './core/nostr-server.js';
|
|
8
|
+
export { createWSServer as createServer } from './core/nostr-server.js';
|
|
5
9
|
export { getLogger } from './utils/logger.js';
|
|
6
|
-
export
|
|
7
|
-
export
|
|
10
|
+
export * from './crypto';
|
|
11
|
+
export * from './nips';
|
|
12
|
+
export * from './types';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file Main entry point for the nostr-websocket-utils library
|
|
3
|
+
* @module nostr-websocket-utils
|
|
4
|
+
*/
|
|
5
|
+
// Core functionality
|
|
6
|
+
export { NostrWSClient } from './core/client.js';
|
|
7
|
+
export { NostrWSServer } from './core/server.js';
|
|
8
|
+
export { NostrWSServer as NostrServer } from './core/nostr-server.js';
|
|
9
|
+
export { createWSServer as createServer } from './core/nostr-server.js';
|
|
10
|
+
// Utilities
|
|
5
11
|
export { getLogger } from './utils/logger.js';
|
|
12
|
+
// Crypto operations
|
|
13
|
+
export * from './crypto';
|
|
14
|
+
// NIP implementations
|
|
15
|
+
export * from './nips';
|
|
16
|
+
// Type definitions
|
|
17
|
+
export * from './types';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIPs implementation index
|
|
3
|
+
* @module nips
|
|
4
|
+
*/
|
|
5
|
+
export * from './nip-01';
|
|
6
|
+
export * from './nip-02';
|
|
7
|
+
export * from './nip-04';
|
|
8
|
+
export * from './nip-05';
|
|
9
|
+
export * from './nip-09';
|
|
10
|
+
export * from './nip-19';
|
|
11
|
+
export * from './nip-26';
|
|
12
|
+
export * from './nip-11';
|
|
13
|
+
export * from './nip-20';
|
|
14
|
+
export * from './nip-13';
|
|
15
|
+
export * from './nip-16';
|
|
16
|
+
export * from './nip-15';
|
|
17
|
+
export * from './nip-22';
|
|
18
|
+
export * from './nip-28';
|
|
19
|
+
export * from './nip-33';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIPs implementation index
|
|
3
|
+
* @module nips
|
|
4
|
+
*/
|
|
5
|
+
// Basic protocol flow
|
|
6
|
+
export * from './nip-01';
|
|
7
|
+
// Contact List and Petnames
|
|
8
|
+
export * from './nip-02';
|
|
9
|
+
// Encrypted Direct Messages
|
|
10
|
+
export * from './nip-04';
|
|
11
|
+
// DNS Identity Verification
|
|
12
|
+
export * from './nip-05';
|
|
13
|
+
// Event Deletion
|
|
14
|
+
export * from './nip-09';
|
|
15
|
+
// bech32-encoded entities
|
|
16
|
+
export * from './nip-19';
|
|
17
|
+
// Delegated Event Signing
|
|
18
|
+
export * from './nip-26';
|
|
19
|
+
// Relay Information Document
|
|
20
|
+
export * from './nip-11';
|
|
21
|
+
// Command Results
|
|
22
|
+
export * from './nip-20';
|
|
23
|
+
// Proof of Work
|
|
24
|
+
export * from './nip-13';
|
|
25
|
+
// Event Treatment
|
|
26
|
+
export * from './nip-16';
|
|
27
|
+
// End of Stored Events Notice
|
|
28
|
+
export * from './nip-15';
|
|
29
|
+
// Event Created At Limits
|
|
30
|
+
export * from './nip-22';
|
|
31
|
+
// Public Chat
|
|
32
|
+
export * from './nip-28';
|
|
33
|
+
// Parameterized Replaceable Events
|
|
34
|
+
export * from './nip-33';
|