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,438 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { schnorr } from '@noble/curves/secp256k1';
|
|
3
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
4
|
+
import EventEmitter from 'events';
|
|
5
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
6
|
+
/* ================ [ Configuration ] ================ */
|
|
7
|
+
const HOST = 'ws://localhost';
|
|
8
|
+
const DEBUG = process.env['DEBUG'] === 'true';
|
|
9
|
+
const VERBOSE = process.env['VERBOSE'] === 'true' || DEBUG;
|
|
10
|
+
// Only log output mode in non-test environments
|
|
11
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
12
|
+
console.error('output mode:', DEBUG ? 'debug' : VERBOSE ? 'verbose' : 'silent');
|
|
13
|
+
}
|
|
14
|
+
/* ================ [ Schema ] ================ */
|
|
15
|
+
const num = z.number().max(Number.MAX_SAFE_INTEGER), str = z.string(), stamp = num.min(500_000_000), hex = str.regex(/^[0-9a-fA-F]*$/).refine(e => e.length % 2 === 0), hash = hex.refine((e) => e.length === 64), sig = hex.refine((e) => e.length === 128), tags = str.array();
|
|
16
|
+
const event_schema = z.object({
|
|
17
|
+
content: str,
|
|
18
|
+
created_at: stamp,
|
|
19
|
+
id: hash,
|
|
20
|
+
kind: num,
|
|
21
|
+
pubkey: hash,
|
|
22
|
+
sig: sig,
|
|
23
|
+
tags: tags.array()
|
|
24
|
+
});
|
|
25
|
+
const filter_schema = z.object({
|
|
26
|
+
ids: hash.array().optional(),
|
|
27
|
+
authors: hash.array().optional(),
|
|
28
|
+
kinds: num.array().optional(),
|
|
29
|
+
since: stamp.optional(),
|
|
30
|
+
until: stamp.optional(),
|
|
31
|
+
limit: num.optional(),
|
|
32
|
+
}).catchall(tags);
|
|
33
|
+
const sub_schema = z.tuple([str]).rest(filter_schema);
|
|
34
|
+
/* ================ [ Server Class ] ================ */
|
|
35
|
+
export class NostrRelay {
|
|
36
|
+
_emitter;
|
|
37
|
+
_port;
|
|
38
|
+
_purge;
|
|
39
|
+
_subs;
|
|
40
|
+
_wss;
|
|
41
|
+
_cache;
|
|
42
|
+
_isClosing = false;
|
|
43
|
+
conn;
|
|
44
|
+
constructor(port, purge_ival) {
|
|
45
|
+
this._cache = [];
|
|
46
|
+
this._emitter = new EventEmitter();
|
|
47
|
+
this._port = port;
|
|
48
|
+
this._purge = purge_ival ?? null;
|
|
49
|
+
this._subs = new Map();
|
|
50
|
+
this._wss = null;
|
|
51
|
+
this.conn = 0;
|
|
52
|
+
}
|
|
53
|
+
get cache() {
|
|
54
|
+
return this._cache;
|
|
55
|
+
}
|
|
56
|
+
get subs() {
|
|
57
|
+
return this._subs;
|
|
58
|
+
}
|
|
59
|
+
get url() {
|
|
60
|
+
return `${HOST}:${this._port}`;
|
|
61
|
+
}
|
|
62
|
+
get wss() {
|
|
63
|
+
if (this._wss === null) {
|
|
64
|
+
throw new Error('websocket server not initialized');
|
|
65
|
+
}
|
|
66
|
+
return this._wss;
|
|
67
|
+
}
|
|
68
|
+
async start() {
|
|
69
|
+
this._wss = new WebSocketServer({ port: this._port });
|
|
70
|
+
this._isClosing = false;
|
|
71
|
+
DEBUG && console.log('[ relay ] running on port:', this._port);
|
|
72
|
+
this.wss.on('connection', socket => {
|
|
73
|
+
const instance = new ClientSession(this, socket);
|
|
74
|
+
socket.on('message', msg => instance._handler(msg.toString()));
|
|
75
|
+
socket.on('error', err => instance._onerr(err));
|
|
76
|
+
socket.on('close', code => instance._cleanup(code));
|
|
77
|
+
this.conn += 1;
|
|
78
|
+
});
|
|
79
|
+
return new Promise(res => {
|
|
80
|
+
this.wss.on('listening', () => {
|
|
81
|
+
if (this._purge !== null) {
|
|
82
|
+
DEBUG && console.log(`[ relay ] purging events every ${this._purge} seconds`);
|
|
83
|
+
setInterval(() => {
|
|
84
|
+
this._cache = [];
|
|
85
|
+
}, this._purge * 1000);
|
|
86
|
+
}
|
|
87
|
+
this._emitter.emit('connected');
|
|
88
|
+
res(this);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
onconnect(cb) {
|
|
93
|
+
this._emitter.on('connected', cb);
|
|
94
|
+
}
|
|
95
|
+
close() {
|
|
96
|
+
return new Promise(resolve => {
|
|
97
|
+
if (this._isClosing) {
|
|
98
|
+
DEBUG && console.log('[ relay ] already closing, skipping duplicate close call');
|
|
99
|
+
resolve();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this._isClosing = true;
|
|
103
|
+
if (this._wss) {
|
|
104
|
+
// Clean up clients first
|
|
105
|
+
if (this._wss.clients && this._wss.clients.size > 0) {
|
|
106
|
+
this._wss.clients.forEach(client => {
|
|
107
|
+
try {
|
|
108
|
+
client.close(1000, 'Server shutting down');
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
// Ignore errors
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// Clear state
|
|
116
|
+
this._subs.clear();
|
|
117
|
+
this._cache = [];
|
|
118
|
+
// Close server with timeout
|
|
119
|
+
const timeout = setTimeout(() => {
|
|
120
|
+
DEBUG && console.log('[ relay ] server close timed out, forcing cleanup');
|
|
121
|
+
this._wss = null;
|
|
122
|
+
resolve();
|
|
123
|
+
}, 500);
|
|
124
|
+
const wss = this._wss;
|
|
125
|
+
this._wss = null;
|
|
126
|
+
wss.close(() => {
|
|
127
|
+
clearTimeout(timeout);
|
|
128
|
+
resolve();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
resolve();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
store(event) {
|
|
137
|
+
this._cache = this._cache.concat(event).sort((a, b) => a > b ? -1 : 1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/* ================ [ Instance Class ] ================ */
|
|
141
|
+
class ClientSession {
|
|
142
|
+
_sid;
|
|
143
|
+
_relay;
|
|
144
|
+
_socket;
|
|
145
|
+
_subs;
|
|
146
|
+
constructor(relay, socket) {
|
|
147
|
+
this._relay = relay;
|
|
148
|
+
this._sid = Math.random().toString().slice(2, 8);
|
|
149
|
+
this._socket = socket;
|
|
150
|
+
this._subs = new Set();
|
|
151
|
+
this.log.client('client connected');
|
|
152
|
+
}
|
|
153
|
+
get sid() {
|
|
154
|
+
return this._sid;
|
|
155
|
+
}
|
|
156
|
+
get relay() {
|
|
157
|
+
return this._relay;
|
|
158
|
+
}
|
|
159
|
+
get socket() {
|
|
160
|
+
return this._socket;
|
|
161
|
+
}
|
|
162
|
+
_cleanup(code) {
|
|
163
|
+
try {
|
|
164
|
+
// First remove all subscriptions associated with this client
|
|
165
|
+
for (const subId of this._subs) {
|
|
166
|
+
this.remSub(subId);
|
|
167
|
+
}
|
|
168
|
+
this._subs.clear();
|
|
169
|
+
// Close the socket if it's still open
|
|
170
|
+
if (this.socket.readyState === WebSocket.OPEN) {
|
|
171
|
+
this.socket.close();
|
|
172
|
+
}
|
|
173
|
+
this.relay.conn -= 1;
|
|
174
|
+
this.log.client(`[ ${this._sid} ]`, 'client disconnected with code:', code);
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
DEBUG && console.error(`[ client ][ ${this._sid} ]`, 'error during cleanup:', e);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
_handler(message) {
|
|
181
|
+
let verb, payload;
|
|
182
|
+
try {
|
|
183
|
+
// Try to parse as JSON
|
|
184
|
+
const parsed = JSON.parse(message);
|
|
185
|
+
// Handle NIP-46 messages (which might not follow standard Nostr format)
|
|
186
|
+
if (parsed && Array.isArray(parsed) && parsed.length > 0) {
|
|
187
|
+
// Check if it's a standard Nostr message
|
|
188
|
+
if (['EVENT', 'REQ', 'CLOSE'].includes(parsed[0])) {
|
|
189
|
+
// Handle standard Nostr messages
|
|
190
|
+
[verb, ...payload] = parsed;
|
|
191
|
+
switch (verb) {
|
|
192
|
+
case 'EVENT':
|
|
193
|
+
if (parsed.length !== 2) {
|
|
194
|
+
DEBUG && console.log(`[ ${this._sid} ]`, 'EVENT message missing params:', parsed);
|
|
195
|
+
return this.send(['NOTICE', 'invalid: EVENT message missing params']);
|
|
196
|
+
}
|
|
197
|
+
return this._onevent(parsed[1]);
|
|
198
|
+
case 'REQ':
|
|
199
|
+
if (parsed.length < 2) {
|
|
200
|
+
DEBUG && console.log(`[ ${this._sid} ]`, 'REQ message missing params:', parsed);
|
|
201
|
+
return this.send(['NOTICE', 'invalid: REQ message missing params']);
|
|
202
|
+
}
|
|
203
|
+
const sub_id = parsed[1];
|
|
204
|
+
const filters = parsed.slice(2);
|
|
205
|
+
return this._onreq(sub_id, filters);
|
|
206
|
+
case 'CLOSE':
|
|
207
|
+
if (parsed.length !== 2) {
|
|
208
|
+
DEBUG && console.log(`[ ${this._sid} ]`, 'CLOSE message missing params:', parsed);
|
|
209
|
+
return this.send(['NOTICE', 'invalid: CLOSE message missing params']);
|
|
210
|
+
}
|
|
211
|
+
return this._onclose(parsed[1]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
// This could be a direct NIP-46 message, broadcast it to other clients
|
|
216
|
+
try {
|
|
217
|
+
this.relay.wss.clients.forEach(client => {
|
|
218
|
+
if (client !== this.socket && client.readyState === WebSocket.OPEN) {
|
|
219
|
+
client.send(message);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
DEBUG && console.error('Error broadcasting message:', e);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
this.log.debug('unhandled message format:', message);
|
|
231
|
+
return this.send(['NOTICE', '', 'Unable to handle message']);
|
|
232
|
+
}
|
|
233
|
+
catch (e) {
|
|
234
|
+
this.log.debug('failed to parse message:\n\n', message);
|
|
235
|
+
return this.send(['NOTICE', '', 'Unable to parse message']);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
_onclose(sub_id) {
|
|
239
|
+
this.log.info('closed subscription:', sub_id);
|
|
240
|
+
this.remSub(sub_id);
|
|
241
|
+
}
|
|
242
|
+
_onerr(err) {
|
|
243
|
+
this.log.info('socket encountered an error:\n\n', err);
|
|
244
|
+
}
|
|
245
|
+
_onevent(event) {
|
|
246
|
+
try {
|
|
247
|
+
// Special handling for NIP-46 events (kind 24133)
|
|
248
|
+
if (event.kind === 24133) {
|
|
249
|
+
this.relay.store(event);
|
|
250
|
+
// Find subscriptions that match this event
|
|
251
|
+
for (const [uid, sub] of this.relay.subs.entries()) {
|
|
252
|
+
for (const filter of sub.filters) {
|
|
253
|
+
if (filter.kinds?.includes(24133)) {
|
|
254
|
+
// Check for #p tag filter
|
|
255
|
+
const pTags = event.tags.filter(tag => tag[0] === 'p').map(tag => tag[1]);
|
|
256
|
+
const pFilters = Object.entries(filter)
|
|
257
|
+
.filter(([key]) => key === '#p')
|
|
258
|
+
.map(([_, value]) => value)
|
|
259
|
+
.flat();
|
|
260
|
+
// If there's a #p filter, make sure the event matches it
|
|
261
|
+
if (pFilters.length > 0 && !pTags.some(tag => pFilters.includes(tag))) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
// Send to matching subscription
|
|
265
|
+
const [clientId, subId] = uid.split('/');
|
|
266
|
+
sub.instance.send(['EVENT', subId, event]);
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Send OK message
|
|
272
|
+
this.send(['OK', event.id, true, '']);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// Standard event processing
|
|
276
|
+
this.log.client('received event id:', event.id);
|
|
277
|
+
this.log.debug('event:', event);
|
|
278
|
+
if (!verify_event(event)) {
|
|
279
|
+
this.log.debug('event failed validation:', event);
|
|
280
|
+
this.send(['OK', event.id, false, 'event failed validation']);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
this.send(['OK', event.id, true, '']);
|
|
284
|
+
this.relay.store(event);
|
|
285
|
+
for (const { filters, instance, sub_id } of this.relay.subs.values()) {
|
|
286
|
+
for (const filter of filters) {
|
|
287
|
+
if (match_filter(event, filter)) {
|
|
288
|
+
instance.log.client(`event matched subscription: ${sub_id}`);
|
|
289
|
+
instance.send(['EVENT', sub_id, event]);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch (e) {
|
|
295
|
+
DEBUG && console.error('Error processing event:', e);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
_onreq(sub_id, filters) {
|
|
299
|
+
if (filters.length === 0) {
|
|
300
|
+
this.log.client('request has no filters');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
this.log.client('received subscription request:', sub_id);
|
|
304
|
+
this.log.debug('filters:', filters);
|
|
305
|
+
// Add subscription
|
|
306
|
+
this.addSub(sub_id, ...filters);
|
|
307
|
+
// Check for NIP-46 subscription
|
|
308
|
+
const hasNip46Filter = filters.some(f => f.kinds?.includes(24133));
|
|
309
|
+
// For each filter
|
|
310
|
+
let count = 0;
|
|
311
|
+
for (const filter of filters) {
|
|
312
|
+
// Set the limit count, if any
|
|
313
|
+
let limitCount = filter.limit;
|
|
314
|
+
for (const event of this.relay.cache) {
|
|
315
|
+
// If limit is reached, stop sending events
|
|
316
|
+
if (limitCount !== undefined && limitCount <= 0)
|
|
317
|
+
break;
|
|
318
|
+
// Check if event matches filter
|
|
319
|
+
if (match_filter(event, filter)) {
|
|
320
|
+
this.send(['EVENT', sub_id, event]);
|
|
321
|
+
count++;
|
|
322
|
+
this.log.client(`event matched in cache: ${event.id}`);
|
|
323
|
+
this.log.client(`event matched subscription: ${sub_id}`);
|
|
324
|
+
// Update limit counter
|
|
325
|
+
if (limitCount !== undefined)
|
|
326
|
+
limitCount--;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
DEBUG && this.log.debug(`sent ${count} matching events from cache`);
|
|
331
|
+
// Send EOSE
|
|
332
|
+
this.send(['EOSE', sub_id]);
|
|
333
|
+
}
|
|
334
|
+
get log() {
|
|
335
|
+
return {
|
|
336
|
+
client: (...msg) => VERBOSE && console.log(`[ client ][ ${this._sid} ]`, ...msg),
|
|
337
|
+
debug: (...msg) => DEBUG && console.log(`[ debug ][ ${this._sid} ]`, ...msg),
|
|
338
|
+
info: (...msg) => VERBOSE && console.log(`[ info ][ ${this._sid} ]`, ...msg),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
addSub(sub_id, ...filters) {
|
|
342
|
+
const uid = `${this.sid}/${sub_id}`;
|
|
343
|
+
this.relay.subs.set(uid, { filters, instance: this, sub_id });
|
|
344
|
+
this._subs.add(sub_id);
|
|
345
|
+
}
|
|
346
|
+
remSub(subId) {
|
|
347
|
+
try {
|
|
348
|
+
const uid = `${this.sid}/${subId}`;
|
|
349
|
+
this.relay.subs.delete(uid);
|
|
350
|
+
this._subs.delete(subId);
|
|
351
|
+
}
|
|
352
|
+
catch (e) {
|
|
353
|
+
// Ignore errors
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
send(message) {
|
|
357
|
+
try {
|
|
358
|
+
if (this.socket.readyState === WebSocket.OPEN) {
|
|
359
|
+
this.socket.send(JSON.stringify(message));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch (e) {
|
|
363
|
+
DEBUG && console.error(`Failed to send message to client ${this._sid}:`, e);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/* ================ [ Methods ] ================ */
|
|
368
|
+
function assert(value) {
|
|
369
|
+
if (value === false)
|
|
370
|
+
throw new Error('assertion failed!');
|
|
371
|
+
}
|
|
372
|
+
function match_filter(event, filter = {}) {
|
|
373
|
+
const { authors, ids, kinds, since, until, limit, ...rest } = filter;
|
|
374
|
+
const tag_filters = Object.entries(rest)
|
|
375
|
+
.filter(e => e[0].startsWith('#'))
|
|
376
|
+
.map(e => [e[0].slice(1, 2), ...e[1]]);
|
|
377
|
+
if (ids !== undefined && !ids.includes(event.id)) {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
else if (since !== undefined && event.created_at < since) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
else if (until !== undefined && event.created_at > until) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
else if (authors !== undefined && !authors.includes(event.pubkey)) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
else if (kinds !== undefined && !kinds.includes(event.kind)) {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
else if (tag_filters.length > 0) {
|
|
393
|
+
return match_tags(tag_filters, event.tags);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function match_tags(filters, tags) {
|
|
400
|
+
// For each filter, we need to find at least one match in event tags
|
|
401
|
+
for (const [key, ...terms] of filters) {
|
|
402
|
+
let filterMatched = false;
|
|
403
|
+
// Skip empty filter terms
|
|
404
|
+
if (terms.length === 0) {
|
|
405
|
+
filterMatched = true;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
// For each tag that matches the filter key
|
|
409
|
+
for (const [tag, ...params] of tags) {
|
|
410
|
+
if (tag !== key)
|
|
411
|
+
continue;
|
|
412
|
+
// For each term in the filter
|
|
413
|
+
for (const term of terms) {
|
|
414
|
+
// If any term matches any parameter, this filter condition is satisfied
|
|
415
|
+
if (params.includes(term)) {
|
|
416
|
+
filterMatched = true;
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// If we found a match for this filter, we can stop checking tags
|
|
421
|
+
if (filterMatched)
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
// If no match was found for this filter condition, event doesn't match
|
|
425
|
+
if (!filterMatched)
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
// All filter conditions were satisfied
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
function verify_event(event) {
|
|
432
|
+
const { content, created_at, id, kind, pubkey, sig, tags } = event;
|
|
433
|
+
const pimg = JSON.stringify([0, pubkey, created_at, kind, tags, content]);
|
|
434
|
+
const dig = Buffer.from(sha256(pimg)).toString('hex');
|
|
435
|
+
if (dig !== id)
|
|
436
|
+
return false;
|
|
437
|
+
return schnorr.verify(sig, id, pubkey);
|
|
438
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { hexToNpub } from './conversion.js';
|
|
2
|
+
/**
|
|
3
|
+
* Format a pubkey for display, converting to npub format
|
|
4
|
+
* @param pubkey The pubkey in hex format
|
|
5
|
+
* @param useShortFormat Whether to use a shortened format
|
|
6
|
+
* @returns The formatted pubkey
|
|
7
|
+
*/
|
|
8
|
+
export function formatPubkey(pubkey, useShortFormat = false) {
|
|
9
|
+
try {
|
|
10
|
+
if (!pubkey)
|
|
11
|
+
return 'unknown';
|
|
12
|
+
// Convert to npub
|
|
13
|
+
const npub = hexToNpub(pubkey);
|
|
14
|
+
// If converting to npub failed, return a shortened hex
|
|
15
|
+
if (!npub) {
|
|
16
|
+
return useShortFormat
|
|
17
|
+
? `${pubkey.substring(0, 4)}...${pubkey.substring(60)}`
|
|
18
|
+
: pubkey;
|
|
19
|
+
}
|
|
20
|
+
// Return appropriately formatted npub
|
|
21
|
+
if (useShortFormat) {
|
|
22
|
+
// For short format, show the first 8 and last 4 characters of the npub
|
|
23
|
+
return `${npub.substring(0, 8)}...${npub.substring(npub.length - 4)}`;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
// For regular format, use the full npub
|
|
27
|
+
return npub;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error('Error formatting pubkey:', error);
|
|
32
|
+
return 'error';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { convertNip19Entity, analyzeNip19Entity } from "./conversion.js";
|
|
3
|
+
// Schema for convertNip19 tool
|
|
4
|
+
export const convertNip19ToolConfig = {
|
|
5
|
+
input: z.string().describe("The NIP-19 entity or hex string to convert"),
|
|
6
|
+
targetType: z.enum(['npub', 'nsec', 'note', 'hex', 'nprofile', 'nevent', 'naddr']).describe("The target format to convert to"),
|
|
7
|
+
relays: z.array(z.string()).optional().describe("Optional relay URLs for complex entities (nprofile, nevent, naddr)"),
|
|
8
|
+
author: z.string().optional().describe("Optional author pubkey (hex format) for nevent/naddr"),
|
|
9
|
+
kind: z.number().optional().describe("Optional event kind for nevent/naddr"),
|
|
10
|
+
identifier: z.string().optional().describe("Required identifier for naddr conversion"),
|
|
11
|
+
};
|
|
12
|
+
// Schema for analyzeNip19 tool
|
|
13
|
+
export const analyzeNip19ToolConfig = {
|
|
14
|
+
input: z.string().describe("The NIP-19 entity or hex string to analyze"),
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Convert any NIP-19 entity to another format
|
|
18
|
+
*/
|
|
19
|
+
export async function convertNip19(input, targetType, relays, author, kind, identifier) {
|
|
20
|
+
try {
|
|
21
|
+
const options = {
|
|
22
|
+
input,
|
|
23
|
+
targetType,
|
|
24
|
+
entityData: {
|
|
25
|
+
...(relays && { relays }),
|
|
26
|
+
...(author && { author }),
|
|
27
|
+
...(kind && { kind }),
|
|
28
|
+
...(identifier && { identifier })
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const result = convertNip19Entity(options);
|
|
32
|
+
if (!result.success) {
|
|
33
|
+
return {
|
|
34
|
+
success: false,
|
|
35
|
+
message: result.message || 'Conversion failed'
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
success: true,
|
|
40
|
+
message: result.message || 'Conversion successful',
|
|
41
|
+
result: result.result,
|
|
42
|
+
originalType: result.originalType,
|
|
43
|
+
data: result.data
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return {
|
|
48
|
+
success: false,
|
|
49
|
+
message: `Error during conversion: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Analyze any NIP-19 entity to get its type and decoded data
|
|
55
|
+
*/
|
|
56
|
+
export async function analyzeNip19(input) {
|
|
57
|
+
try {
|
|
58
|
+
const result = analyzeNip19Entity(input);
|
|
59
|
+
if (!result.success) {
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
message: result.message || 'Analysis failed'
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
success: true,
|
|
67
|
+
message: result.message || 'Analysis successful',
|
|
68
|
+
type: result.originalType,
|
|
69
|
+
data: result.data
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return {
|
|
74
|
+
success: false,
|
|
75
|
+
message: `Error during analysis: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Format analysis result for display
|
|
81
|
+
*/
|
|
82
|
+
export function formatAnalysisResult(type, data) {
|
|
83
|
+
switch (type) {
|
|
84
|
+
case 'hex':
|
|
85
|
+
return `Hex String: ${data}`;
|
|
86
|
+
case 'npub':
|
|
87
|
+
return `Public Key (npub): ${data}`;
|
|
88
|
+
case 'nsec':
|
|
89
|
+
return `Private Key (nsec): ${data}`;
|
|
90
|
+
case 'note':
|
|
91
|
+
return `Note ID: ${data}`;
|
|
92
|
+
case 'nprofile':
|
|
93
|
+
return [
|
|
94
|
+
`Profile Entity:`,
|
|
95
|
+
` Public Key: ${data.pubkey}`,
|
|
96
|
+
` Relays: ${data.relays?.length ? data.relays.join(', ') : 'None'}`
|
|
97
|
+
].join('\n');
|
|
98
|
+
case 'nevent':
|
|
99
|
+
return [
|
|
100
|
+
`Event Entity:`,
|
|
101
|
+
` Event ID: ${data.id}`,
|
|
102
|
+
` Author: ${data.author || 'Not specified'}`,
|
|
103
|
+
` Kind: ${data.kind || 'Not specified'}`,
|
|
104
|
+
` Relays: ${data.relays?.length ? data.relays.join(', ') : 'None'}`
|
|
105
|
+
].join('\n');
|
|
106
|
+
case 'naddr':
|
|
107
|
+
return [
|
|
108
|
+
`Address Entity:`,
|
|
109
|
+
` Identifier: ${data.identifier}`,
|
|
110
|
+
` Public Key: ${data.pubkey}`,
|
|
111
|
+
` Kind: ${data.kind}`,
|
|
112
|
+
` Relays: ${data.relays?.length ? data.relays.join(', ') : 'None'}`
|
|
113
|
+
].join('\n');
|
|
114
|
+
default:
|
|
115
|
+
return `Unknown type: ${type}`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { RelayPool } from "snstr";
|
|
2
|
+
/**
|
|
3
|
+
* Extended RelayPool with compatibility methods for existing codebase
|
|
4
|
+
*/
|
|
5
|
+
export class CompatibleRelayPool extends RelayPool {
|
|
6
|
+
constructor(relays = []) {
|
|
7
|
+
super(relays);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Compatibility method to match existing codebase API
|
|
11
|
+
* Maps to snstr's querySync method
|
|
12
|
+
*/
|
|
13
|
+
async get(relays, filter) {
|
|
14
|
+
try {
|
|
15
|
+
const events = await this.querySync(relays, filter, { timeout: 8000 });
|
|
16
|
+
return events.length > 0 ? events[0] : null;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.error('Error in pool.get:', error);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Compatibility method to match existing codebase API
|
|
25
|
+
* Maps to snstr's querySync method for multiple events
|
|
26
|
+
*/
|
|
27
|
+
async getMany(relays, filter) {
|
|
28
|
+
try {
|
|
29
|
+
return await this.querySync(relays, filter, { timeout: 8000 });
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error('Error in pool.getMany:', error);
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Compatibility method to match existing codebase API
|
|
38
|
+
* Maps to snstr's close method but ignores relay parameter
|
|
39
|
+
*/
|
|
40
|
+
async close(_relays) {
|
|
41
|
+
try {
|
|
42
|
+
await super.close();
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error('Error in pool.close:', error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Create a fresh RelayPool instance for making Nostr requests
|
|
51
|
+
* @returns A new CompatibleRelayPool instance
|
|
52
|
+
*/
|
|
53
|
+
export function getFreshPool(relays = []) {
|
|
54
|
+
return new CompatibleRelayPool(relays);
|
|
55
|
+
}
|