nostr-mcp-server 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +498 -0
- package/build/__tests__/basic.test.js +87 -0
- package/build/__tests__/error-handling.test.js +145 -0
- package/build/__tests__/format-conversion.test.js +137 -0
- package/build/__tests__/integration.test.js +163 -0
- package/build/__tests__/mocks.js +109 -0
- package/build/__tests__/nip19-conversion.test.js +268 -0
- package/build/__tests__/nips-search.test.js +109 -0
- package/build/__tests__/note-creation.test.js +148 -0
- package/build/__tests__/note-tools-functions.test.js +173 -0
- package/build/__tests__/note-tools-unit.test.js +97 -0
- package/build/__tests__/profile-notes-simple.test.js +78 -0
- package/build/__tests__/profile-postnote.test.js +120 -0
- package/build/__tests__/profile-tools.test.js +90 -0
- package/build/__tests__/relay-specification.test.js +136 -0
- package/build/__tests__/search-nips-simple.test.js +96 -0
- package/build/__tests__/websocket-integration.test.js +257 -0
- package/build/__tests__/zap-tools-simple.test.js +72 -0
- package/build/__tests__/zap-tools-tests.test.js +197 -0
- package/build/index.js +1285 -0
- package/build/nips/nips-tools.js +567 -0
- package/build/nips-tools.js +421 -0
- package/build/note/note-tools.js +296 -0
- package/build/note-tools.js +53 -0
- package/build/profile/profile-tools.js +260 -0
- package/build/utils/constants.js +27 -0
- package/build/utils/conversion.js +332 -0
- package/build/utils/ephemeral-relay.js +438 -0
- package/build/utils/formatting.js +34 -0
- package/build/utils/index.js +6 -0
- package/build/utils/nip19-tools.js +117 -0
- package/build/utils/pool.js +55 -0
- package/build/zap/zap-tools.js +980 -0
- package/build/zap-tools.js +989 -0
- package/package.json +59 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { decode, encodePublicKey, encodePrivateKey, encodeNoteId, encodeProfile, encodeEvent, encodeAddress } from "snstr";
|
|
2
|
+
/**
|
|
3
|
+
* Simple relay URL validation - checks for ws:// or wss:// protocol
|
|
4
|
+
*/
|
|
5
|
+
function isValidRelayUrl(url) {
|
|
6
|
+
try {
|
|
7
|
+
const parsed = new URL(url);
|
|
8
|
+
return (parsed.protocol === 'ws:' || parsed.protocol === 'wss:') &&
|
|
9
|
+
!!parsed.hostname &&
|
|
10
|
+
!parsed.username && // No credentials in URL
|
|
11
|
+
!parsed.password;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Filter invalid relay URLs from profile data
|
|
19
|
+
*/
|
|
20
|
+
function filterProfile(profile) {
|
|
21
|
+
if (!profile || typeof profile !== 'object')
|
|
22
|
+
return profile;
|
|
23
|
+
return {
|
|
24
|
+
...profile,
|
|
25
|
+
relays: profile.relays ? profile.relays.filter(isValidRelayUrl) : []
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Filter invalid relay URLs from event data
|
|
30
|
+
*/
|
|
31
|
+
function filterEvent(event) {
|
|
32
|
+
if (!event || typeof event !== 'object')
|
|
33
|
+
return event;
|
|
34
|
+
return {
|
|
35
|
+
...event,
|
|
36
|
+
relays: event.relays ? event.relays.filter(isValidRelayUrl) : []
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Filter invalid relay URLs from address data
|
|
41
|
+
*/
|
|
42
|
+
function filterAddress(address) {
|
|
43
|
+
if (!address || typeof address !== 'object')
|
|
44
|
+
return address;
|
|
45
|
+
return {
|
|
46
|
+
...address,
|
|
47
|
+
relays: address.relays ? address.relays.filter(isValidRelayUrl) : []
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Convert an npub or hex string to hex format
|
|
52
|
+
* @param pubkey The pubkey in either npub or hex format
|
|
53
|
+
* @returns The pubkey in hex format, or null if invalid
|
|
54
|
+
*/
|
|
55
|
+
export function npubToHex(pubkey) {
|
|
56
|
+
try {
|
|
57
|
+
// Clean up input
|
|
58
|
+
pubkey = pubkey.trim();
|
|
59
|
+
// If already hex
|
|
60
|
+
if (/^[0-9a-fA-F]{64}$/.test(pubkey)) {
|
|
61
|
+
return pubkey.toLowerCase();
|
|
62
|
+
}
|
|
63
|
+
// If npub
|
|
64
|
+
if (pubkey.startsWith('npub1')) {
|
|
65
|
+
try {
|
|
66
|
+
const result = decode(pubkey);
|
|
67
|
+
if (result.type === 'npub') {
|
|
68
|
+
return result.data;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
console.error('Error decoding npub:', e);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Not a valid pubkey format
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.error('Error in npubToHex:', error);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Convert a hex pubkey to npub format
|
|
86
|
+
* @param hex The pubkey in hex format
|
|
87
|
+
* @returns The pubkey in npub format, or null if invalid
|
|
88
|
+
*/
|
|
89
|
+
export function hexToNpub(hex) {
|
|
90
|
+
try {
|
|
91
|
+
// Clean up input
|
|
92
|
+
hex = hex.trim();
|
|
93
|
+
// Validate hex format
|
|
94
|
+
if (!/^[0-9a-fA-F]{64}$/.test(hex)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
// Convert to npub
|
|
98
|
+
return encodePublicKey(hex.toLowerCase());
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
console.error('Error in hexToNpub:', error);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Convert any NIP-19 entity to any other format
|
|
107
|
+
*/
|
|
108
|
+
export function convertNip19Entity(options) {
|
|
109
|
+
try {
|
|
110
|
+
const { input, targetType, entityData } = options;
|
|
111
|
+
const cleanInput = input.trim();
|
|
112
|
+
// First, detect what type of input we have
|
|
113
|
+
let sourceData;
|
|
114
|
+
let sourceType;
|
|
115
|
+
// Try to decode as NIP-19 entity first
|
|
116
|
+
if (cleanInput.includes('1')) {
|
|
117
|
+
try {
|
|
118
|
+
const decoded = decode(cleanInput);
|
|
119
|
+
sourceType = decoded.type;
|
|
120
|
+
sourceData = decoded.data;
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
// Not a valid NIP-19 entity, might be hex
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// If not NIP-19, check if it's hex
|
|
127
|
+
if (!sourceType) {
|
|
128
|
+
if (/^[0-9a-fA-F]{64}$/.test(cleanInput)) {
|
|
129
|
+
sourceType = 'hex';
|
|
130
|
+
sourceData = cleanInput.toLowerCase();
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
return {
|
|
134
|
+
success: false,
|
|
135
|
+
message: 'Input is not a valid NIP-19 entity or hex string'
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Apply security filtering for complex types
|
|
140
|
+
if (['nprofile', 'nevent', 'naddr'].includes(sourceType)) {
|
|
141
|
+
if (sourceType === 'nprofile') {
|
|
142
|
+
sourceData = filterProfile(sourceData);
|
|
143
|
+
}
|
|
144
|
+
else if (sourceType === 'nevent') {
|
|
145
|
+
sourceData = filterEvent(sourceData);
|
|
146
|
+
}
|
|
147
|
+
else if (sourceType === 'naddr') {
|
|
148
|
+
sourceData = filterAddress(sourceData);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Now convert to target type
|
|
152
|
+
let result;
|
|
153
|
+
switch (targetType) {
|
|
154
|
+
case 'hex':
|
|
155
|
+
const hexResult = extractHexFromEntity(sourceType, sourceData);
|
|
156
|
+
if (!hexResult)
|
|
157
|
+
throw new Error('Cannot extract hex from input');
|
|
158
|
+
result = hexResult;
|
|
159
|
+
break;
|
|
160
|
+
case 'npub':
|
|
161
|
+
const pubkeyHex = extractHexFromEntity(sourceType, sourceData);
|
|
162
|
+
if (!pubkeyHex)
|
|
163
|
+
throw new Error('Cannot extract pubkey from input');
|
|
164
|
+
result = encodePublicKey(pubkeyHex);
|
|
165
|
+
break;
|
|
166
|
+
case 'nsec':
|
|
167
|
+
if (sourceType !== 'nsec' && sourceType !== 'hex') {
|
|
168
|
+
throw new Error('Can only convert private keys to nsec format');
|
|
169
|
+
}
|
|
170
|
+
const privkeyHex = sourceData;
|
|
171
|
+
result = encodePrivateKey(privkeyHex);
|
|
172
|
+
break;
|
|
173
|
+
case 'note':
|
|
174
|
+
if (sourceType === 'nevent') {
|
|
175
|
+
result = encodeNoteId(sourceData.id);
|
|
176
|
+
}
|
|
177
|
+
else if (sourceType === 'note') {
|
|
178
|
+
result = cleanInput; // Already a note
|
|
179
|
+
}
|
|
180
|
+
else if (sourceType === 'hex') {
|
|
181
|
+
result = encodeNoteId(sourceData);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
throw new Error('Cannot convert this entity type to note format');
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
case 'nprofile':
|
|
188
|
+
const profilePubkey = extractHexFromEntity(sourceType, sourceData);
|
|
189
|
+
if (!profilePubkey)
|
|
190
|
+
throw new Error('Cannot extract pubkey from input');
|
|
191
|
+
const profileData = {
|
|
192
|
+
pubkey: profilePubkey,
|
|
193
|
+
relays: entityData?.relays?.filter(url => isValidRelayUrl(url)) || []
|
|
194
|
+
};
|
|
195
|
+
result = encodeProfile(profileData);
|
|
196
|
+
break;
|
|
197
|
+
case 'nevent':
|
|
198
|
+
let eventId;
|
|
199
|
+
if (sourceType === 'nevent') {
|
|
200
|
+
eventId = sourceData.id;
|
|
201
|
+
}
|
|
202
|
+
else if (sourceType === 'note') {
|
|
203
|
+
eventId = sourceData;
|
|
204
|
+
}
|
|
205
|
+
else if (sourceType === 'hex') {
|
|
206
|
+
eventId = sourceData;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
throw new Error('Cannot convert this entity type to nevent format');
|
|
210
|
+
}
|
|
211
|
+
const eventData = {
|
|
212
|
+
id: eventId,
|
|
213
|
+
relays: entityData?.relays?.filter(url => isValidRelayUrl(url)) || [],
|
|
214
|
+
...(entityData?.author && { author: entityData.author }),
|
|
215
|
+
...(entityData?.kind && { kind: entityData.kind })
|
|
216
|
+
};
|
|
217
|
+
result = encodeEvent(eventData);
|
|
218
|
+
break;
|
|
219
|
+
case 'naddr':
|
|
220
|
+
if (!entityData?.identifier || !entityData?.kind) {
|
|
221
|
+
throw new Error('naddr conversion requires identifier and kind');
|
|
222
|
+
}
|
|
223
|
+
const addrPubkey = extractHexFromEntity(sourceType, sourceData);
|
|
224
|
+
if (!addrPubkey) {
|
|
225
|
+
if (!entityData?.author) {
|
|
226
|
+
throw new Error('naddr conversion requires a pubkey (from input or entityData.author)');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const addressData = {
|
|
230
|
+
identifier: entityData.identifier,
|
|
231
|
+
pubkey: addrPubkey || entityData.author,
|
|
232
|
+
kind: entityData.kind,
|
|
233
|
+
relays: entityData?.relays?.filter(url => isValidRelayUrl(url)) || []
|
|
234
|
+
};
|
|
235
|
+
result = encodeAddress(addressData);
|
|
236
|
+
break;
|
|
237
|
+
default:
|
|
238
|
+
throw new Error(`Unsupported target type: ${targetType}`);
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
success: true,
|
|
242
|
+
result,
|
|
243
|
+
originalType: sourceType,
|
|
244
|
+
targetType,
|
|
245
|
+
message: `Successfully converted ${sourceType} to ${targetType}`,
|
|
246
|
+
data: sourceData
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
return {
|
|
251
|
+
success: false,
|
|
252
|
+
message: `Conversion failed: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Extract hex data from any entity type
|
|
258
|
+
* For entities that contain multiple hex values (like nevent), this function
|
|
259
|
+
* prioritizes pubkeys over event IDs for compatibility with npub/nprofile conversions
|
|
260
|
+
*/
|
|
261
|
+
function extractHexFromEntity(sourceType, sourceData) {
|
|
262
|
+
switch (sourceType) {
|
|
263
|
+
case 'hex':
|
|
264
|
+
return sourceData;
|
|
265
|
+
case 'npub':
|
|
266
|
+
case 'nsec':
|
|
267
|
+
case 'note':
|
|
268
|
+
return sourceData;
|
|
269
|
+
case 'nprofile':
|
|
270
|
+
return sourceData.pubkey;
|
|
271
|
+
case 'nevent':
|
|
272
|
+
// For nevent, we return the author pubkey if available
|
|
273
|
+
// This is because extractHexFromEntity is primarily used for pubkey extraction
|
|
274
|
+
// when converting to npub/nprofile formats
|
|
275
|
+
// If you need the event ID, access sourceData.id directly
|
|
276
|
+
return sourceData.author || null;
|
|
277
|
+
case 'naddr':
|
|
278
|
+
return sourceData.pubkey;
|
|
279
|
+
default:
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Get information about any NIP-19 entity without conversion
|
|
285
|
+
*/
|
|
286
|
+
export function analyzeNip19Entity(input) {
|
|
287
|
+
try {
|
|
288
|
+
const cleanInput = input.trim();
|
|
289
|
+
// Check if hex
|
|
290
|
+
if (/^[0-9a-fA-F]{64}$/.test(cleanInput)) {
|
|
291
|
+
return {
|
|
292
|
+
success: true,
|
|
293
|
+
originalType: 'hex',
|
|
294
|
+
message: 'Valid 64-character hex string',
|
|
295
|
+
data: cleanInput.toLowerCase()
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
// Try to decode as NIP-19
|
|
299
|
+
if (cleanInput.includes('1')) {
|
|
300
|
+
const decoded = decode(cleanInput);
|
|
301
|
+
// Apply security filtering for complex types
|
|
302
|
+
let safeData = decoded.data;
|
|
303
|
+
if (['nprofile', 'nevent', 'naddr'].includes(decoded.type)) {
|
|
304
|
+
if (decoded.type === 'nprofile') {
|
|
305
|
+
safeData = filterProfile(decoded.data);
|
|
306
|
+
}
|
|
307
|
+
else if (decoded.type === 'nevent') {
|
|
308
|
+
safeData = filterEvent(decoded.data);
|
|
309
|
+
}
|
|
310
|
+
else if (decoded.type === 'naddr') {
|
|
311
|
+
safeData = filterAddress(decoded.data);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
success: true,
|
|
316
|
+
originalType: decoded.type,
|
|
317
|
+
message: `Valid ${decoded.type} entity`,
|
|
318
|
+
data: safeData
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
success: false,
|
|
323
|
+
message: 'Input is not a valid NIP-19 entity or hex string'
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
return {
|
|
328
|
+
success: false,
|
|
329
|
+
message: `Analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|