@contextvm/sdk 0.1.48 → 0.2.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 (58) hide show
  1. package/dist/esm/core/constants.d.ts +3 -0
  2. package/dist/esm/core/constants.d.ts.map +1 -1
  3. package/dist/esm/core/constants.js +3 -0
  4. package/dist/esm/core/constants.js.map +1 -1
  5. package/dist/esm/core/utils/logger.d.ts +1 -1
  6. package/dist/esm/core/utils/logger.d.ts.map +1 -1
  7. package/dist/esm/core/utils/lru-cache.d.ts.map +1 -1
  8. package/dist/esm/core/utils/lru-cache.js +9 -1
  9. package/dist/esm/core/utils/lru-cache.js.map +1 -1
  10. package/dist/esm/core/utils/task-queue.d.ts +5 -0
  11. package/dist/esm/core/utils/task-queue.d.ts.map +1 -1
  12. package/dist/esm/core/utils/task-queue.js +11 -0
  13. package/dist/esm/core/utils/task-queue.js.map +1 -1
  14. package/dist/esm/core/utils/utils.d.ts +8 -0
  15. package/dist/esm/core/utils/utils.d.ts.map +1 -1
  16. package/dist/esm/core/utils/utils.js +23 -0
  17. package/dist/esm/core/utils/utils.js.map +1 -1
  18. package/dist/esm/relay/applesauce-relay-pool.d.ts +37 -4
  19. package/dist/esm/relay/applesauce-relay-pool.d.ts.map +1 -1
  20. package/dist/esm/relay/applesauce-relay-pool.js +165 -11
  21. package/dist/esm/relay/applesauce-relay-pool.js.map +1 -1
  22. package/dist/esm/transport/base-nostr-transport.d.ts +1 -1
  23. package/dist/esm/transport/base-nostr-transport.d.ts.map +1 -1
  24. package/dist/esm/transport/base-nostr-transport.js +8 -6
  25. package/dist/esm/transport/base-nostr-transport.js.map +1 -1
  26. package/dist/esm/transport/nostr-client/correlation-store.d.ts +60 -0
  27. package/dist/esm/transport/nostr-client/correlation-store.d.ts.map +1 -0
  28. package/dist/esm/transport/nostr-client/correlation-store.js +61 -0
  29. package/dist/esm/transport/nostr-client/correlation-store.js.map +1 -0
  30. package/dist/esm/transport/nostr-client/stateless-mode-handler.d.ts +24 -0
  31. package/dist/esm/transport/nostr-client/stateless-mode-handler.d.ts.map +1 -0
  32. package/dist/esm/transport/nostr-client/stateless-mode-handler.js +61 -0
  33. package/dist/esm/transport/nostr-client/stateless-mode-handler.js.map +1 -0
  34. package/dist/esm/transport/nostr-client-transport.d.ts +48 -24
  35. package/dist/esm/transport/nostr-client-transport.d.ts.map +1 -1
  36. package/dist/esm/transport/nostr-client-transport.js +92 -131
  37. package/dist/esm/transport/nostr-client-transport.js.map +1 -1
  38. package/dist/esm/transport/nostr-server/announcement-manager.d.ts +116 -0
  39. package/dist/esm/transport/nostr-server/announcement-manager.d.ts.map +1 -0
  40. package/dist/esm/transport/nostr-server/announcement-manager.js +288 -0
  41. package/dist/esm/transport/nostr-server/announcement-manager.js.map +1 -0
  42. package/dist/esm/transport/nostr-server/authorization-policy.d.ts +74 -0
  43. package/dist/esm/transport/nostr-server/authorization-policy.d.ts.map +1 -0
  44. package/dist/esm/transport/nostr-server/authorization-policy.js +91 -0
  45. package/dist/esm/transport/nostr-server/authorization-policy.js.map +1 -0
  46. package/dist/esm/transport/nostr-server/correlation-store.d.ts +102 -0
  47. package/dist/esm/transport/nostr-server/correlation-store.d.ts.map +1 -0
  48. package/dist/esm/transport/nostr-server/correlation-store.js +167 -0
  49. package/dist/esm/transport/nostr-server/correlation-store.js.map +1 -0
  50. package/dist/esm/transport/nostr-server/session-store.d.ts +99 -0
  51. package/dist/esm/transport/nostr-server/session-store.d.ts.map +1 -0
  52. package/dist/esm/transport/nostr-server/session-store.js +123 -0
  53. package/dist/esm/transport/nostr-server/session-store.js.map +1 -0
  54. package/dist/esm/transport/nostr-server-transport.d.ts +18 -63
  55. package/dist/esm/transport/nostr-server-transport.d.ts.map +1 -1
  56. package/dist/esm/transport/nostr-server-transport.js +135 -378
  57. package/dist/esm/transport/nostr-server-transport.js.map +1 -1
  58. package/package.json +11 -12
@@ -1,10 +1,12 @@
1
- import { InitializeResultSchema, isJSONRPCRequest, isJSONRPCNotification, LATEST_PROTOCOL_VERSION, ListPromptsResultSchema, ListResourcesResultSchema, ListResourceTemplatesResultSchema, ListToolsResultSchema, isJSONRPCResultResponse, isJSONRPCErrorResponse, } from '@modelcontextprotocol/sdk/types.js';
1
+ import { InitializeResultSchema, isJSONRPCRequest, isJSONRPCNotification, isJSONRPCResultResponse, isJSONRPCErrorResponse, } from '@modelcontextprotocol/sdk/types.js';
2
2
  import { BaseNostrTransport, } from './base-nostr-transport.js';
3
- import { announcementMethods, CTXVM_MESSAGES_KIND, GIFT_WRAP_KIND, NOSTR_TAGS, PROMPTS_LIST_KIND, RESOURCES_LIST_KIND, RESOURCETEMPLATES_LIST_KIND, SERVER_ANNOUNCEMENT_KIND, TOOLS_LIST_KIND, decryptMessage, } from '../core/index.js';
3
+ import { CTXVM_MESSAGES_KIND, GIFT_WRAP_KIND, NOSTR_TAGS, NOTIFICATIONS_INITIALIZED_METHOD, decryptMessage, } from '../core/index.js';
4
4
  import { EncryptionMode } from '../core/interfaces.js';
5
- import { EventDeletion } from 'nostr-tools/kinds';
6
- import { LruCache } from '../core/utils/lru-cache.js';
7
- import { injectClientPubkey } from '../core/utils/utils.js';
5
+ import { injectClientPubkey, withTimeout } from '../core/utils/utils.js';
6
+ import { CorrelationStore } from './nostr-server/correlation-store.js';
7
+ import { SessionStore } from './nostr-server/session-store.js';
8
+ import { AuthorizationPolicy, } from './nostr-server/authorization-policy.js';
9
+ import { AnnouncementManager, } from './nostr-server/announcement-manager.js';
8
10
  /**
9
11
  * A server-side transport layer for CTXVM that uses Nostr events for communication.
10
12
  * This transport listens for incoming MCP requests via Nostr events and can send
@@ -15,55 +17,45 @@ export class NostrServerTransport extends BaseNostrTransport {
15
17
  constructor(options) {
16
18
  var _a;
17
19
  super('nostr-server-transport', options);
18
- this.eventIdToClient = new Map(); // eventId -> clientPubkey
19
- this.maxSessions = 1000; // LRU cache limit
20
- this.isInitialized = false;
21
- this.serverInfo = options.serverInfo;
22
- this.isPublicServer = options.isPublicServer;
23
- this.allowedPublicKeys = options.allowedPublicKeys
24
- ? new Set(options.allowedPublicKeys)
25
- : undefined;
26
- this.excludedCapabilities = options.excludedCapabilities;
27
20
  this.injectClientPubkey = (_a = options.injectClientPubkey) !== null && _a !== void 0 ? _a : false;
28
- // Initialize LRU cache with eviction callback for cleanup
29
- this.clientSessions = new LruCache(this.maxSessions, (key, session) => {
30
- // Clean up reverse lookup mappings for evicted session
31
- for (const eventId of session.pendingRequests.keys()) {
32
- this.eventIdToClient.delete(eventId);
33
- }
34
- for (const eventId of session.eventToProgressToken.keys()) {
35
- this.eventIdToClient.delete(eventId);
36
- }
37
- this.logger.info(`Evicted LRU session for ${key}`);
21
+ // Initialize authorization policy
22
+ this.authorizationPolicy = new AuthorizationPolicy({
23
+ allowedPublicKeys: options.allowedPublicKeys
24
+ ? new Set(options.allowedPublicKeys)
25
+ : undefined,
26
+ excludedCapabilities: options.excludedCapabilities,
27
+ isPublicServer: options.isPublicServer,
28
+ });
29
+ // Initialize session store with eviction callback for correlation cleanup
30
+ this.sessionStore = new SessionStore({
31
+ maxSessions: 1000,
32
+ onSessionEvicted: (clientPubkey) => {
33
+ // Clean up all correlation data for evicted session
34
+ const removedCount = this.correlationStore.removeRoutesForClient(clientPubkey);
35
+ this.logger.info(`Evicted session for ${clientPubkey} (removed ${removedCount} routes)`);
36
+ },
37
+ });
38
+ // Initialize correlation store with bounded caches
39
+ this.correlationStore = new CorrelationStore({
40
+ maxEventRoutes: 10000,
41
+ maxProgressTokens: 10000,
42
+ onEventRouteEvicted: (eventId, route) => {
43
+ this.logger.debug(`Evicted event route for ${eventId}`, {
44
+ clientPubkey: route.clientPubkey,
45
+ });
46
+ },
47
+ });
48
+ // Initialize announcement manager
49
+ this.announcementManager = new AnnouncementManager({
50
+ serverInfo: options.serverInfo,
51
+ encryptionMode: this.encryptionMode,
52
+ onSendMessage: (message) => { var _a; return (_a = this.onmessage) === null || _a === void 0 ? void 0 : _a.call(this, message); },
53
+ onPublishEvent: (event) => this.publishEvent(event),
54
+ onSignEvent: (eventTemplate) => this.signer.signEvent(eventTemplate),
55
+ onGetPublicKey: () => this.getPublicKey(),
56
+ onSubscribe: (filters, onEvent) => this.relayHandler.subscribe(filters, onEvent),
57
+ logger: this.logger,
38
58
  });
39
- }
40
- /**
41
- * Generates common tags from server information for use in Nostr events.
42
- * @returns Array of tag arrays for Nostr events.
43
- */
44
- generateCommonTags() {
45
- var _a, _b, _c, _d;
46
- if (this.cachedCommonTags) {
47
- return this.cachedCommonTags;
48
- }
49
- const commonTags = [];
50
- if ((_a = this.serverInfo) === null || _a === void 0 ? void 0 : _a.name) {
51
- commonTags.push([NOSTR_TAGS.NAME, this.serverInfo.name]);
52
- }
53
- if ((_b = this.serverInfo) === null || _b === void 0 ? void 0 : _b.about) {
54
- commonTags.push([NOSTR_TAGS.ABOUT, this.serverInfo.about]);
55
- }
56
- if ((_c = this.serverInfo) === null || _c === void 0 ? void 0 : _c.website) {
57
- commonTags.push([NOSTR_TAGS.WEBSITE, this.serverInfo.website]);
58
- }
59
- if ((_d = this.serverInfo) === null || _d === void 0 ? void 0 : _d.picture) {
60
- commonTags.push([NOSTR_TAGS.PICTURE, this.serverInfo.picture]);
61
- }
62
- if (this.encryptionMode !== EncryptionMode.DISABLED) {
63
- commonTags.push([NOSTR_TAGS.SUPPORT_ENCRYPTION]);
64
- }
65
- this.cachedCommonTags = commonTags;
66
- return commonTags;
67
59
  }
68
60
  /**
69
61
  * Starts the transport, connecting to the relay and setting up event listeners
@@ -94,17 +86,13 @@ export class NostrServerTransport extends BaseNostrTransport {
94
86
  (_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error instanceof Error ? error : new Error(String(error)));
95
87
  }
96
88
  });
97
- if (this.isPublicServer) {
98
- await this.getAnnouncementData();
89
+ if (this.authorizationPolicy.isPublicServer) {
90
+ await this.announcementManager.getAnnouncementData();
99
91
  }
100
92
  }
101
93
  catch (error) {
102
- this.logger.error('Error starting NostrServerTransport', {
103
- error: error instanceof Error ? error.message : String(error),
104
- stack: error instanceof Error ? error.stack : undefined,
105
- });
106
94
  (_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error instanceof Error ? error : new Error(String(error)));
107
- throw error;
95
+ this.logAndRethrowError('Error starting NostrServerTransport', error);
108
96
  }
109
97
  }
110
98
  /**
@@ -113,17 +101,17 @@ export class NostrServerTransport extends BaseNostrTransport {
113
101
  async close() {
114
102
  var _a, _b;
115
103
  try {
104
+ // Shutdown the task queue to prevent new tasks from being queued
105
+ // and clear pending tasks to avoid operating on stale state
106
+ this.taskQueue.shutdown();
116
107
  await this.disconnect();
117
- this.clientSessions.clear();
108
+ this.sessionStore.clear();
109
+ this.correlationStore.clear();
118
110
  (_a = this.onclose) === null || _a === void 0 ? void 0 : _a.call(this);
119
111
  }
120
112
  catch (error) {
121
- this.logger.error('Error closing NostrServerTransport', {
122
- error: error instanceof Error ? error.message : String(error),
123
- stack: error instanceof Error ? error.stack : undefined,
124
- });
125
113
  (_b = this.onerror) === null || _b === void 0 ? void 0 : _b.call(this, error instanceof Error ? error : new Error(String(error)));
126
- throw error;
114
+ this.logAndRethrowError('Error closing NostrServerTransport', error);
127
115
  }
128
116
  }
129
117
  /**
@@ -151,230 +139,46 @@ export class NostrServerTransport extends BaseNostrTransport {
151
139
  * @returns Promise that resolves to an array of deletion events that were published.
152
140
  */
153
141
  async deleteAnnouncement(reason = 'Service offline') {
154
- const publicKey = await this.getPublicKey();
155
- const events = [];
156
- const kinds = [
157
- SERVER_ANNOUNCEMENT_KIND,
158
- TOOLS_LIST_KIND,
159
- RESOURCES_LIST_KIND,
160
- RESOURCETEMPLATES_LIST_KIND,
161
- PROMPTS_LIST_KIND,
162
- ];
163
- for (const kind of kinds) {
164
- const filter = {
165
- kinds: [kind],
166
- authors: [publicKey],
167
- };
168
- // Collect events using the subscribe method with onEvent hook
169
- await this.relayHandler.subscribe([filter], (event) => {
170
- var _a;
171
- try {
172
- events.push(event);
173
- }
174
- catch (error) {
175
- this.logger.error('Error in relay subscription event collection', {
176
- error: error instanceof Error ? error.message : String(error),
177
- eventId: event.id,
178
- });
179
- (_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error instanceof Error ? error : new Error(String(error)));
180
- }
181
- });
182
- if (!events.length) {
183
- this.logger.info(`No events found for kind ${kind} to delete`);
184
- continue;
185
- }
186
- const deletionEventTemplate = {
187
- kind: EventDeletion,
188
- pubkey: publicKey,
189
- content: reason,
190
- tags: events.map((ev) => ['e', ev.id]),
191
- created_at: Math.floor(Date.now() / 1000),
192
- };
193
- const deletionEvent = await this.signer.signEvent(deletionEventTemplate);
194
- await this.relayHandler.publish(deletionEvent);
195
- this.logger.info(`Published deletion event for kind ${kind} (${events.length} events)`);
196
- }
197
- return events;
198
- }
199
- /**
200
- * Initiates the process of fetching announcement data from the server's internal logic.
201
- * This method now properly handles the initialization handshake by first sending
202
- * the initialize request, waiting for the response, and then proceeding with other announcements.
203
- */
204
- async getAnnouncementData() {
205
- var _a, _b, _c;
206
- try {
207
- const initializeParams = {
208
- protocolVersion: LATEST_PROTOCOL_VERSION,
209
- capabilities: {},
210
- clientInfo: {
211
- name: 'DummyClient',
212
- version: '1.0.0',
213
- },
214
- };
215
- // Send the initialize request if not already initialized
216
- if (!this.isInitialized) {
217
- const initializeMessage = {
218
- jsonrpc: '2.0',
219
- id: 'announcement',
220
- method: 'initialize',
221
- params: initializeParams,
222
- };
223
- this.logger.info('Sending initialize request for announcement');
224
- (_a = this.onmessage) === null || _a === void 0 ? void 0 : _a.call(this, initializeMessage);
225
- }
226
- try {
227
- // Wait for initialization to complete
228
- await this.waitForInitialization();
229
- // Send all announcements now that we're initialized
230
- for (const [key, methodValue] of Object.entries(announcementMethods)) {
231
- this.logger.info('Sending announcement', { key, methodValue });
232
- const message = {
233
- jsonrpc: '2.0',
234
- id: 'announcement',
235
- method: methodValue,
236
- params: key === 'server' ? initializeParams : {},
237
- };
238
- (_b = this.onmessage) === null || _b === void 0 ? void 0 : _b.call(this, message);
239
- }
240
- }
241
- catch (error) {
242
- this.logger.warn('Server not initialized after waiting, skipping announcements', { error: error instanceof Error ? error.message : error });
243
- }
244
- }
245
- catch (error) {
246
- this.logger.error('Error in getAnnouncementData', {
247
- error: error instanceof Error ? error.message : String(error),
248
- stack: error instanceof Error ? error.stack : undefined,
249
- });
250
- (_c = this.onerror) === null || _c === void 0 ? void 0 : _c.call(this, error instanceof Error ? error : new Error(String(error)));
251
- }
252
- }
253
- /**
254
- * Waits for the server to be initialized with a timeout.
255
- * @returns Promise that resolves when initialized or after 10-second timeout.
256
- * The method will always resolve, allowing announcements to proceed.
257
- */
258
- async waitForInitialization() {
259
- if (this.isInitialized)
260
- return;
261
- if (!this.initializationPromise) {
262
- this.initializationPromise = new Promise((resolve) => {
263
- this.initializationResolver = resolve;
264
- });
265
- }
266
- const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Initialization timeout')), 10000));
267
- try {
268
- await Promise.race([this.initializationPromise, timeout]);
269
- }
270
- catch (_a) {
271
- this.logger.warn('Server initialization not completed within timeout, proceeding with announcements');
272
- }
273
- }
274
- /**
275
- * Handles the JSON-RPC responses for public server announcements and publishes
276
- * them as Nostr events to the configured relays.
277
- * @param message The JSON-RPC response containing the announcement data.
278
- */
279
- async announcer(message) {
280
- var _a;
281
- try {
282
- // Only process successful responses with result data
283
- if (!isJSONRPCResultResponse(message) || !message.result) {
284
- return;
285
- }
286
- const recipientPubkey = await this.getPublicKey();
287
- const commonTags = this.generateCommonTags();
288
- const announcementMapping = [
289
- {
290
- schema: InitializeResultSchema,
291
- kind: SERVER_ANNOUNCEMENT_KIND,
292
- tags: commonTags,
293
- },
294
- { schema: ListToolsResultSchema, kind: TOOLS_LIST_KIND, tags: [] },
295
- {
296
- schema: ListResourcesResultSchema,
297
- kind: RESOURCES_LIST_KIND,
298
- tags: [],
299
- },
300
- {
301
- schema: ListResourceTemplatesResultSchema,
302
- kind: RESOURCETEMPLATES_LIST_KIND,
303
- tags: [],
304
- },
305
- { schema: ListPromptsResultSchema, kind: PROMPTS_LIST_KIND, tags: [] },
306
- ];
307
- for (const mapping of announcementMapping) {
308
- if (mapping.schema.safeParse(message.result).success) {
309
- await this.sendMcpMessage(message.result, recipientPubkey, mapping.kind, mapping.tags);
310
- break;
311
- }
312
- }
313
- }
314
- catch (error) {
315
- this.logger.error('Error in announcer', {
316
- error: error instanceof Error ? error.message : String(error),
317
- stack: error instanceof Error ? error.stack : undefined,
318
- });
319
- (_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error instanceof Error ? error : new Error(String(error)));
320
- }
142
+ return await this.announcementManager.deleteAnnouncement(reason);
321
143
  }
322
144
  /**
323
145
  * Gets or creates a client session with proper initialization.
324
146
  * @param clientPubkey The client's public key.
325
- * @param now Current timestamp.
326
147
  * @param isEncrypted Whether the session uses encryption.
327
148
  * @returns The client session.
328
149
  */
329
- getOrCreateClientSession(clientPubkey, now, isEncrypted) {
330
- const session = this.clientSessions.get(clientPubkey);
150
+ getOrCreateClientSession(clientPubkey, isEncrypted) {
151
+ const session = this.sessionStore.getSession(clientPubkey);
331
152
  if (!session) {
332
153
  this.logger.info(`Session created for ${clientPubkey}`);
333
- const newSession = {
334
- isInitialized: false,
335
- isEncrypted,
336
- lastActivity: now,
337
- pendingRequests: new Map(),
338
- eventToProgressToken: new Map(),
339
- };
340
- this.clientSessions.set(clientPubkey, newSession);
341
- return newSession;
342
154
  }
343
- session.isEncrypted = isEncrypted;
344
- return session;
155
+ return this.sessionStore.getOrCreateSession(clientPubkey, isEncrypted);
345
156
  }
346
157
  /**
347
158
  * Handles incoming requests with correlation tracking.
348
- * @param session The client session.
349
159
  * @param eventId The Nostr event ID.
350
160
  * @param request The request message.
161
+ * @param clientPubkey The client's public key.
351
162
  */
352
- handleIncomingRequest(session, eventId, request, clientPubkey) {
163
+ handleIncomingRequest(eventId, request, clientPubkey) {
353
164
  var _a, _b;
354
165
  // Store the original request ID for later restoration
355
166
  const originalRequestId = request.id;
356
167
  // Use the unique Nostr event ID as the MCP request ID to avoid collisions
357
168
  request.id = eventId;
358
- // Store in client session
359
- session.pendingRequests.set(eventId, originalRequestId);
360
- this.eventIdToClient.set(eventId, clientPubkey);
361
- // Track progress tokens if provided
169
+ // Register the event route in the correlation store
362
170
  const progressToken = (_b = (_a = request.params) === null || _a === void 0 ? void 0 : _a._meta) === null || _b === void 0 ? void 0 : _b.progressToken;
363
- if (progressToken) {
364
- const tokenStr = String(progressToken);
365
- session.pendingRequests.set(tokenStr, eventId);
366
- session.eventToProgressToken.set(eventId, tokenStr);
367
- }
171
+ this.correlationStore.registerEventRoute(eventId, clientPubkey, originalRequestId, progressToken ? String(progressToken) : undefined);
368
172
  }
369
173
  /**
370
174
  * Handles incoming notifications.
371
- * @param session The client session.
175
+ * @param clientPubkey The client's public key.
372
176
  * @param notification The notification message.
373
177
  */
374
- handleIncomingNotification(session, notification) {
178
+ handleIncomingNotification(clientPubkey, notification) {
375
179
  if (isJSONRPCNotification(notification) &&
376
- notification.method === 'notifications/initialized') {
377
- session.isInitialized = true;
180
+ notification.method === NOTIFICATIONS_INITIALIZED_METHOD) {
181
+ this.sessionStore.markInitialized(clientPubkey);
378
182
  }
379
183
  }
380
184
  /**
@@ -382,64 +186,44 @@ export class NostrServerTransport extends BaseNostrTransport {
382
186
  * @param response The JSON-RPC response or error to send.
383
187
  */
384
188
  async handleResponse(response) {
385
- var _a, _b, _c, _d, _e;
189
+ var _a, _b;
386
190
  // Handle special announcement responses
387
191
  if (response.id === 'announcement') {
388
- if (isJSONRPCResultResponse(response)) {
192
+ const wasHandled = await this.announcementManager.handleAnnouncementResponse(response);
193
+ if (wasHandled && isJSONRPCResultResponse(response)) {
389
194
  if (InitializeResultSchema.safeParse(response.result).success) {
390
- this.isInitialized = true;
391
- (_a = this.initializationResolver) === null || _a === void 0 ? void 0 : _a.call(this); // Resolve waiting promise
392
- // Send the initialized notification
393
- const initializedNotification = {
394
- jsonrpc: '2.0',
395
- method: 'notifications/initialized',
396
- };
397
- (_b = this.onmessage) === null || _b === void 0 ? void 0 : _b.call(this, initializedNotification);
398
195
  this.logger.info('Initialized');
399
196
  }
400
- await this.announcer(response);
401
197
  }
402
198
  return;
403
199
  }
404
- // Find the client session with this pending request using O(1) lookup
200
+ // Find the event route using O(1) lookup
405
201
  const nostrEventId = response.id;
406
- const targetClientPubkey = this.eventIdToClient.get(nostrEventId);
407
- if (!targetClientPubkey) {
408
- (_c = this.onerror) === null || _c === void 0 ? void 0 : _c.call(this, new Error(`No pending request found for response ID: ${response.id}`));
202
+ const route = this.correlationStore.getEventRoute(nostrEventId);
203
+ if (!route) {
204
+ (_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, new Error(`No pending request found for response ID: ${response.id}`));
409
205
  return;
410
206
  }
411
- const session = this.clientSessions.get(targetClientPubkey);
207
+ const session = this.sessionStore.getSession(route.clientPubkey);
412
208
  if (!session) {
413
- (_d = this.onerror) === null || _d === void 0 ? void 0 : _d.call(this, new Error(`No session found for client: ${targetClientPubkey}`));
414
- return;
415
- }
416
- const originalRequestId = session.pendingRequests.get(nostrEventId);
417
- if (originalRequestId === undefined) {
418
- (_e = this.onerror) === null || _e === void 0 ? void 0 : _e.call(this, new Error(`No original request ID found for response ID: ${response.id}`));
209
+ (_b = this.onerror) === null || _b === void 0 ? void 0 : _b.call(this, new Error(`No session found for client: ${route.clientPubkey}`));
419
210
  return;
420
211
  }
421
212
  // Restore the original request ID in the response
422
- response.id = originalRequestId;
213
+ response.id = route.originalRequestId;
423
214
  // Send the response back to the original requester
424
- const tags = this.createResponseTags(targetClientPubkey, nostrEventId);
215
+ const tags = this.createResponseTags(route.clientPubkey, nostrEventId);
216
+ // Add common tags for initialize responses (independent of encryption mode)
425
217
  if (isJSONRPCResultResponse(response) &&
426
- InitializeResultSchema.safeParse(response.result).success &&
427
- session.isEncrypted) {
428
- const commonTags = this.generateCommonTags();
218
+ InitializeResultSchema.safeParse(response.result).success) {
219
+ const commonTags = this.announcementManager.getCommonTags();
429
220
  commonTags.forEach((tag) => {
430
221
  tags.push(tag);
431
222
  });
432
223
  }
433
- await this.sendMcpMessage(response, targetClientPubkey, CTXVM_MESSAGES_KIND, tags, session.isEncrypted);
434
- // Clean up the pending request and any associated progress token
435
- session.pendingRequests.delete(nostrEventId);
436
- this.eventIdToClient.delete(nostrEventId);
437
- // Clean up progress token if it exists
438
- const progressToken = session.eventToProgressToken.get(nostrEventId);
439
- if (progressToken) {
440
- session.pendingRequests.delete(progressToken);
441
- session.eventToProgressToken.delete(nostrEventId);
442
- }
224
+ await this.sendMcpMessage(response, route.clientPubkey, CTXVM_MESSAGES_KIND, tags, session.isEncrypted);
225
+ // Clean up the event route (this also cleans up progress token mapping)
226
+ this.correlationStore.removeEventRoute(nostrEventId);
443
227
  }
444
228
  /**
445
229
  * Handles notification messages with routing.
@@ -454,28 +238,22 @@ export class NostrServerTransport extends BaseNostrTransport {
454
238
  notification.method === 'notifications/progress' &&
455
239
  ((_b = (_a = notification.params) === null || _a === void 0 ? void 0 : _a._meta) === null || _b === void 0 ? void 0 : _b.progressToken)) {
456
240
  const token = String(notification.params._meta.progressToken);
457
- // Use reverse lookup map for O(1) progress token routing
458
- // First find the session that has this progress token
459
- let targetClientPubkey;
460
- let nostrEventId;
461
- for (const [clientPubkey, session] of this.clientSessions.entries()) {
462
- if (session.pendingRequests.has(token)) {
463
- nostrEventId = session.pendingRequests.get(token);
464
- targetClientPubkey = clientPubkey;
465
- break;
241
+ // Use O(1) lookup for progress token routing
242
+ const nostrEventId = this.correlationStore.getEventIdByProgressToken(token);
243
+ if (nostrEventId) {
244
+ const route = this.correlationStore.getEventRoute(nostrEventId);
245
+ if (route) {
246
+ await this.sendNotification(route.clientPubkey, notification, nostrEventId);
247
+ return;
466
248
  }
467
249
  }
468
- if (targetClientPubkey && nostrEventId) {
469
- await this.sendNotification(targetClientPubkey, notification, nostrEventId);
470
- return;
471
- }
472
250
  const error = new Error(`No client found for progress token: ${token}`);
473
251
  this.logger.error('Progress token not found', { token });
474
252
  (_c = this.onerror) === null || _c === void 0 ? void 0 : _c.call(this, error);
475
253
  return;
476
254
  }
477
255
  // Use TaskQueue for outbound notification broadcasting to prevent event loop blocking
478
- for (const [clientPubkey, session] of this.clientSessions.entries()) {
256
+ for (const [clientPubkey, session,] of this.sessionStore.getAllSessions()) {
479
257
  if (session.isInitialized) {
480
258
  this.taskQueue.add(async () => {
481
259
  try {
@@ -509,7 +287,7 @@ export class NostrServerTransport extends BaseNostrTransport {
509
287
  * @returns Promise that resolves when the notification is sent.
510
288
  */
511
289
  async sendNotification(clientPubkey, notification, correlatedEventId) {
512
- const session = this.clientSessions.get(clientPubkey);
290
+ const session = this.sessionStore.getSession(clientPubkey);
513
291
  if (!session) {
514
292
  throw new Error(`No active session found for client: ${clientPubkey}`);
515
293
  }
@@ -557,7 +335,8 @@ export class NostrServerTransport extends BaseNostrTransport {
557
335
  return;
558
336
  }
559
337
  try {
560
- const decryptedJson = await decryptMessage(event, this.signer);
338
+ const decryptedJson = await withTimeout(decryptMessage(event, this.signer), 30000, // 30 seconds default timeout
339
+ 'Decrypt message timed out');
561
340
  const currentEvent = JSON.parse(decryptedJson);
562
341
  this.authorizeAndProcessEvent(currentEvent, true);
563
342
  }
@@ -584,34 +363,6 @@ export class NostrServerTransport extends BaseNostrTransport {
584
363
  }
585
364
  this.authorizeAndProcessEvent(event, false);
586
365
  }
587
- /**
588
- * Checks if a capability is excluded from whitelisting requirements.
589
- * @param method The JSON-RPC method (e.g., 'tools/call', 'tools/list')
590
- * @param name Optional capability name for method-specific exclusions (e.g., 'get_weather')
591
- * @returns true if the capability should bypass whitelisting, false otherwise
592
- */
593
- isCapabilityExcluded(method, name) {
594
- var _a;
595
- // Always allow fundamental MCP methods for connection establishment
596
- if (method === 'initialize' || method === 'notifications/initialized') {
597
- return true;
598
- }
599
- if (!((_a = this.excludedCapabilities) === null || _a === void 0 ? void 0 : _a.length)) {
600
- return false;
601
- }
602
- return this.excludedCapabilities.some((exclusion) => {
603
- // Check if method matches
604
- if (exclusion.method !== method) {
605
- return false;
606
- }
607
- // If exclusion has no name requirement, method match is sufficient
608
- if (!exclusion.name) {
609
- return true;
610
- }
611
- // If exclusion has a name requirement, check if it matches the provided name
612
- return exclusion.name === name;
613
- });
614
- }
615
366
  /**
616
367
  * Authorizes and processes an incoming Nostr event, handling message validation,
617
368
  * client authorization, session management, and optional client public key injection.
@@ -619,7 +370,7 @@ export class NostrServerTransport extends BaseNostrTransport {
619
370
  * @param isEncrypted Whether the original event was encrypted.
620
371
  */
621
372
  authorizeAndProcessEvent(event, isEncrypted) {
622
- var _a, _b, _c, _d, _e;
373
+ var _a, _b;
623
374
  try {
624
375
  const mcpMessage = this.convertNostrEventToMcpMessage(event);
625
376
  if (!mcpMessage) {
@@ -630,52 +381,48 @@ export class NostrServerTransport extends BaseNostrTransport {
630
381
  });
631
382
  return;
632
383
  }
633
- if ((_a = this.allowedPublicKeys) === null || _a === void 0 ? void 0 : _a.size) {
634
- // Check if the message should bypass whitelisting due to excluded capabilities
635
- const shouldBypassWhitelisting = ((_b = this.excludedCapabilities) === null || _b === void 0 ? void 0 : _b.length) &&
636
- (isJSONRPCRequest(mcpMessage) || isJSONRPCNotification(mcpMessage)) &&
637
- this.isCapabilityExcluded(mcpMessage.method, (_c = mcpMessage.params) === null || _c === void 0 ? void 0 : _c.name);
638
- if (!this.allowedPublicKeys.has(event.pubkey) &&
639
- !shouldBypassWhitelisting) {
640
- this.logger.error(`Unauthorized message from ${event.pubkey}, message: ${JSON.stringify(mcpMessage)}. Ignoring.`);
641
- if (this.isPublicServer && isJSONRPCRequest(mcpMessage)) {
642
- const errorResponse = {
643
- jsonrpc: '2.0',
644
- id: mcpMessage.id,
645
- error: {
646
- code: -32000,
647
- message: 'Unauthorized',
648
- },
649
- };
650
- const tags = this.createResponseTags(event.pubkey, event.id);
651
- this.sendMcpMessage(errorResponse, event.pubkey, CTXVM_MESSAGES_KIND, tags, isEncrypted).catch((err) => {
652
- var _a;
653
- this.logger.error('Failed to send unauthorized response', {
654
- error: err instanceof Error ? err.message : String(err),
655
- pubkey: event.pubkey,
656
- eventId: event.id,
657
- });
658
- (_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, new Error(`Failed to send unauthorized response: ${err}`));
384
+ // Check authorization using the authorization policy
385
+ const authDecision = this.authorizationPolicy.authorize(event.pubkey, mcpMessage);
386
+ if (!authDecision.allowed) {
387
+ this.logger.error(`Unauthorized message from ${event.pubkey}, message: ${JSON.stringify(mcpMessage)}. Ignoring.`);
388
+ if ('shouldReplyUnauthorized' in authDecision &&
389
+ authDecision.shouldReplyUnauthorized &&
390
+ isJSONRPCRequest(mcpMessage)) {
391
+ const errorResponse = {
392
+ jsonrpc: '2.0',
393
+ id: mcpMessage.id,
394
+ error: {
395
+ code: -32000,
396
+ message: 'Unauthorized',
397
+ },
398
+ };
399
+ const tags = this.createResponseTags(event.pubkey, event.id);
400
+ this.sendMcpMessage(errorResponse, event.pubkey, CTXVM_MESSAGES_KIND, tags, isEncrypted).catch((err) => {
401
+ var _a;
402
+ this.logger.error('Failed to send unauthorized response', {
403
+ error: err instanceof Error ? err.message : String(err),
404
+ pubkey: event.pubkey,
405
+ eventId: event.id,
659
406
  });
660
- }
661
- return;
407
+ (_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, new Error(`Failed to send unauthorized response: ${err}`));
408
+ });
662
409
  }
410
+ return;
663
411
  }
664
- const now = Date.now();
665
- const session = this.getOrCreateClientSession(event.pubkey, now, isEncrypted);
666
- session.lastActivity = now;
412
+ // Get or create session for this client (ensures session exists for authorized messages)
413
+ this.getOrCreateClientSession(event.pubkey, isEncrypted);
667
414
  // Handle message routing and conditionally inject client pubkey
668
415
  if (isJSONRPCRequest(mcpMessage)) {
669
- this.handleIncomingRequest(session, event.id, mcpMessage, event.pubkey);
416
+ this.handleIncomingRequest(event.id, mcpMessage, event.pubkey);
670
417
  // Inject client public key for enhanced server integration (in-place mutation)
671
418
  if (this.injectClientPubkey) {
672
419
  injectClientPubkey(mcpMessage, event.pubkey);
673
420
  }
674
421
  }
675
422
  else if (isJSONRPCNotification(mcpMessage)) {
676
- this.handleIncomingNotification(session, mcpMessage);
423
+ this.handleIncomingNotification(event.pubkey, mcpMessage);
677
424
  }
678
- (_d = this.onmessage) === null || _d === void 0 ? void 0 : _d.call(this, mcpMessage);
425
+ (_a = this.onmessage) === null || _a === void 0 ? void 0 : _a.call(this, mcpMessage);
679
426
  }
680
427
  catch (error) {
681
428
  this.logger.error('Error in authorizeAndProcessEvent', {
@@ -684,8 +431,18 @@ export class NostrServerTransport extends BaseNostrTransport {
684
431
  eventId: event.id,
685
432
  pubkey: event.pubkey,
686
433
  });
687
- (_e = this.onerror) === null || _e === void 0 ? void 0 : _e.call(this, error instanceof Error ? error : new Error(String(error)));
434
+ (_b = this.onerror) === null || _b === void 0 ? void 0 : _b.call(this, error instanceof Error ? error : new Error(String(error)));
688
435
  }
689
436
  }
437
+ /**
438
+ * Test-only accessor for internal state.
439
+ * @internal
440
+ */
441
+ getInternalStateForTesting() {
442
+ return {
443
+ sessionStore: this.sessionStore,
444
+ correlationStore: this.correlationStore,
445
+ };
446
+ }
690
447
  }
691
448
  //# sourceMappingURL=nostr-server-transport.js.map