@contextvm/sdk 0.1.30-rc.0 → 0.1.30-rc.2
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/dist/esm/core/utils/logger.d.ts.map +1 -1
- package/dist/esm/core/utils/logger.js +4 -1
- package/dist/esm/core/utils/logger.js.map +1 -1
- package/dist/esm/transport/base-nostr-transport.d.ts +9 -0
- package/dist/esm/transport/base-nostr-transport.d.ts.map +1 -1
- package/dist/esm/transport/base-nostr-transport.js +159 -42
- package/dist/esm/transport/base-nostr-transport.js.map +1 -1
- package/dist/esm/transport/nostr-client-transport.d.ts.map +1 -1
- package/dist/esm/transport/nostr-client-transport.js +208 -64
- package/dist/esm/transport/nostr-client-transport.js.map +1 -1
- package/dist/esm/transport/nostr-server-transport.d.ts +11 -0
- package/dist/esm/transport/nostr-server-transport.d.ts.map +1 -1
- package/dist/esm/transport/nostr-server-transport.js +335 -178
- package/dist/esm/transport/nostr-server-transport.js.map +1 -1
- package/package.json +1 -1
|
@@ -13,13 +13,17 @@ const logger = createLogger('nostr-server-transport');
|
|
|
13
13
|
*/
|
|
14
14
|
export class NostrServerTransport extends BaseNostrTransport {
|
|
15
15
|
constructor(options) {
|
|
16
|
+
var _a, _b;
|
|
16
17
|
super(options);
|
|
17
18
|
this.clientSessions = new Map();
|
|
19
|
+
this.eventIdToClient = new Map(); // eventId -> clientPubkey
|
|
18
20
|
this.isInitialized = false;
|
|
19
21
|
this.serverInfo = options.serverInfo;
|
|
20
22
|
this.isPublicServer = options.isPublicServer;
|
|
21
23
|
this.allowedPublicKeys = options.allowedPublicKeys;
|
|
22
24
|
this.excludedCapabilities = options.excludedCapabilities;
|
|
25
|
+
this.cleanupIntervalMs = (_a = options.cleanupIntervalMs) !== null && _a !== void 0 ? _a : 60000;
|
|
26
|
+
this.sessionTimeoutMs = (_b = options.sessionTimeoutMs) !== null && _b !== void 0 ? _b : 300000;
|
|
23
27
|
}
|
|
24
28
|
/**
|
|
25
29
|
* Generates common tags from server information for use in Nostr events.
|
|
@@ -27,6 +31,9 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
27
31
|
*/
|
|
28
32
|
generateCommonTags() {
|
|
29
33
|
var _a, _b, _c, _d;
|
|
34
|
+
if (this.cachedCommonTags) {
|
|
35
|
+
return this.cachedCommonTags;
|
|
36
|
+
}
|
|
30
37
|
const commonTags = [];
|
|
31
38
|
if ((_a = this.serverInfo) === null || _a === void 0 ? void 0 : _a.name) {
|
|
32
39
|
commonTags.push([NOSTR_TAGS.NAME, this.serverInfo.name]);
|
|
@@ -43,6 +50,7 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
43
50
|
if (this.encryptionMode !== EncryptionMode.DISABLED) {
|
|
44
51
|
commonTags.push([NOSTR_TAGS.SUPPORT_ENCRYPTION]);
|
|
45
52
|
}
|
|
53
|
+
this.cachedCommonTags = commonTags;
|
|
46
54
|
return commonTags;
|
|
47
55
|
}
|
|
48
56
|
/**
|
|
@@ -50,24 +58,70 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
50
58
|
* to receive incoming MCP requests.
|
|
51
59
|
*/
|
|
52
60
|
async start() {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
await this.
|
|
61
|
+
var _a;
|
|
62
|
+
try {
|
|
63
|
+
await this.connect();
|
|
64
|
+
const pubkey = await this.getPublicKey();
|
|
65
|
+
logger.info('Server pubkey:', pubkey);
|
|
66
|
+
// Subscribe to events targeting this server's public key
|
|
67
|
+
const filters = this.createSubscriptionFilters(pubkey);
|
|
68
|
+
await this.subscribe(filters, async (event) => {
|
|
69
|
+
var _a;
|
|
70
|
+
try {
|
|
71
|
+
await this.processIncomingEvent(event);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
logger.error('Error processing incoming event', {
|
|
75
|
+
error: error instanceof Error ? error.message : String(error),
|
|
76
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
77
|
+
eventId: event.id,
|
|
78
|
+
});
|
|
79
|
+
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error instanceof Error ? error : new Error(String(error)));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
if (this.isPublicServer) {
|
|
83
|
+
await this.getAnnouncementData();
|
|
84
|
+
}
|
|
85
|
+
// Start periodic cleanup of inactive sessions
|
|
86
|
+
this.cleanupInterval = setInterval(() => {
|
|
87
|
+
const cleaned = this.cleanupInactiveSessions();
|
|
88
|
+
if (cleaned > 0) {
|
|
89
|
+
logger.info(`Cleaned up ${cleaned} inactive sessions`);
|
|
90
|
+
}
|
|
91
|
+
}, this.cleanupIntervalMs);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
logger.error('Error starting NostrServerTransport', {
|
|
95
|
+
error: error instanceof Error ? error.message : String(error),
|
|
96
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
97
|
+
});
|
|
98
|
+
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error instanceof Error ? error : new Error(String(error)));
|
|
99
|
+
throw error;
|
|
61
100
|
}
|
|
62
101
|
}
|
|
63
102
|
/**
|
|
64
103
|
* Closes the transport, disconnecting from the relay.
|
|
65
104
|
*/
|
|
66
105
|
async close() {
|
|
67
|
-
var _a;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
106
|
+
var _a, _b;
|
|
107
|
+
try {
|
|
108
|
+
// Clear the cleanup interval
|
|
109
|
+
if (this.cleanupInterval) {
|
|
110
|
+
clearInterval(this.cleanupInterval);
|
|
111
|
+
this.cleanupInterval = undefined;
|
|
112
|
+
}
|
|
113
|
+
await this.disconnect();
|
|
114
|
+
this.clientSessions.clear();
|
|
115
|
+
(_a = this.onclose) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
logger.error('Error closing NostrServerTransport', {
|
|
119
|
+
error: error instanceof Error ? error.message : String(error),
|
|
120
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
121
|
+
});
|
|
122
|
+
(_b = this.onerror) === null || _b === void 0 ? void 0 : _b.call(this, error instanceof Error ? error : new Error(String(error)));
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
71
125
|
}
|
|
72
126
|
/**
|
|
73
127
|
* Sends JSON-RPC messages over the Nostr transport.
|
|
@@ -80,7 +134,6 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
80
134
|
await this.handleResponse(message);
|
|
81
135
|
}
|
|
82
136
|
else if (isJSONRPCNotification(message)) {
|
|
83
|
-
this.cleanupInactiveSessions();
|
|
84
137
|
await this.handleNotification(message);
|
|
85
138
|
}
|
|
86
139
|
else {
|
|
@@ -111,7 +164,17 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
111
164
|
};
|
|
112
165
|
// Collect events using the subscribe method with onEvent hook
|
|
113
166
|
await this.relayHandler.subscribe([filter], (event) => {
|
|
114
|
-
|
|
167
|
+
var _a;
|
|
168
|
+
try {
|
|
169
|
+
events.push(event);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
logger.error('Error in relay subscription event collection', {
|
|
173
|
+
error: error instanceof Error ? error.message : String(error),
|
|
174
|
+
eventId: event.id,
|
|
175
|
+
});
|
|
176
|
+
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error instanceof Error ? error : new Error(String(error)));
|
|
177
|
+
}
|
|
115
178
|
});
|
|
116
179
|
if (!events.length) {
|
|
117
180
|
logger.info(`No events found for kind ${kind} to delete`);
|
|
@@ -136,43 +199,52 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
136
199
|
* the initialize request, waiting for the response, and then proceeding with other announcements.
|
|
137
200
|
*/
|
|
138
201
|
async getAnnouncementData() {
|
|
139
|
-
var _a, _b;
|
|
140
|
-
const initializeParams = {
|
|
141
|
-
protocolVersion: LATEST_PROTOCOL_VERSION,
|
|
142
|
-
capabilities: {},
|
|
143
|
-
clientInfo: {
|
|
144
|
-
name: 'DummyClient',
|
|
145
|
-
version: '1.0.0',
|
|
146
|
-
},
|
|
147
|
-
};
|
|
148
|
-
// Send the initialize request if not already initialized
|
|
149
|
-
if (!this.isInitialized) {
|
|
150
|
-
const initializeMessage = {
|
|
151
|
-
jsonrpc: '2.0',
|
|
152
|
-
id: 'announcement',
|
|
153
|
-
method: 'initialize',
|
|
154
|
-
params: initializeParams,
|
|
155
|
-
};
|
|
156
|
-
logger.info('Sending initialize request for announcement');
|
|
157
|
-
(_a = this.onmessage) === null || _a === void 0 ? void 0 : _a.call(this, initializeMessage);
|
|
158
|
-
}
|
|
202
|
+
var _a, _b, _c;
|
|
159
203
|
try {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
204
|
+
const initializeParams = {
|
|
205
|
+
protocolVersion: LATEST_PROTOCOL_VERSION,
|
|
206
|
+
capabilities: {},
|
|
207
|
+
clientInfo: {
|
|
208
|
+
name: 'DummyClient',
|
|
209
|
+
version: '1.0.0',
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
// Send the initialize request if not already initialized
|
|
213
|
+
if (!this.isInitialized) {
|
|
214
|
+
const initializeMessage = {
|
|
166
215
|
jsonrpc: '2.0',
|
|
167
216
|
id: 'announcement',
|
|
168
|
-
method:
|
|
169
|
-
params:
|
|
217
|
+
method: 'initialize',
|
|
218
|
+
params: initializeParams,
|
|
170
219
|
};
|
|
171
|
-
(
|
|
220
|
+
logger.info('Sending initialize request for announcement');
|
|
221
|
+
(_a = this.onmessage) === null || _a === void 0 ? void 0 : _a.call(this, initializeMessage);
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
// Wait for initialization to complete
|
|
225
|
+
await this.waitForInitialization();
|
|
226
|
+
// Send all announcements now that we're initialized
|
|
227
|
+
for (const [key, methodValue] of Object.entries(announcementMethods)) {
|
|
228
|
+
logger.info('Sending announcement', { key, methodValue });
|
|
229
|
+
const message = {
|
|
230
|
+
jsonrpc: '2.0',
|
|
231
|
+
id: 'announcement',
|
|
232
|
+
method: methodValue,
|
|
233
|
+
params: key === 'server' ? initializeParams : {},
|
|
234
|
+
};
|
|
235
|
+
(_b = this.onmessage) === null || _b === void 0 ? void 0 : _b.call(this, message);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
logger.warn('Server not initialized after waiting, skipping announcements', { error: error instanceof Error ? error.message : error });
|
|
172
240
|
}
|
|
173
241
|
}
|
|
174
242
|
catch (error) {
|
|
175
|
-
logger.
|
|
243
|
+
logger.error('Error in getAnnouncementData', {
|
|
244
|
+
error: error instanceof Error ? error.message : String(error),
|
|
245
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
246
|
+
});
|
|
247
|
+
(_c = this.onerror) === null || _c === void 0 ? void 0 : _c.call(this, error instanceof Error ? error : new Error(String(error)));
|
|
176
248
|
}
|
|
177
249
|
}
|
|
178
250
|
/**
|
|
@@ -181,13 +253,18 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
181
253
|
* The method will always resolve, allowing announcements to proceed.
|
|
182
254
|
*/
|
|
183
255
|
async waitForInitialization() {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
256
|
+
if (this.isInitialized)
|
|
257
|
+
return;
|
|
258
|
+
if (!this.initializationPromise) {
|
|
259
|
+
this.initializationPromise = new Promise((resolve) => {
|
|
260
|
+
this.initializationResolver = resolve;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Initialization timeout')), 10000));
|
|
264
|
+
try {
|
|
265
|
+
await Promise.race([this.initializationPromise, timeout]);
|
|
188
266
|
}
|
|
189
|
-
|
|
190
|
-
if (!this.isInitialized) {
|
|
267
|
+
catch (_a) {
|
|
191
268
|
logger.warn('Server initialization not completed within timeout, proceeding with announcements');
|
|
192
269
|
}
|
|
193
270
|
}
|
|
@@ -197,33 +274,43 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
197
274
|
* @param message The JSON-RPC response containing the announcement data.
|
|
198
275
|
*/
|
|
199
276
|
async announcer(message) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
schema:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
277
|
+
var _a;
|
|
278
|
+
try {
|
|
279
|
+
const recipientPubkey = await this.getPublicKey();
|
|
280
|
+
const commonTags = this.generateCommonTags();
|
|
281
|
+
const announcementMapping = [
|
|
282
|
+
{
|
|
283
|
+
schema: InitializeResultSchema,
|
|
284
|
+
kind: SERVER_ANNOUNCEMENT_KIND,
|
|
285
|
+
tags: commonTags,
|
|
286
|
+
},
|
|
287
|
+
{ schema: ListToolsResultSchema, kind: TOOLS_LIST_KIND, tags: [] },
|
|
288
|
+
{
|
|
289
|
+
schema: ListResourcesResultSchema,
|
|
290
|
+
kind: RESOURCES_LIST_KIND,
|
|
291
|
+
tags: [],
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
schema: ListResourceTemplatesResultSchema,
|
|
295
|
+
kind: RESOURCETEMPLATES_LIST_KIND,
|
|
296
|
+
tags: [],
|
|
297
|
+
},
|
|
298
|
+
{ schema: ListPromptsResultSchema, kind: PROMPTS_LIST_KIND, tags: [] },
|
|
299
|
+
];
|
|
300
|
+
for (const mapping of announcementMapping) {
|
|
301
|
+
if (mapping.schema.safeParse(message.result).success) {
|
|
302
|
+
await this.sendMcpMessage(message.result, recipientPubkey, mapping.kind, mapping.tags);
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
225
305
|
}
|
|
226
306
|
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
logger.error('Error in announcer', {
|
|
309
|
+
error: error instanceof Error ? error.message : String(error),
|
|
310
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
311
|
+
});
|
|
312
|
+
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error instanceof Error ? error : new Error(String(error)));
|
|
313
|
+
}
|
|
227
314
|
}
|
|
228
315
|
/**
|
|
229
316
|
* Gets or creates a client session with proper initialization.
|
|
@@ -240,6 +327,7 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
240
327
|
isEncrypted,
|
|
241
328
|
lastActivity: now,
|
|
242
329
|
pendingRequests: new Map(),
|
|
330
|
+
eventToProgressToken: new Map(),
|
|
243
331
|
};
|
|
244
332
|
this.clientSessions.set(clientPubkey, newSession);
|
|
245
333
|
return newSession;
|
|
@@ -253,7 +341,7 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
253
341
|
* @param eventId The Nostr event ID.
|
|
254
342
|
* @param request The request message.
|
|
255
343
|
*/
|
|
256
|
-
handleIncomingRequest(session, eventId, request) {
|
|
344
|
+
handleIncomingRequest(session, eventId, request, clientPubkey) {
|
|
257
345
|
var _a, _b;
|
|
258
346
|
// Store the original request ID for later restoration
|
|
259
347
|
const originalRequestId = request.id;
|
|
@@ -261,10 +349,13 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
261
349
|
request.id = eventId;
|
|
262
350
|
// Store in client session
|
|
263
351
|
session.pendingRequests.set(eventId, originalRequestId);
|
|
352
|
+
this.eventIdToClient.set(eventId, clientPubkey);
|
|
264
353
|
// Track progress tokens if provided
|
|
265
354
|
const progressToken = (_b = (_a = request.params) === null || _a === void 0 ? void 0 : _a._meta) === null || _b === void 0 ? void 0 : _b.progressToken;
|
|
266
355
|
if (progressToken) {
|
|
267
|
-
|
|
356
|
+
const tokenStr = String(progressToken);
|
|
357
|
+
session.pendingRequests.set(tokenStr, eventId);
|
|
358
|
+
session.eventToProgressToken.set(eventId, tokenStr);
|
|
268
359
|
}
|
|
269
360
|
}
|
|
270
361
|
/**
|
|
@@ -283,48 +374,45 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
283
374
|
* @param response The JSON-RPC response or error to send.
|
|
284
375
|
*/
|
|
285
376
|
async handleResponse(response) {
|
|
286
|
-
var _a, _b, _c;
|
|
377
|
+
var _a, _b, _c, _d, _e;
|
|
287
378
|
// Handle special announcement responses
|
|
288
379
|
if (response.id === 'announcement') {
|
|
289
380
|
if (isJSONRPCResponse(response)) {
|
|
290
381
|
if (InitializeResultSchema.safeParse(response.result).success) {
|
|
291
382
|
this.isInitialized = true;
|
|
383
|
+
(_a = this.initializationResolver) === null || _a === void 0 ? void 0 : _a.call(this); // Resolve waiting promise
|
|
292
384
|
// Send the initialized notification
|
|
293
385
|
const initializedNotification = {
|
|
294
386
|
jsonrpc: '2.0',
|
|
295
387
|
method: 'notifications/initialized',
|
|
296
388
|
};
|
|
297
|
-
(
|
|
389
|
+
(_b = this.onmessage) === null || _b === void 0 ? void 0 : _b.call(this, initializedNotification);
|
|
298
390
|
logger.info('Initialized');
|
|
299
391
|
}
|
|
300
392
|
await this.announcer(response);
|
|
301
393
|
}
|
|
302
394
|
return;
|
|
303
395
|
}
|
|
304
|
-
// Find the client session with this pending request
|
|
396
|
+
// Find the client session with this pending request using O(1) lookup
|
|
305
397
|
const nostrEventId = response.id;
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const originalId = session.pendingRequests.get(nostrEventId);
|
|
310
|
-
if (originalId !== undefined) {
|
|
311
|
-
targetClientPubkey = clientPubkey;
|
|
312
|
-
originalRequestId = originalId;
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
if (!targetClientPubkey || originalRequestId === undefined) {
|
|
317
|
-
(_b = this.onerror) === null || _b === void 0 ? void 0 : _b.call(this, new Error(`No pending request found for response ID: ${response.id}`));
|
|
398
|
+
const targetClientPubkey = this.eventIdToClient.get(nostrEventId);
|
|
399
|
+
if (!targetClientPubkey) {
|
|
400
|
+
(_c = this.onerror) === null || _c === void 0 ? void 0 : _c.call(this, new Error(`No pending request found for response ID: ${response.id}`));
|
|
318
401
|
return;
|
|
319
402
|
}
|
|
320
|
-
// Restore the original request ID in the response
|
|
321
|
-
response.id = originalRequestId;
|
|
322
|
-
// Send the response back to the original requester
|
|
323
403
|
const session = this.clientSessions.get(targetClientPubkey);
|
|
324
404
|
if (!session) {
|
|
325
|
-
(
|
|
405
|
+
(_d = this.onerror) === null || _d === void 0 ? void 0 : _d.call(this, new Error(`No session found for client: ${targetClientPubkey}`));
|
|
326
406
|
return;
|
|
327
407
|
}
|
|
408
|
+
const originalRequestId = session.pendingRequests.get(nostrEventId);
|
|
409
|
+
if (originalRequestId === undefined) {
|
|
410
|
+
(_e = this.onerror) === null || _e === void 0 ? void 0 : _e.call(this, new Error(`No original request ID found for response ID: ${response.id}`));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
// Restore the original request ID in the response
|
|
414
|
+
response.id = originalRequestId;
|
|
415
|
+
// Send the response back to the original requester
|
|
328
416
|
const tags = this.createResponseTags(targetClientPubkey, nostrEventId);
|
|
329
417
|
if (isJSONRPCResponse(response) &&
|
|
330
418
|
InitializeResultSchema.safeParse(response.result).success &&
|
|
@@ -336,19 +424,13 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
336
424
|
}
|
|
337
425
|
await this.sendMcpMessage(response, targetClientPubkey, CTXVM_MESSAGES_KIND, tags, session.isEncrypted);
|
|
338
426
|
// Clean up the pending request and any associated progress token
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
break;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
if (progressTokenToDelete !== undefined) {
|
|
350
|
-
session.pendingRequests.delete(String(progressTokenToDelete));
|
|
351
|
-
}
|
|
427
|
+
session.pendingRequests.delete(nostrEventId);
|
|
428
|
+
this.eventIdToClient.delete(nostrEventId);
|
|
429
|
+
// Clean up progress token if it exists
|
|
430
|
+
const progressToken = session.eventToProgressToken.get(nostrEventId);
|
|
431
|
+
if (progressToken) {
|
|
432
|
+
session.pendingRequests.delete(progressToken);
|
|
433
|
+
session.eventToProgressToken.delete(nostrEventId);
|
|
352
434
|
}
|
|
353
435
|
}
|
|
354
436
|
/**
|
|
@@ -356,30 +438,60 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
356
438
|
* @param notification The JSON-RPC notification to send.
|
|
357
439
|
*/
|
|
358
440
|
async handleNotification(notification) {
|
|
359
|
-
var _a, _b, _c;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
notification
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
441
|
+
var _a, _b, _c, _d, _e;
|
|
442
|
+
try {
|
|
443
|
+
// Special handling for progress notifications
|
|
444
|
+
// TODO: Add handling for `notifications/resources/updated`, as they need to be associated with an id
|
|
445
|
+
if (isJSONRPCNotification(notification) &&
|
|
446
|
+
notification.method === 'notifications/progress' &&
|
|
447
|
+
((_b = (_a = notification.params) === null || _a === void 0 ? void 0 : _a._meta) === null || _b === void 0 ? void 0 : _b.progressToken)) {
|
|
448
|
+
const token = String(notification.params._meta.progressToken);
|
|
449
|
+
// Use reverse lookup map for O(1) progress token routing
|
|
450
|
+
// First find the session that has this progress token
|
|
451
|
+
let targetClientPubkey;
|
|
452
|
+
let nostrEventId;
|
|
453
|
+
for (const [clientPubkey, session] of this.clientSessions.entries()) {
|
|
454
|
+
if (session.pendingRequests.has(token)) {
|
|
455
|
+
nostrEventId = session.pendingRequests.get(token);
|
|
456
|
+
targetClientPubkey = clientPubkey;
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (targetClientPubkey && nostrEventId) {
|
|
461
|
+
await this.sendNotification(targetClientPubkey, notification, nostrEventId);
|
|
370
462
|
return;
|
|
371
463
|
}
|
|
464
|
+
const error = new Error(`No client found for progress token: ${token}`);
|
|
465
|
+
logger.error('Progress token not found', { token });
|
|
466
|
+
(_c = this.onerror) === null || _c === void 0 ? void 0 : _c.call(this, error);
|
|
467
|
+
return;
|
|
372
468
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (session.isInitialized) {
|
|
379
|
-
promises.push(this.sendNotification(clientPubkey, notification));
|
|
469
|
+
const promises = [];
|
|
470
|
+
for (const [clientPubkey, session] of this.clientSessions.entries()) {
|
|
471
|
+
if (session.isInitialized) {
|
|
472
|
+
promises.push(this.sendNotification(clientPubkey, notification));
|
|
473
|
+
}
|
|
380
474
|
}
|
|
475
|
+
try {
|
|
476
|
+
await Promise.all(promises);
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
logger.error('Error broadcasting notification', {
|
|
480
|
+
error: error instanceof Error ? error.message : String(error),
|
|
481
|
+
method: isJSONRPCNotification(notification)
|
|
482
|
+
? notification.method
|
|
483
|
+
: 'unknown',
|
|
484
|
+
});
|
|
485
|
+
(_d = this.onerror) === null || _d === void 0 ? void 0 : _d.call(this, error instanceof Error ? error : new Error(String(error)));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
logger.error('Error in handleNotification', {
|
|
490
|
+
error: error instanceof Error ? error.message : String(error),
|
|
491
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
492
|
+
});
|
|
493
|
+
(_e = this.onerror) === null || _e === void 0 ? void 0 : _e.call(this, error instanceof Error ? error : new Error(String(error)));
|
|
381
494
|
}
|
|
382
|
-
await Promise.all(promises);
|
|
383
495
|
}
|
|
384
496
|
/**
|
|
385
497
|
* Sends a notification to a specific client by their public key.
|
|
@@ -406,11 +518,23 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
406
518
|
* @param event The incoming Nostr event.
|
|
407
519
|
*/
|
|
408
520
|
async processIncomingEvent(event) {
|
|
409
|
-
|
|
410
|
-
|
|
521
|
+
var _a;
|
|
522
|
+
try {
|
|
523
|
+
if (event.kind === GIFT_WRAP_KIND) {
|
|
524
|
+
await this.handleEncryptedEvent(event);
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
this.handleUnencryptedEvent(event);
|
|
528
|
+
}
|
|
411
529
|
}
|
|
412
|
-
|
|
413
|
-
|
|
530
|
+
catch (error) {
|
|
531
|
+
logger.error('Error in processIncomingEvent', {
|
|
532
|
+
error: error instanceof Error ? error.message : String(error),
|
|
533
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
534
|
+
eventId: event.id,
|
|
535
|
+
eventKind: event.kind,
|
|
536
|
+
});
|
|
537
|
+
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error instanceof Error ? error : new Error(String(error)));
|
|
414
538
|
}
|
|
415
539
|
}
|
|
416
540
|
/**
|
|
@@ -429,6 +553,12 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
429
553
|
this.authorizeAndProcessEvent(currentEvent, true);
|
|
430
554
|
}
|
|
431
555
|
catch (error) {
|
|
556
|
+
logger.error('Failed to handle encrypted Nostr event', {
|
|
557
|
+
error: error instanceof Error ? error.message : String(error),
|
|
558
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
559
|
+
eventId: event.id,
|
|
560
|
+
pubkey: event.pubkey,
|
|
561
|
+
});
|
|
432
562
|
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error instanceof Error
|
|
433
563
|
? error
|
|
434
564
|
: new Error('Failed to handle encrypted Nostr event'));
|
|
@@ -479,66 +609,93 @@ export class NostrServerTransport extends BaseNostrTransport {
|
|
|
479
609
|
* @param isEncrypted Whether the original event was encrypted.
|
|
480
610
|
*/
|
|
481
611
|
authorizeAndProcessEvent(event, isEncrypted) {
|
|
482
|
-
var _a, _b, _c, _d;
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
(isJSONRPCRequest(mcpMessage) || isJSONRPCNotification(mcpMessage)) &&
|
|
492
|
-
this.isCapabilityExcluded(mcpMessage.method, (_c = mcpMessage.params) === null || _c === void 0 ? void 0 : _c.name);
|
|
493
|
-
if (!this.allowedPublicKeys.includes(event.pubkey) &&
|
|
494
|
-
!shouldBypassWhitelisting) {
|
|
495
|
-
logger.error(`Unauthorized message from ${event.pubkey}, message: ${JSON.stringify(mcpMessage)}. Ignoring.`);
|
|
496
|
-
if (this.isPublicServer && isJSONRPCRequest(mcpMessage)) {
|
|
497
|
-
const errorResponse = {
|
|
498
|
-
jsonrpc: '2.0',
|
|
499
|
-
id: mcpMessage.id,
|
|
500
|
-
error: {
|
|
501
|
-
code: -32000,
|
|
502
|
-
message: 'Unauthorized',
|
|
503
|
-
},
|
|
504
|
-
};
|
|
505
|
-
const tags = this.createResponseTags(event.pubkey, event.id);
|
|
506
|
-
this.sendMcpMessage(errorResponse, event.pubkey, CTXVM_MESSAGES_KIND, tags, isEncrypted).catch((err) => {
|
|
507
|
-
var _a;
|
|
508
|
-
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, new Error(`Failed to send unauthorized response: ${err}`));
|
|
509
|
-
});
|
|
510
|
-
}
|
|
612
|
+
var _a, _b, _c, _d, _e;
|
|
613
|
+
try {
|
|
614
|
+
const mcpMessage = this.convertNostrEventToMcpMessage(event);
|
|
615
|
+
if (!mcpMessage) {
|
|
616
|
+
logger.error('Skipping invalid Nostr event with malformed JSON content', {
|
|
617
|
+
eventId: event.id,
|
|
618
|
+
pubkey: event.pubkey,
|
|
619
|
+
content: event.content,
|
|
620
|
+
});
|
|
511
621
|
return;
|
|
512
622
|
}
|
|
623
|
+
if ((_a = this.allowedPublicKeys) === null || _a === void 0 ? void 0 : _a.length) {
|
|
624
|
+
// Check if the message should bypass whitelisting due to excluded capabilities
|
|
625
|
+
const shouldBypassWhitelisting = ((_b = this.excludedCapabilities) === null || _b === void 0 ? void 0 : _b.length) &&
|
|
626
|
+
(isJSONRPCRequest(mcpMessage) || isJSONRPCNotification(mcpMessage)) &&
|
|
627
|
+
this.isCapabilityExcluded(mcpMessage.method, (_c = mcpMessage.params) === null || _c === void 0 ? void 0 : _c.name);
|
|
628
|
+
if (!this.allowedPublicKeys.includes(event.pubkey) &&
|
|
629
|
+
!shouldBypassWhitelisting) {
|
|
630
|
+
logger.error(`Unauthorized message from ${event.pubkey}, message: ${JSON.stringify(mcpMessage)}. Ignoring.`);
|
|
631
|
+
if (this.isPublicServer && isJSONRPCRequest(mcpMessage)) {
|
|
632
|
+
const errorResponse = {
|
|
633
|
+
jsonrpc: '2.0',
|
|
634
|
+
id: mcpMessage.id,
|
|
635
|
+
error: {
|
|
636
|
+
code: -32000,
|
|
637
|
+
message: 'Unauthorized',
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
const tags = this.createResponseTags(event.pubkey, event.id);
|
|
641
|
+
this.sendMcpMessage(errorResponse, event.pubkey, CTXVM_MESSAGES_KIND, tags, isEncrypted).catch((err) => {
|
|
642
|
+
var _a;
|
|
643
|
+
logger.error('Failed to send unauthorized response', {
|
|
644
|
+
error: err instanceof Error ? err.message : String(err),
|
|
645
|
+
pubkey: event.pubkey,
|
|
646
|
+
eventId: event.id,
|
|
647
|
+
});
|
|
648
|
+
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, new Error(`Failed to send unauthorized response: ${err}`));
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const now = Date.now();
|
|
655
|
+
const session = this.getOrCreateClientSession(event.pubkey, now, isEncrypted);
|
|
656
|
+
session.lastActivity = now;
|
|
657
|
+
if (isJSONRPCRequest(mcpMessage)) {
|
|
658
|
+
this.handleIncomingRequest(session, event.id, mcpMessage, event.pubkey);
|
|
659
|
+
}
|
|
660
|
+
else if (isJSONRPCNotification(mcpMessage)) {
|
|
661
|
+
this.handleIncomingNotification(session, mcpMessage);
|
|
662
|
+
}
|
|
663
|
+
(_d = this.onmessage) === null || _d === void 0 ? void 0 : _d.call(this, mcpMessage);
|
|
513
664
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
this.
|
|
665
|
+
catch (error) {
|
|
666
|
+
logger.error('Error in authorizeAndProcessEvent', {
|
|
667
|
+
error: error instanceof Error ? error.message : String(error),
|
|
668
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
669
|
+
eventId: event.id,
|
|
670
|
+
pubkey: event.pubkey,
|
|
671
|
+
});
|
|
672
|
+
(_e = this.onerror) === null || _e === void 0 ? void 0 : _e.call(this, error instanceof Error ? error : new Error(String(error)));
|
|
522
673
|
}
|
|
523
|
-
(_d = this.onmessage) === null || _d === void 0 ? void 0 : _d.call(this, mcpMessage);
|
|
524
674
|
}
|
|
525
675
|
/**
|
|
526
676
|
* Cleans up inactive client sessions based on a timeout.
|
|
527
677
|
* @param timeoutMs Timeout in milliseconds for considering a session inactive (default: 5 minutes).
|
|
528
678
|
* @returns The number of sessions that were cleaned up.
|
|
529
679
|
*/
|
|
530
|
-
cleanupInactiveSessions(timeoutMs
|
|
680
|
+
cleanupInactiveSessions(timeoutMs) {
|
|
531
681
|
const now = Date.now();
|
|
532
|
-
const
|
|
682
|
+
const timeout = timeoutMs !== null && timeoutMs !== void 0 ? timeoutMs : this.sessionTimeoutMs;
|
|
683
|
+
let cleaned = 0;
|
|
533
684
|
for (const [clientPubkey, session] of this.clientSessions.entries()) {
|
|
534
|
-
if (now - session.lastActivity >
|
|
535
|
-
|
|
685
|
+
if (now - session.lastActivity > timeout) {
|
|
686
|
+
// Clean up reverse lookup mappings for this session
|
|
687
|
+
for (const eventId of session.pendingRequests.keys()) {
|
|
688
|
+
this.eventIdToClient.delete(eventId);
|
|
689
|
+
}
|
|
690
|
+
// Clean up progress token mappings
|
|
691
|
+
for (const eventId of session.eventToProgressToken.keys()) {
|
|
692
|
+
this.eventIdToClient.delete(eventId);
|
|
693
|
+
}
|
|
694
|
+
this.clientSessions.delete(clientPubkey);
|
|
695
|
+
cleaned++;
|
|
536
696
|
}
|
|
537
697
|
}
|
|
538
|
-
|
|
539
|
-
this.clientSessions.delete(key);
|
|
540
|
-
}
|
|
541
|
-
return keysToDelete.length;
|
|
698
|
+
return cleaned;
|
|
542
699
|
}
|
|
543
700
|
}
|
|
544
701
|
//# sourceMappingURL=nostr-server-transport.js.map
|