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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +498 -0
  3. package/build/__tests__/basic.test.js +87 -0
  4. package/build/__tests__/error-handling.test.js +145 -0
  5. package/build/__tests__/format-conversion.test.js +137 -0
  6. package/build/__tests__/integration.test.js +163 -0
  7. package/build/__tests__/mocks.js +109 -0
  8. package/build/__tests__/nip19-conversion.test.js +268 -0
  9. package/build/__tests__/nips-search.test.js +109 -0
  10. package/build/__tests__/note-creation.test.js +148 -0
  11. package/build/__tests__/note-tools-functions.test.js +173 -0
  12. package/build/__tests__/note-tools-unit.test.js +97 -0
  13. package/build/__tests__/profile-notes-simple.test.js +78 -0
  14. package/build/__tests__/profile-postnote.test.js +120 -0
  15. package/build/__tests__/profile-tools.test.js +90 -0
  16. package/build/__tests__/relay-specification.test.js +136 -0
  17. package/build/__tests__/search-nips-simple.test.js +96 -0
  18. package/build/__tests__/websocket-integration.test.js +257 -0
  19. package/build/__tests__/zap-tools-simple.test.js +72 -0
  20. package/build/__tests__/zap-tools-tests.test.js +197 -0
  21. package/build/index.js +1285 -0
  22. package/build/nips/nips-tools.js +567 -0
  23. package/build/nips-tools.js +421 -0
  24. package/build/note/note-tools.js +296 -0
  25. package/build/note-tools.js +53 -0
  26. package/build/profile/profile-tools.js +260 -0
  27. package/build/utils/constants.js +27 -0
  28. package/build/utils/conversion.js +332 -0
  29. package/build/utils/ephemeral-relay.js +438 -0
  30. package/build/utils/formatting.js +34 -0
  31. package/build/utils/index.js +6 -0
  32. package/build/utils/nip19-tools.js +117 -0
  33. package/build/utils/pool.js +55 -0
  34. package/build/zap/zap-tools.js +980 -0
  35. package/build/zap-tools.js +989 -0
  36. 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
+ }