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,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIP-28: Public Chat
|
|
3
|
+
* @module nips/nip-28
|
|
4
|
+
* @see https://github.com/nostr-protocol/nips/blob/master/28.md
|
|
5
|
+
*/
|
|
6
|
+
import type { NostrWSMessage } from '../types/messages';
|
|
7
|
+
import type { Logger } from '../types/logger';
|
|
8
|
+
/**
|
|
9
|
+
* Chat event kinds
|
|
10
|
+
*/
|
|
11
|
+
export declare const ChatEventKinds: {
|
|
12
|
+
readonly CHANNEL_CREATION: 40;
|
|
13
|
+
readonly CHANNEL_METADATA: 41;
|
|
14
|
+
readonly CHANNEL_MESSAGE: 42;
|
|
15
|
+
readonly CHANNEL_HIDE_MESSAGE: 43;
|
|
16
|
+
readonly CHANNEL_MUTE_USER: 44;
|
|
17
|
+
readonly USER_MUTE: 45;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Channel metadata structure
|
|
21
|
+
*/
|
|
22
|
+
export interface ChannelMetadata {
|
|
23
|
+
name: string;
|
|
24
|
+
about?: string;
|
|
25
|
+
picture?: string;
|
|
26
|
+
rules?: string[];
|
|
27
|
+
moderators?: string[];
|
|
28
|
+
pinned?: string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Creates a channel creation message
|
|
32
|
+
* @param metadata - Channel metadata
|
|
33
|
+
* @returns {NostrWSMessage} Channel creation event
|
|
34
|
+
*/
|
|
35
|
+
export declare function createChannelCreationEvent(metadata: ChannelMetadata): NostrWSMessage;
|
|
36
|
+
/**
|
|
37
|
+
* Creates a channel message
|
|
38
|
+
* @param channelId - Channel ID
|
|
39
|
+
* @param content - Message content
|
|
40
|
+
* @param replyTo - Optional ID of message being replied to
|
|
41
|
+
* @returns {NostrWSMessage} Channel message event
|
|
42
|
+
*/
|
|
43
|
+
export declare function createChannelMessage(channelId: string, content: string, replyTo?: string): NostrWSMessage;
|
|
44
|
+
/**
|
|
45
|
+
* Creates a message moderation event
|
|
46
|
+
* @param channelId - Channel ID
|
|
47
|
+
* @param messageId - Message ID to moderate
|
|
48
|
+
* @param reason - Moderation reason
|
|
49
|
+
* @returns {NostrWSMessage} Hide message event
|
|
50
|
+
*/
|
|
51
|
+
export declare function createHideMessageEvent(channelId: string, messageId: string, reason: string): NostrWSMessage;
|
|
52
|
+
/**
|
|
53
|
+
* Chat message handler interface
|
|
54
|
+
*/
|
|
55
|
+
export interface ChatMessageHandler {
|
|
56
|
+
/**
|
|
57
|
+
* Handles incoming chat message
|
|
58
|
+
* @param message - Chat message
|
|
59
|
+
* @returns {Promise<void>}
|
|
60
|
+
*/
|
|
61
|
+
handleMessage(message: NostrWSMessage): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Handles message moderation
|
|
64
|
+
* @param message - Moderation message
|
|
65
|
+
* @returns {Promise<void>}
|
|
66
|
+
*/
|
|
67
|
+
handleModeration(message: NostrWSMessage): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Creates a chat message handler
|
|
71
|
+
* @param logger - Logger instance
|
|
72
|
+
* @returns {ChatMessageHandler} Message handler
|
|
73
|
+
*/
|
|
74
|
+
export declare function createChatMessageHandler(logger: Logger): ChatMessageHandler;
|
|
75
|
+
/**
|
|
76
|
+
* Channel subscription manager interface
|
|
77
|
+
*/
|
|
78
|
+
export interface ChannelSubscriptionManager {
|
|
79
|
+
/**
|
|
80
|
+
* Subscribes to a channel
|
|
81
|
+
* @param channelId - Channel ID
|
|
82
|
+
* @returns {NostrWSMessage} Subscription message
|
|
83
|
+
*/
|
|
84
|
+
subscribe(channelId: string): NostrWSMessage;
|
|
85
|
+
/**
|
|
86
|
+
* Unsubscribes from a channel
|
|
87
|
+
* @param channelId - Channel ID
|
|
88
|
+
* @returns {NostrWSMessage} Unsubscribe message
|
|
89
|
+
*/
|
|
90
|
+
unsubscribe(channelId: string): NostrWSMessage;
|
|
91
|
+
/**
|
|
92
|
+
* Gets channel metadata
|
|
93
|
+
* @param channelId - Channel ID
|
|
94
|
+
* @returns {Promise<ChannelMetadata | undefined>} Channel metadata
|
|
95
|
+
*/
|
|
96
|
+
getMetadata(channelId: string): Promise<ChannelMetadata | undefined>;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Creates a channel subscription manager
|
|
100
|
+
* @param logger - Logger instance
|
|
101
|
+
* @returns {ChannelSubscriptionManager} Subscription manager
|
|
102
|
+
*/
|
|
103
|
+
export declare function createChannelSubscriptionManager(logger: Logger): ChannelSubscriptionManager;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIP-28: Public Chat
|
|
3
|
+
* @module nips/nip-28
|
|
4
|
+
* @see https://github.com/nostr-protocol/nips/blob/master/28.md
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Chat event kinds
|
|
8
|
+
*/
|
|
9
|
+
export const ChatEventKinds = {
|
|
10
|
+
CHANNEL_CREATION: 40,
|
|
11
|
+
CHANNEL_METADATA: 41,
|
|
12
|
+
CHANNEL_MESSAGE: 42,
|
|
13
|
+
CHANNEL_HIDE_MESSAGE: 43,
|
|
14
|
+
CHANNEL_MUTE_USER: 44,
|
|
15
|
+
USER_MUTE: 45
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Creates a channel creation message
|
|
19
|
+
* @param metadata - Channel metadata
|
|
20
|
+
* @returns {NostrWSMessage} Channel creation event
|
|
21
|
+
*/
|
|
22
|
+
export function createChannelCreationEvent(metadata) {
|
|
23
|
+
return {
|
|
24
|
+
type: 'EVENT',
|
|
25
|
+
data: {
|
|
26
|
+
kind: ChatEventKinds.CHANNEL_CREATION,
|
|
27
|
+
content: JSON.stringify(metadata),
|
|
28
|
+
tags: []
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Creates a channel message
|
|
34
|
+
* @param channelId - Channel ID
|
|
35
|
+
* @param content - Message content
|
|
36
|
+
* @param replyTo - Optional ID of message being replied to
|
|
37
|
+
* @returns {NostrWSMessage} Channel message event
|
|
38
|
+
*/
|
|
39
|
+
export function createChannelMessage(channelId, content, replyTo) {
|
|
40
|
+
const tags = [['e', channelId, '', 'root']];
|
|
41
|
+
if (replyTo) {
|
|
42
|
+
tags.push(['e', replyTo, '', 'reply']);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
type: 'EVENT',
|
|
46
|
+
data: {
|
|
47
|
+
kind: ChatEventKinds.CHANNEL_MESSAGE,
|
|
48
|
+
content,
|
|
49
|
+
tags
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Creates a message moderation event
|
|
55
|
+
* @param channelId - Channel ID
|
|
56
|
+
* @param messageId - Message ID to moderate
|
|
57
|
+
* @param reason - Moderation reason
|
|
58
|
+
* @returns {NostrWSMessage} Hide message event
|
|
59
|
+
*/
|
|
60
|
+
export function createHideMessageEvent(channelId, messageId, reason) {
|
|
61
|
+
return {
|
|
62
|
+
type: 'EVENT',
|
|
63
|
+
data: {
|
|
64
|
+
kind: ChatEventKinds.CHANNEL_HIDE_MESSAGE,
|
|
65
|
+
content: reason,
|
|
66
|
+
tags: [
|
|
67
|
+
['e', channelId, '', 'root'],
|
|
68
|
+
['e', messageId, '', 'reply']
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Creates a chat message handler
|
|
75
|
+
* @param logger - Logger instance
|
|
76
|
+
* @returns {ChatMessageHandler} Message handler
|
|
77
|
+
*/
|
|
78
|
+
export function createChatMessageHandler(logger) {
|
|
79
|
+
return {
|
|
80
|
+
async handleMessage(message) {
|
|
81
|
+
try {
|
|
82
|
+
if (message.type !== 'EVENT' || !message.data)
|
|
83
|
+
return;
|
|
84
|
+
const event = message.data;
|
|
85
|
+
if (event.kind !== ChatEventKinds.CHANNEL_MESSAGE)
|
|
86
|
+
return;
|
|
87
|
+
// Extract channel ID and reply ID from tags
|
|
88
|
+
const tags = event.tags;
|
|
89
|
+
const channelId = tags.find(tag => tag[0] === 'e' && tag[3] === 'root')?.[1];
|
|
90
|
+
const replyId = tags.find(tag => tag[0] === 'e' && tag[3] === 'reply')?.[1];
|
|
91
|
+
// Process message
|
|
92
|
+
logger.debug('Processing chat message', {
|
|
93
|
+
channelId,
|
|
94
|
+
replyId,
|
|
95
|
+
content: event.content
|
|
96
|
+
});
|
|
97
|
+
// Additional message processing logic here
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
logger.error('Error handling chat message:', error);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
async handleModeration(message) {
|
|
104
|
+
try {
|
|
105
|
+
if (message.type !== 'EVENT' || !message.data)
|
|
106
|
+
return;
|
|
107
|
+
const event = message.data;
|
|
108
|
+
if (event.kind !== ChatEventKinds.CHANNEL_HIDE_MESSAGE)
|
|
109
|
+
return;
|
|
110
|
+
// Extract channel and message IDs
|
|
111
|
+
const tags = event.tags;
|
|
112
|
+
const channelId = tags.find(tag => tag[0] === 'e' && tag[3] === 'root')?.[1];
|
|
113
|
+
const messageId = tags.find(tag => tag[0] === 'e' && tag[3] === 'reply')?.[1];
|
|
114
|
+
// Process moderation
|
|
115
|
+
logger.debug('Processing message moderation', {
|
|
116
|
+
channelId,
|
|
117
|
+
messageId,
|
|
118
|
+
reason: event.content
|
|
119
|
+
});
|
|
120
|
+
// Additional moderation logic here
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
logger.error('Error handling message moderation:', error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Creates a channel subscription manager
|
|
130
|
+
* @param logger - Logger instance
|
|
131
|
+
* @returns {ChannelSubscriptionManager} Subscription manager
|
|
132
|
+
*/
|
|
133
|
+
export function createChannelSubscriptionManager(logger) {
|
|
134
|
+
const subscriptions = new Map(); // channelId -> subscriptionId
|
|
135
|
+
const metadata = new Map();
|
|
136
|
+
return {
|
|
137
|
+
subscribe(channelId) {
|
|
138
|
+
const subscriptionId = `chat:${channelId}:${Date.now()}`;
|
|
139
|
+
subscriptions.set(channelId, subscriptionId);
|
|
140
|
+
return {
|
|
141
|
+
type: 'REQ',
|
|
142
|
+
data: {
|
|
143
|
+
subscription_id: subscriptionId,
|
|
144
|
+
filter: {
|
|
145
|
+
kinds: [
|
|
146
|
+
ChatEventKinds.CHANNEL_MESSAGE,
|
|
147
|
+
ChatEventKinds.CHANNEL_HIDE_MESSAGE
|
|
148
|
+
],
|
|
149
|
+
'#e': [channelId]
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
unsubscribe(channelId) {
|
|
155
|
+
const subscriptionId = subscriptions.get(channelId);
|
|
156
|
+
if (!subscriptionId) {
|
|
157
|
+
logger.debug(`No subscription found for channel ${channelId}`);
|
|
158
|
+
return { type: 'CLOSE', data: { subscription_id: '' } };
|
|
159
|
+
}
|
|
160
|
+
subscriptions.delete(channelId);
|
|
161
|
+
return {
|
|
162
|
+
type: 'CLOSE',
|
|
163
|
+
data: { subscription_id: subscriptionId }
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
async getMetadata(channelId) {
|
|
167
|
+
return metadata.get(channelId);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIP-33: Parameterized Replaceable Events
|
|
3
|
+
* @module nips/nip-33
|
|
4
|
+
* @see https://github.com/nostr-protocol/nips/blob/master/33.md
|
|
5
|
+
*/
|
|
6
|
+
import type { NostrWSMessage } from '../types/messages';
|
|
7
|
+
import type { Logger } from '../types/logger';
|
|
8
|
+
/**
|
|
9
|
+
* Parameterized replaceable event kinds (30000-39999)
|
|
10
|
+
*/
|
|
11
|
+
export declare const PARAMETERIZED_REPLACEABLE_KINDS: {
|
|
12
|
+
CATEGORIZED_BOOKMARK: number;
|
|
13
|
+
CATEGORIZED_HIGHLIGHT: number;
|
|
14
|
+
CATEGORIZED_PEOPLE_LIST: number;
|
|
15
|
+
PROFILE_BADGES: number;
|
|
16
|
+
BADGE_DEFINITION: number;
|
|
17
|
+
CUSTOM_START: number;
|
|
18
|
+
CUSTOM_END: number;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Creates a parameterized replaceable event
|
|
22
|
+
* @param kind - Event kind
|
|
23
|
+
* @param content - Event content
|
|
24
|
+
* @param identifier - Unique identifier for the parameter
|
|
25
|
+
* @param additionalTags - Additional tags
|
|
26
|
+
* @returns {NostrWSMessage} Parameterized replaceable event
|
|
27
|
+
*/
|
|
28
|
+
export declare function createParameterizedEvent(kind: number, content: string, identifier: string, additionalTags?: string[][]): NostrWSMessage;
|
|
29
|
+
/**
|
|
30
|
+
* Validates a parameterized replaceable event
|
|
31
|
+
* @param message - Message to validate
|
|
32
|
+
* @param _logger - Logger instance
|
|
33
|
+
* @returns {boolean} True if valid
|
|
34
|
+
*/
|
|
35
|
+
export declare function validateParameterizedEvent(message: NostrWSMessage, _logger: Logger): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Parameterized event manager interface
|
|
38
|
+
*/
|
|
39
|
+
export interface ParameterizedEventManager {
|
|
40
|
+
/**
|
|
41
|
+
* Creates a new parameterized event
|
|
42
|
+
* @param kind - Event kind
|
|
43
|
+
* @param content - Event content
|
|
44
|
+
* @param identifier - Parameter identifier
|
|
45
|
+
* @param tags - Additional tags
|
|
46
|
+
* @returns {NostrWSMessage} Created event
|
|
47
|
+
*/
|
|
48
|
+
createEvent(kind: number, content: string, identifier: string, tags?: string[][]): NostrWSMessage;
|
|
49
|
+
/**
|
|
50
|
+
* Updates an existing parameterized event
|
|
51
|
+
* @param kind - Event kind
|
|
52
|
+
* @param identifier - Parameter identifier
|
|
53
|
+
* @param content - New content
|
|
54
|
+
* @returns {NostrWSMessage} Update event
|
|
55
|
+
*/
|
|
56
|
+
updateEvent(kind: number, identifier: string, content: string): NostrWSMessage;
|
|
57
|
+
/**
|
|
58
|
+
* Subscribes to parameterized events
|
|
59
|
+
* @param kinds - Event kinds to subscribe to
|
|
60
|
+
* @param identifiers - Parameter identifiers
|
|
61
|
+
* @returns {NostrWSMessage} Subscription message
|
|
62
|
+
*/
|
|
63
|
+
subscribe(kinds: number[], identifiers: string[]): NostrWSMessage;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Creates a parameterized event manager
|
|
67
|
+
* @param _logger - Logger instance
|
|
68
|
+
* @returns {ParameterizedEventManager} Event manager
|
|
69
|
+
*/
|
|
70
|
+
export declare function createParameterizedEventManager(_logger: Logger): ParameterizedEventManager;
|
|
71
|
+
/**
|
|
72
|
+
* Event replacement handler interface
|
|
73
|
+
*/
|
|
74
|
+
export interface EventReplacementHandler {
|
|
75
|
+
/**
|
|
76
|
+
* Checks if an event should replace another
|
|
77
|
+
* @param newEvent - New event
|
|
78
|
+
* @param existingEvent - Existing event
|
|
79
|
+
* @returns {boolean} True if should replace
|
|
80
|
+
*/
|
|
81
|
+
shouldReplace(newEvent: Record<string, unknown>, existingEvent: Record<string, unknown>): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Gets replacement key for an event
|
|
84
|
+
* @param event - Event to get key for
|
|
85
|
+
* @returns {string} Replacement key
|
|
86
|
+
*/
|
|
87
|
+
getReplacementKey(event: Record<string, unknown>): string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Creates an event replacement handler
|
|
91
|
+
* @param _logger - Logger instance
|
|
92
|
+
* @returns {EventReplacementHandler} Replacement handler
|
|
93
|
+
*/
|
|
94
|
+
export declare function createEventReplacementHandler(_logger: Logger): EventReplacementHandler;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIP-33: Parameterized Replaceable Events
|
|
3
|
+
* @module nips/nip-33
|
|
4
|
+
* @see https://github.com/nostr-protocol/nips/blob/master/33.md
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Parameterized replaceable event kinds (30000-39999)
|
|
8
|
+
*/
|
|
9
|
+
export const PARAMETERIZED_REPLACEABLE_KINDS = {
|
|
10
|
+
CATEGORIZED_BOOKMARK: 30000,
|
|
11
|
+
CATEGORIZED_HIGHLIGHT: 30001,
|
|
12
|
+
CATEGORIZED_PEOPLE_LIST: 30002,
|
|
13
|
+
PROFILE_BADGES: 30008,
|
|
14
|
+
BADGE_DEFINITION: 30009,
|
|
15
|
+
CUSTOM_START: 31000,
|
|
16
|
+
CUSTOM_END: 39999
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Creates a parameterized replaceable event
|
|
20
|
+
* @param kind - Event kind
|
|
21
|
+
* @param content - Event content
|
|
22
|
+
* @param identifier - Unique identifier for the parameter
|
|
23
|
+
* @param additionalTags - Additional tags
|
|
24
|
+
* @returns {NostrWSMessage} Parameterized replaceable event
|
|
25
|
+
*/
|
|
26
|
+
export function createParameterizedEvent(kind, content, identifier, additionalTags = []) {
|
|
27
|
+
return {
|
|
28
|
+
type: 'EVENT',
|
|
29
|
+
data: {
|
|
30
|
+
kind,
|
|
31
|
+
content,
|
|
32
|
+
tags: [
|
|
33
|
+
['d', identifier],
|
|
34
|
+
...additionalTags
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Validates a parameterized replaceable event
|
|
41
|
+
* @param message - Message to validate
|
|
42
|
+
* @param _logger - Logger instance
|
|
43
|
+
* @returns {boolean} True if valid
|
|
44
|
+
*/
|
|
45
|
+
export function validateParameterizedEvent(message, _logger) {
|
|
46
|
+
try {
|
|
47
|
+
if (message.type !== 'EVENT' || !message.data) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const event = message.data;
|
|
51
|
+
const kind = event.kind;
|
|
52
|
+
// Check if kind is in valid range
|
|
53
|
+
if (kind < 30000 || kind > 39999) {
|
|
54
|
+
_logger.debug('Invalid kind for parameterized replaceable event');
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
// Must have exactly one 'd' tag
|
|
58
|
+
if (!Array.isArray(event.tags)) {
|
|
59
|
+
_logger.debug('Missing tags array');
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const dTags = event.tags.filter(tag => Array.isArray(tag) && tag[0] === 'd' && tag[1]);
|
|
63
|
+
if (dTags.length !== 1) {
|
|
64
|
+
_logger.debug('Must have exactly one d tag');
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
_logger.error('Error validating parameterized event:', error);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Creates a parameterized event manager
|
|
76
|
+
* @param _logger - Logger instance
|
|
77
|
+
* @returns {ParameterizedEventManager} Event manager
|
|
78
|
+
*/
|
|
79
|
+
export function createParameterizedEventManager(_logger) {
|
|
80
|
+
return {
|
|
81
|
+
createEvent(kind, content, identifier, tags = []) {
|
|
82
|
+
return createParameterizedEvent(kind, content, identifier, tags);
|
|
83
|
+
},
|
|
84
|
+
updateEvent(kind, identifier, content) {
|
|
85
|
+
return createParameterizedEvent(kind, content, identifier);
|
|
86
|
+
},
|
|
87
|
+
subscribe(kinds, identifiers) {
|
|
88
|
+
return {
|
|
89
|
+
type: 'REQ',
|
|
90
|
+
data: {
|
|
91
|
+
filter: {
|
|
92
|
+
kinds,
|
|
93
|
+
'#d': identifiers
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Creates an event replacement handler
|
|
102
|
+
* @param _logger - Logger instance
|
|
103
|
+
* @returns {EventReplacementHandler} Replacement handler
|
|
104
|
+
*/
|
|
105
|
+
export function createEventReplacementHandler(_logger) {
|
|
106
|
+
return {
|
|
107
|
+
shouldReplace(newEvent, existingEvent) {
|
|
108
|
+
try {
|
|
109
|
+
// Must be same kind
|
|
110
|
+
if (newEvent.kind !== existingEvent.kind)
|
|
111
|
+
return false;
|
|
112
|
+
// Must have same 'd' tag value
|
|
113
|
+
const newTags = newEvent.tags;
|
|
114
|
+
const existingTags = existingEvent.tags;
|
|
115
|
+
const newDTag = newTags?.find(tag => tag[0] === 'd')?.[1];
|
|
116
|
+
const existingDTag = existingTags?.find(tag => tag[0] === 'd')?.[1];
|
|
117
|
+
if (newDTag !== existingDTag)
|
|
118
|
+
return false;
|
|
119
|
+
// Replace if newer
|
|
120
|
+
return newEvent.created_at > existingEvent.created_at;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
_logger.error('Error checking event replacement:', error);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
getReplacementKey(event) {
|
|
128
|
+
const kind = event.kind;
|
|
129
|
+
const dTag = event.tags?.find(tag => tag[0] === 'd')?.[1];
|
|
130
|
+
return `${kind}:${dTag}`;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
package/dist/nostr-server.d.ts
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
import { NostrWSServerOptions } from './types/nostr';
|
|
2
|
+
/**
|
|
3
|
+
* Represents a Nostr WebSocket server
|
|
4
|
+
*/
|
|
2
5
|
export declare class NostrWSServer {
|
|
6
|
+
/**
|
|
7
|
+
* The underlying WebSocket server instance
|
|
8
|
+
*/
|
|
3
9
|
private server;
|
|
10
|
+
/**
|
|
11
|
+
* Logger instance for this server
|
|
12
|
+
*/
|
|
4
13
|
private logger;
|
|
14
|
+
/**
|
|
15
|
+
* Creates a new Nostr WebSocket server instance
|
|
16
|
+
*
|
|
17
|
+
* @param {NostrWSServerOptions} options - Server configuration options
|
|
18
|
+
*/
|
|
5
19
|
constructor(options: NostrWSServerOptions);
|
|
20
|
+
/**
|
|
21
|
+
* Closes the WebSocket server
|
|
22
|
+
*/
|
|
6
23
|
close(): void;
|
|
7
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Creates a new Nostr WebSocket server instance
|
|
27
|
+
*
|
|
28
|
+
* @param {NostrWSServerOptions} options - Server configuration options
|
|
29
|
+
* @returns {NostrWSServer} The created server instance
|
|
30
|
+
*/
|
|
8
31
|
export declare function createWSServer(options: NostrWSServerOptions): NostrWSServer;
|
package/dist/nostr-server.js
CHANGED
|
@@ -1,15 +1,36 @@
|
|
|
1
1
|
import { Server as WebSocketServer } from 'ws';
|
|
2
2
|
import { getLogger } from './utils/logger';
|
|
3
|
+
/**
|
|
4
|
+
* Represents a Nostr WebSocket server
|
|
5
|
+
*/
|
|
3
6
|
export class NostrWSServer {
|
|
7
|
+
/**
|
|
8
|
+
* Creates a new Nostr WebSocket server instance
|
|
9
|
+
*
|
|
10
|
+
* @param {NostrWSServerOptions} options - Server configuration options
|
|
11
|
+
*/
|
|
4
12
|
constructor(options) {
|
|
13
|
+
/**
|
|
14
|
+
* Logger instance for this server
|
|
15
|
+
*/
|
|
5
16
|
this.logger = getLogger('NostrWSServer');
|
|
6
17
|
this.server = new WebSocketServer({ port: options.port });
|
|
18
|
+
/**
|
|
19
|
+
* Handles incoming WebSocket connections
|
|
20
|
+
*
|
|
21
|
+
* @param {NostrWSSocket} socket - The connected WebSocket client
|
|
22
|
+
*/
|
|
7
23
|
this.server.on('connection', (socket) => {
|
|
8
24
|
socket.subscriptions = new Set();
|
|
9
25
|
socket.isAlive = true;
|
|
10
26
|
if (options.onConnection) {
|
|
11
27
|
options.onConnection(socket);
|
|
12
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Handles incoming messages from the client
|
|
31
|
+
*
|
|
32
|
+
* @param {Buffer | ArrayBuffer | Buffer[]} data - The incoming message data
|
|
33
|
+
*/
|
|
13
34
|
const handleMessage = async (data) => {
|
|
14
35
|
try {
|
|
15
36
|
const message = JSON.parse(data.toString());
|
|
@@ -24,11 +45,19 @@ export class NostrWSServer {
|
|
|
24
45
|
}
|
|
25
46
|
};
|
|
26
47
|
socket.on('message', handleMessage);
|
|
48
|
+
/**
|
|
49
|
+
* Handles client disconnections
|
|
50
|
+
*/
|
|
27
51
|
socket.on('close', () => {
|
|
28
52
|
if (options.handlers?.close) {
|
|
29
53
|
options.handlers.close(socket);
|
|
30
54
|
}
|
|
31
55
|
});
|
|
56
|
+
/**
|
|
57
|
+
* Handles WebSocket errors
|
|
58
|
+
*
|
|
59
|
+
* @param {Error} error - The error that occurred
|
|
60
|
+
*/
|
|
32
61
|
socket.on('error', (error) => {
|
|
33
62
|
if (options.handlers?.error) {
|
|
34
63
|
options.handlers.error(socket, error);
|
|
@@ -45,19 +74,34 @@ export class NostrWSServer {
|
|
|
45
74
|
socket.isAlive = false;
|
|
46
75
|
socket.ping();
|
|
47
76
|
}, options.heartbeatInterval);
|
|
77
|
+
/**
|
|
78
|
+
* Handles WebSocket pong responses
|
|
79
|
+
*/
|
|
48
80
|
socket.on('pong', () => {
|
|
49
81
|
socket.isAlive = true;
|
|
50
82
|
});
|
|
83
|
+
/**
|
|
84
|
+
* Handles client disconnections (again, to clear the interval)
|
|
85
|
+
*/
|
|
51
86
|
socket.on('close', () => {
|
|
52
87
|
clearInterval(interval);
|
|
53
88
|
});
|
|
54
89
|
}
|
|
55
90
|
});
|
|
56
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Closes the WebSocket server
|
|
94
|
+
*/
|
|
57
95
|
close() {
|
|
58
96
|
this.server.close();
|
|
59
97
|
}
|
|
60
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Creates a new Nostr WebSocket server instance
|
|
101
|
+
*
|
|
102
|
+
* @param {NostrWSServerOptions} options - Server configuration options
|
|
103
|
+
* @returns {NostrWSServer} The created server instance
|
|
104
|
+
*/
|
|
61
105
|
export function createWSServer(options) {
|
|
62
106
|
return new NostrWSServer(options);
|
|
63
107
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
import { EventEmitter } from 'events';
|
|
3
2
|
import { WebSocketServer } from 'ws';
|
|
4
|
-
import
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
import type { NostrWSOptions, NostrWSMessage } from './types/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* WebSocket server implementation for Nostr protocol
|
|
7
|
+
* Extends EventEmitter to provide event-based message handling
|
|
8
|
+
*/
|
|
5
9
|
export declare class NostrWSServer extends EventEmitter {
|
|
6
10
|
private wss;
|
|
7
11
|
private options;
|
|
12
|
+
private clients;
|
|
8
13
|
private heartbeatInterval;
|
|
9
|
-
clients: Map<string, ExtendedWebSocket>;
|
|
10
14
|
constructor(wss: WebSocketServer, options?: Partial<NostrWSOptions>);
|
|
11
15
|
private setupServer;
|
|
12
16
|
private handleConnection;
|
|
@@ -14,4 +18,10 @@ export declare class NostrWSServer extends EventEmitter {
|
|
|
14
18
|
broadcast(message: NostrWSMessage): void;
|
|
15
19
|
broadcastToChannel(channel: string, message: NostrWSMessage): void;
|
|
16
20
|
close(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Check if a client with the given ID exists
|
|
23
|
+
* @param clientId - The ID of the client to check
|
|
24
|
+
* @returns boolean indicating if the client exists
|
|
25
|
+
*/
|
|
26
|
+
hasClient(clientId: string): boolean;
|
|
17
27
|
}
|