@juspay/neurolink 13.8.14 → 13.8.16

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 (31) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/browser/neurolink.min.js +398 -398
  3. package/dist/cli/utils/serverUtils.d.ts +4 -0
  4. package/dist/cli/utils/serverUtils.js +5 -1
  5. package/dist/core/conversationMemoryFactory.d.ts +2 -2
  6. package/dist/core/conversationMemoryFactory.js +2 -2
  7. package/dist/core/conversationMemoryInitializer.d.ts +2 -2
  8. package/dist/core/conversationMemoryInitializer.js +2 -2
  9. package/dist/core/redisConversationMemoryManager.d.ts +13 -2
  10. package/dist/core/redisConversationMemoryManager.js +31 -1
  11. package/dist/neurolink.js +1 -1
  12. package/dist/server/abstract/baseServerAdapter.d.ts +30 -1
  13. package/dist/server/abstract/baseServerAdapter.js +71 -0
  14. package/dist/server/adapters/expressAdapter.js +5 -0
  15. package/dist/server/adapters/fastifyAdapter.d.ts +1 -0
  16. package/dist/server/adapters/fastifyAdapter.js +16 -3
  17. package/dist/server/adapters/honoAdapter.d.ts +6 -0
  18. package/dist/server/adapters/honoAdapter.js +67 -16
  19. package/dist/server/adapters/koaAdapter.js +5 -0
  20. package/dist/server/middleware/auth.js +3 -0
  21. package/dist/server/routes/agentRoutes.d.ts +42 -1
  22. package/dist/server/routes/agentRoutes.js +140 -31
  23. package/dist/server/routes/index.d.ts +9 -0
  24. package/dist/server/routes/index.js +15 -1
  25. package/dist/server/utils/carriedContextMark.d.ts +28 -0
  26. package/dist/server/utils/carriedContextMark.js +32 -0
  27. package/dist/types/common.d.ts +2 -1
  28. package/dist/types/conversation.d.ts +12 -0
  29. package/dist/types/server.d.ts +140 -2
  30. package/docs-site/static/search-index.json +10 -6
  31. package/package.json +2 -1
@@ -16,6 +16,10 @@ export declare function readServerApiKeys(): string[];
16
16
  * Require one of `apiKeys` as `Authorization: Bearer <key>` or `X-API-Key`.
17
17
  * Health probes stay open so orchestrators can reach them, and the dev
18
18
  * playground header cannot bypass a key the operator configured.
19
+ *
20
+ * Every holder of a key gets the same `{ id: "api-key" }`, marked `shared` so
21
+ * the agent routes know it names no person and do not warn when a request
22
+ * names its own userId.
19
23
  */
20
24
  export declare function createServerApiKeyMiddleware(apiKeys: string[], basePath: string): MiddlewareDefinition;
21
25
  /**
@@ -41,6 +41,10 @@ const matchesKey = (key, token) => key.length === token.length && timingSafeEqua
41
41
  * Require one of `apiKeys` as `Authorization: Bearer <key>` or `X-API-Key`.
42
42
  * Health probes stay open so orchestrators can reach them, and the dev
43
43
  * playground header cannot bypass a key the operator configured.
44
+ *
45
+ * Every holder of a key gets the same `{ id: "api-key" }`, marked `shared` so
46
+ * the agent routes know it names no person and do not warn when a request
47
+ * names its own userId.
44
48
  */
45
49
  export function createServerApiKeyMiddleware(apiKeys, basePath) {
46
50
  const keys = apiKeys.map((key) => Buffer.from(key, "utf8"));
@@ -53,7 +57,7 @@ export function createServerApiKeyMiddleware(apiKeys, basePath) {
53
57
  validate: async (token) => {
54
58
  const candidate = Buffer.from(token, "utf8");
55
59
  return keys.some((key) => matchesKey(key, candidate))
56
- ? { id: "api-key" }
60
+ ? { id: "api-key", shared: true }
57
61
  : null;
58
62
  },
59
63
  skipPaths: [`${basePath.replace(/\/+$/, "")}/health`],
@@ -2,13 +2,13 @@
2
2
  * Conversation Memory Factory for NeuroLink
3
3
  * Creates appropriate conversation memory manager based on configuration
4
4
  */
5
- import type { StorageType, ConversationMemoryConfig, RedisStorageConfig } from "../types/index.js";
5
+ import type { StorageType, ConversationMemoryConfig, NeuroLinkEvents, RedisStorageConfig, TypedEventEmitter } from "../types/index.js";
6
6
  import { ConversationMemoryManager } from "./conversationMemoryManager.js";
7
7
  import { RedisConversationMemoryManager } from "./redisConversationMemoryManager.js";
8
8
  /**
9
9
  * Creates a conversation memory manager based on configuration
10
10
  */
11
- export declare function createConversationMemoryManager(config: ConversationMemoryConfig, storageType?: StorageType, redisConfig?: RedisStorageConfig): ConversationMemoryManager | RedisConversationMemoryManager;
11
+ export declare function createConversationMemoryManager(config: ConversationMemoryConfig, storageType?: StorageType, redisConfig?: RedisStorageConfig, eventEmitter?: TypedEventEmitter<NeuroLinkEvents>): ConversationMemoryManager | RedisConversationMemoryManager;
12
12
  /**
13
13
  * Get storage type from environment variable or configuration
14
14
  */
@@ -8,7 +8,7 @@ import { RedisConversationMemoryManager } from "./redisConversationMemoryManager
8
8
  /**
9
9
  * Creates a conversation memory manager based on configuration
10
10
  */
11
- export function createConversationMemoryManager(config, storageType = "memory", redisConfig) {
11
+ export function createConversationMemoryManager(config, storageType = "memory", redisConfig, eventEmitter) {
12
12
  logger.debug("[conversationMemoryFactory] Creating conversation memory manager", {
13
13
  storageType,
14
14
  config: {
@@ -38,7 +38,7 @@ export function createConversationMemoryManager(config, storageType = "memory",
38
38
  ttl: redisConfig?.ttl || 86400,
39
39
  hasConnectionOptions: !!redisConfig?.connectionOptions,
40
40
  });
41
- const redisManager = new RedisConversationMemoryManager(config, redisConfig);
41
+ const redisManager = new RedisConversationMemoryManager(config, redisConfig, eventEmitter);
42
42
  logger.debug("[conversationMemoryFactory] Redis conversation manager created successfully", {
43
43
  managerType: redisManager.constructor.name,
44
44
  config: {
@@ -2,7 +2,7 @@
2
2
  * Conversation Memory Initializer
3
3
  * Provides integration with Redis storage for conversation memory
4
4
  */
5
- import type { ConversationMemoryConfig } from "../types/index.js";
5
+ import type { ConversationMemoryConfig, NeuroLinkEvents, TypedEventEmitter } from "../types/index.js";
6
6
  import type { ConversationMemoryManager } from "./conversationMemoryManager.js";
7
7
  import type { RedisConversationMemoryManager } from "./redisConversationMemoryManager.js";
8
8
  /**
@@ -11,4 +11,4 @@ import type { RedisConversationMemoryManager } from "./redisConversationMemoryMa
11
11
  */
12
12
  export declare function initializeConversationMemory(config?: {
13
13
  conversationMemory?: Partial<ConversationMemoryConfig>;
14
- }): Promise<ConversationMemoryManager | RedisConversationMemoryManager | null>;
14
+ }, eventEmitter?: TypedEventEmitter<NeuroLinkEvents>): Promise<ConversationMemoryManager | RedisConversationMemoryManager | null>;
@@ -9,7 +9,7 @@ import { createConversationMemoryManager, getRedisConfigFromEnv, getStorageType,
9
9
  * Initialize conversation memory for NeuroLink
10
10
  * This function decides whether to use in-memory or Redis storage
11
11
  */
12
- export async function initializeConversationMemory(config) {
12
+ export async function initializeConversationMemory(config, eventEmitter) {
13
13
  logger.debug("[conversationMemoryInitializer] Initialize conversation memory called", {
14
14
  hasConfig: !!config,
15
15
  hasMemoryConfig: !!config?.conversationMemory,
@@ -58,7 +58,7 @@ export async function initializeConversationMemory(config) {
58
58
  });
59
59
  // Create Redis-based conversation memory manager
60
60
  logger.debug("[conversationMemoryInitializer] Creating Redis conversation memory manager");
61
- const redisMemoryManager = createConversationMemoryManager(memoryConfig, "redis", redisConfig);
61
+ const redisMemoryManager = createConversationMemoryManager(memoryConfig, "redis", redisConfig, eventEmitter);
62
62
  logger.debug("[conversationMemoryInitializer] Checking Redis manager creation result", {
63
63
  managerType: redisMemoryManager?.constructor?.name || "unknown",
64
64
  isRedisType: redisMemoryManager?.constructor?.name ===
@@ -2,7 +2,7 @@
2
2
  * Redis Conversation Memory Manager for NeuroLink
3
3
  * Redis-based implementation of conversation storage with same interface as ConversationMemoryManager
4
4
  */
5
- import type { ChatMessage, ConversationMemoryConfig, ConversationMemoryStats, RedisConversationObject, RedisStorageConfig, SessionListItem, SessionMemory, SessionMetadata, StoreConversationTurnOptions, AgenticLoopReportMetadata, IConversationMemoryManager } from "../types/index.js";
5
+ import type { ChatMessage, ConversationMemoryConfig, ConversationMemoryStats, RedisConversationObject, RedisStorageConfig, SessionListItem, SessionMemory, SessionMetadata, StoreConversationTurnOptions, AgenticLoopReportMetadata, IConversationMemoryManager, NeuroLinkEvents, TypedEventEmitter } from "../types/index.js";
6
6
  /**
7
7
  * Redis-based implementation of the ConversationMemoryManager
8
8
  * Uses the same interface but stores data in Redis
@@ -13,6 +13,11 @@ export declare class RedisConversationMemoryManager implements IConversationMemo
13
13
  private summarizationEngine;
14
14
  private redisConfig;
15
15
  private redisClient;
16
+ /**
17
+ * Receives `conversation:titleGenerated` once a new conversation's title is
18
+ * stored. Optional because the manager can be constructed standalone.
19
+ */
20
+ private readonly eventEmitter?;
16
21
  /**
17
22
  * Temporary storage for tool execution data to prevent race conditions
18
23
  * Key format: "${sessionId}:${userId}"
@@ -28,7 +33,7 @@ export declare class RedisConversationMemoryManager implements IConversationMemo
28
33
  * Key format: "${sessionId}:${userId}"
29
34
  */
30
35
  private summarizationInProgress;
31
- constructor(config: ConversationMemoryConfig, redisConfig?: RedisStorageConfig);
36
+ constructor(config: ConversationMemoryConfig, redisConfig?: RedisStorageConfig, eventEmitter?: TypedEventEmitter<NeuroLinkEvents>);
32
37
  /**
33
38
  * Initialize the memory manager with Redis connection
34
39
  */
@@ -155,6 +160,12 @@ export declare class RedisConversationMemoryManager implements IConversationMemo
155
160
  * @returns Complete conversation object with all data, or null if session doesn't exist
156
161
  */
157
162
  getUserSessionObject(userId: string, sessionId: string): Promise<RedisConversationObject | null>;
163
+ /**
164
+ * Announce a stored title. A throwing listener is logged rather than
165
+ * propagated: the title is already persisted, and the caller's catch block
166
+ * reports title-generation failures.
167
+ */
168
+ private emitTitleGenerated;
158
169
  /**
159
170
  * Generate a conversation title from the first user message
160
171
  * Uses AI to create a concise, descriptive title (5-8 words)
@@ -29,6 +29,11 @@ export class RedisConversationMemoryManager {
29
29
  summarizationEngine = new SummarizationEngine();
30
30
  redisConfig;
31
31
  redisClient = null;
32
+ /**
33
+ * Receives `conversation:titleGenerated` once a new conversation's title is
34
+ * stored. Optional because the manager can be constructed standalone.
35
+ */
36
+ eventEmitter;
32
37
  /**
33
38
  * Temporary storage for tool execution data to prevent race conditions
34
39
  * Key format: "${sessionId}:${userId}"
@@ -44,9 +49,10 @@ export class RedisConversationMemoryManager {
44
49
  * Key format: "${sessionId}:${userId}"
45
50
  */
46
51
  summarizationInProgress = new Set();
47
- constructor(config, redisConfig = {}) {
52
+ constructor(config, redisConfig = {}, eventEmitter) {
48
53
  this.config = config;
49
54
  this.redisConfig = getNormalizedConfig(redisConfig);
55
+ this.eventEmitter = eventEmitter;
50
56
  }
51
57
  /**
52
58
  * Initialize the memory manager with Redis connection
@@ -419,6 +425,12 @@ export class RedisConversationMemoryManager {
419
425
  const serializedData = serializeConversation(updatedConversation);
420
426
  if (this.redisClient) {
421
427
  await setWithTtl(this.redisClient, updatedRedisKey, serializedData, this.redisConfig.ttl);
428
+ this.emitTitleGenerated({
429
+ sessionId: options.sessionId,
430
+ userId: normalizedUserId,
431
+ title,
432
+ timestamp: Date.now(),
433
+ });
422
434
  }
423
435
  }
424
436
  }
@@ -1023,6 +1035,24 @@ export class RedisConversationMemoryManager {
1023
1035
  return null;
1024
1036
  }
1025
1037
  }
1038
+ /**
1039
+ * Announce a stored title. A throwing listener is logged rather than
1040
+ * propagated: the title is already persisted, and the caller's catch block
1041
+ * reports title-generation failures.
1042
+ */
1043
+ emitTitleGenerated(event) {
1044
+ try {
1045
+ this.eventEmitter?.emit("conversation:titleGenerated", event);
1046
+ }
1047
+ catch (listenerError) {
1048
+ logger.warn("[RedisConversationMemoryManager] conversation:titleGenerated listener threw", {
1049
+ sessionId: event.sessionId,
1050
+ error: listenerError instanceof Error
1051
+ ? listenerError.message
1052
+ : String(listenerError),
1053
+ });
1054
+ }
1055
+ }
1026
1056
  /**
1027
1057
  * Generate a conversation title from the first user message
1028
1058
  * Uses AI to create a concise, descriptive title (5-8 words)
package/dist/neurolink.js CHANGED
@@ -13851,7 +13851,7 @@ Current user's request: ${currentInput}`;
13851
13851
  // Import the integration module
13852
13852
  const { initializeConversationMemory } = await import("./core/conversationMemoryInitializer.js");
13853
13853
  // Use the integration module to create the appropriate memory manager
13854
- const memoryManager = await initializeConversationMemory(this.conversationMemoryConfig);
13854
+ const memoryManager = await initializeConversationMemory(this.conversationMemoryConfig, this.emitter);
13855
13855
  // Assign to conversationMemory with proper type to handle both memory manager types
13856
13856
  this.conversationMemory = memoryManager;
13857
13857
  // Reset the lazy init flag since we've now initialized
@@ -7,7 +7,7 @@ import { EventEmitter } from "events";
7
7
  import type { ExternalServerManager } from "../../mcp/externalServerManager.js";
8
8
  import type { MCPToolRegistry } from "../../mcp/toolRegistry.js";
9
9
  import type { NeuroLink } from "../../neurolink.js";
10
- import type { MiddlewareDefinition, RedactionConfig, RequiredServerAdapterConfig, RequiredShutdownConfig, RouteDefinition, ServerAdapterConfig, ServerAdapterEvents, ServerContext, ServerLifecycleState, ServerStatus, TrackedConnection } from "../../types/index.js";
10
+ import type { MiddlewareDefinition, RedactionConfig, RequiredServerAdapterConfig, RequiredShutdownConfig, RouteDefinition, ServerAdapterConfig, ServerAdapterEvents, ServerCarriedContext, ServerContext, ServerLifecycleState, ServerStatus, TrackedConnection } from "../../types/index.js";
11
11
  /**
12
12
  * Abstract base class for server adapters
13
13
  * Provides common functionality and defines the interface for framework-specific implementations
@@ -28,6 +28,7 @@ export declare abstract class BaseServerAdapter extends EventEmitter {
28
28
  protected lifecycleState: ServerLifecycleState;
29
29
  protected activeConnections: Map<string, TrackedConnection>;
30
30
  protected readonly shutdownConfig: RequiredShutdownConfig;
31
+ private carryRequestedByRoutes;
31
32
  constructor(neurolink: NeuroLink, config?: ServerAdapterConfig);
32
33
  /**
33
34
  * Initialize the underlying server framework
@@ -155,6 +156,34 @@ export declare abstract class BaseServerAdapter extends EventEmitter {
155
156
  * Only registers health routes if disableBuiltInHealth is false (default)
156
157
  */
157
158
  protected registerBuiltInRoutes(): Promise<void>;
159
+ /**
160
+ * Ask the adapter to carry middleware state (`ctx.user` and the rest) to later
161
+ * middleware and the route handler, as the `carryMiddlewareState` option does.
162
+ * `registerAllRoutes()` calls it when given `perUserAuth`, which cannot work
163
+ * without the user in the handler. It takes effect for requests that start
164
+ * after the call, so call it before the server starts, as registering routes
165
+ * is.
166
+ */
167
+ enableMiddlewareStateCarry(): void;
168
+ /** Whether this adapter carries middleware state to the handler. */
169
+ protected carriesMiddlewareState(): boolean;
170
+ /**
171
+ * What middleware has set on its context that later middleware and the route
172
+ * handler must see. Each step gets a freshly built context, so unless this is
173
+ * parked in the framework's per-request storage an authenticated `ctx.user`
174
+ * set by middleware is gone before the handler runs. `undefined` when the
175
+ * adapter does not carry middleware state, so nothing is parked.
176
+ */
177
+ protected captureCarriedContext(ctx: ServerContext): ServerCarriedContext | undefined;
178
+ /**
179
+ * Put what `captureCarriedContext` saved onto a freshly built context, and
180
+ * mark the context as one that received the carry. The agent routes read the
181
+ * mark: a request that never used to reach its handler with `ctx.user` must
182
+ * not start being identified by it unless the server opts in. Does nothing
183
+ * when the adapter does not carry middleware state: the context stays the
184
+ * fresh one it always was, and unmarked.
185
+ */
186
+ protected restoreCarriedContext(ctx: ServerContext, carried: ServerCarriedContext | undefined): void;
158
187
  /**
159
188
  * Generate unique request ID
160
189
  */
@@ -7,6 +7,7 @@ import { EventEmitter } from "events";
7
7
  import { getMetricsAggregator, SpanSerializer, SpanStatus, SpanType, } from "../../observability/index.js";
8
8
  import { withTimeout } from "../../utils/errorHandling.js";
9
9
  import { logger } from "../../utils/logger.js";
10
+ import { markContextCarried } from "../utils/carriedContextMark.js";
10
11
  import { tryImport } from "../../utils/tryImport.js";
11
12
  import { DrainTimeoutError, InvalidLifecycleStateError, MissingDependencyError, ServerAdapterError, ShutdownTimeoutError, } from "../errors.js";
12
13
  /**
@@ -33,6 +34,9 @@ export class BaseServerAdapter extends EventEmitter {
33
34
  lifecycleState = "uninitialized";
34
35
  activeConnections = new Map();
35
36
  shutdownConfig;
37
+ // Set by enableMiddlewareStateCarry(); the carryMiddlewareState option is the
38
+ // other way in.
39
+ carryRequestedByRoutes = false;
36
40
  constructor(neurolink, config = {}) {
37
41
  super();
38
42
  this.neurolink = neurolink;
@@ -101,6 +105,7 @@ export class BaseServerAdapter extends EventEmitter {
101
105
  enableSwagger: config.enableSwagger ?? false,
102
106
  disableBuiltInHealth: config.disableBuiltInHealth ?? false,
103
107
  shutdown: this.shutdownConfig,
108
+ carryMiddlewareState: config.carryMiddlewareState ?? false,
104
109
  };
105
110
  }
106
111
  /**
@@ -454,6 +459,72 @@ export class BaseServerAdapter extends EventEmitter {
454
459
  });
455
460
  }
456
461
  }
462
+ /**
463
+ * Ask the adapter to carry middleware state (`ctx.user` and the rest) to later
464
+ * middleware and the route handler, as the `carryMiddlewareState` option does.
465
+ * `registerAllRoutes()` calls it when given `perUserAuth`, which cannot work
466
+ * without the user in the handler. It takes effect for requests that start
467
+ * after the call, so call it before the server starts, as registering routes
468
+ * is.
469
+ */
470
+ enableMiddlewareStateCarry() {
471
+ this.carryRequestedByRoutes = true;
472
+ }
473
+ /** Whether this adapter carries middleware state to the handler. */
474
+ carriesMiddlewareState() {
475
+ return this.config.carryMiddlewareState || this.carryRequestedByRoutes;
476
+ }
477
+ /**
478
+ * What middleware has set on its context that later middleware and the route
479
+ * handler must see. Each step gets a freshly built context, so unless this is
480
+ * parked in the framework's per-request storage an authenticated `ctx.user`
481
+ * set by middleware is gone before the handler runs. `undefined` when the
482
+ * adapter does not carry middleware state, so nothing is parked.
483
+ */
484
+ captureCarriedContext(ctx) {
485
+ if (!this.carriesMiddlewareState()) {
486
+ return undefined;
487
+ }
488
+ return {
489
+ user: ctx.user,
490
+ session: ctx.session,
491
+ metadata: ctx.metadata,
492
+ abortSignal: ctx.abortSignal,
493
+ abortController: ctx.abortController,
494
+ };
495
+ }
496
+ /**
497
+ * Put what `captureCarriedContext` saved onto a freshly built context, and
498
+ * mark the context as one that received the carry. The agent routes read the
499
+ * mark: a request that never used to reach its handler with `ctx.user` must
500
+ * not start being identified by it unless the server opts in. Does nothing
501
+ * when the adapter does not carry middleware state: the context stays the
502
+ * fresh one it always was, and unmarked.
503
+ */
504
+ restoreCarriedContext(ctx, carried) {
505
+ if (!this.carriesMiddlewareState()) {
506
+ return;
507
+ }
508
+ markContextCarried(ctx);
509
+ if (!carried) {
510
+ return;
511
+ }
512
+ if (carried.user) {
513
+ ctx.user = carried.user;
514
+ }
515
+ if (carried.session) {
516
+ ctx.session = carried.session;
517
+ }
518
+ if (carried.metadata) {
519
+ ctx.metadata = { ...ctx.metadata, ...carried.metadata };
520
+ }
521
+ if (carried.abortSignal) {
522
+ ctx.abortSignal = carried.abortSignal;
523
+ }
524
+ if (carried.abortController) {
525
+ ctx.abortController = carried.abortController;
526
+ }
527
+ }
457
528
  /**
458
529
  * Generate unique request ID
459
530
  */
@@ -166,6 +166,7 @@ export class ExpressServerAdapter extends BaseServerAdapter {
166
166
  if (res.locals.responseHeaders) {
167
167
  ctx.responseHeaders = { ...res.locals.responseHeaders };
168
168
  }
169
+ this.restoreCarriedContext(ctx, res.locals.carriedContext);
169
170
  // Emit request event
170
171
  this.emit("request", {
171
172
  requestId,
@@ -366,9 +367,13 @@ export class ExpressServerAdapter extends BaseServerAdapter {
366
367
  });
367
368
  // Copy existing response headers to context
368
369
  ctx.responseHeaders = { ...res.locals.responseHeaders };
370
+ this.restoreCarriedContext(ctx, res.locals.carriedContext);
369
371
  // Execute middleware
370
372
  try {
371
373
  await middleware.handler(ctx, async () => {
374
+ // What this middleware set (ctx.user included) is for the next
375
+ // middleware and the route handler, which build their own contexts.
376
+ res.locals.carriedContext = this.captureCarriedContext(ctx);
372
377
  // After middleware execution, merge response headers back to res.locals
373
378
  if (ctx.responseHeaders) {
374
379
  Object.assign(res.locals.responseHeaders, ctx.responseHeaders);
@@ -13,6 +13,7 @@ import type { MiddlewareDefinition, RouteDefinition, ServerAdapterConfig } from
13
13
  export declare class FastifyServerAdapter extends BaseServerAdapter {
14
14
  private app;
15
15
  private frameworkInitialized;
16
+ private connectionIds;
16
17
  constructor(neurolink: NeuroLink, config?: ServerAdapterConfig);
17
18
  /**
18
19
  * Initialize Fastify framework
@@ -16,6 +16,11 @@ import { isRawBodyContentType, isWebResponse, readWebResponse, } from "../utils/
16
16
  export class FastifyServerAdapter extends BaseServerAdapter {
17
17
  app = null;
18
18
  frameworkInitialized = false;
19
+ // The shutdown drain's record for each in-flight request. Fastify takes
20
+ // `request.id` from the caller's X-Request-ID, so that id can repeat across
21
+ // overlapping requests: a record named after it would be shared, and the
22
+ // first request to finish would untrack the other.
23
+ connectionIds = new WeakMap();
19
24
  constructor(neurolink, config = {}) {
20
25
  super(neurolink, config);
21
26
  }
@@ -239,6 +244,7 @@ export class FastifyServerAdapter extends BaseServerAdapter {
239
244
  if (reqWithHeaders.responseHeaders) {
240
245
  ctx.responseHeaders = { ...reqWithHeaders.responseHeaders };
241
246
  }
247
+ this.restoreCarriedContext(ctx, reqWithHeaders.carriedContext);
242
248
  // Emit request event
243
249
  this.emit("request", {
244
250
  requestId,
@@ -399,8 +405,12 @@ export class FastifyServerAdapter extends BaseServerAdapter {
399
405
  });
400
406
  // Copy existing response headers to context
401
407
  ctx.responseHeaders = { ...reqWithHeaders.responseHeaders };
408
+ this.restoreCarriedContext(ctx, reqWithHeaders.carriedContext);
402
409
  // Execute middleware
403
410
  await middleware.handler(ctx, async () => {
411
+ // What this middleware set (ctx.user included) is for the next
412
+ // middleware and the route handler, which build their own contexts.
413
+ reqWithHeaders.carriedContext = this.captureCarriedContext(ctx);
404
414
  // After middleware execution, merge response headers back to request
405
415
  if (ctx.responseHeaders) {
406
416
  Object.assign(reqWithHeaders.responseHeaders, ctx.responseHeaders);
@@ -431,12 +441,15 @@ export class FastifyServerAdapter extends BaseServerAdapter {
431
441
  const startupTimeout = this.config.timeout || 30000;
432
442
  // Track connections via Fastify hooks (must be registered before listen)
433
443
  app.addHook("onRequest", async (request) => {
434
- const connectionId = `conn-${request.id}`;
444
+ const connectionId = `conn-${this.generateRequestId()}`;
445
+ this.connectionIds.set(request, connectionId);
435
446
  this.trackConnection(connectionId, request.raw.socket, request.id);
436
447
  });
437
448
  app.addHook("onResponse", async (request) => {
438
- const connectionId = `conn-${request.id}`;
439
- this.untrackConnection(connectionId);
449
+ const connectionId = this.connectionIds.get(request);
450
+ if (connectionId) {
451
+ this.untrackConnection(connectionId);
452
+ }
440
453
  });
441
454
  try {
442
455
  await withTimeout(app.listen({ port, host }), startupTimeout, new ServerStartError(`Fastify server startup timed out after ${startupTimeout}ms`, undefined, port, host));
@@ -16,6 +16,7 @@ export declare class HonoServerAdapter extends BaseServerAdapter {
16
16
  private rateLimitStore;
17
17
  private rateLimitCleanupInterval?;
18
18
  private requestContextStore;
19
+ private requestIds;
19
20
  private HTTPExceptionCtor;
20
21
  private streamSSEFn;
21
22
  private parseFormBody;
@@ -52,6 +53,11 @@ export declare class HonoServerAdapter extends BaseServerAdapter {
52
53
  * Register middleware with Hono
53
54
  */
54
55
  protected registerFrameworkMiddleware(middleware: MiddlewareDefinition): void;
56
+ /**
57
+ * The id of this request: the caller's `X-Request-ID` when sent, otherwise
58
+ * one generated on first use, and the same one on every later use.
59
+ */
60
+ private requestIdFor;
55
61
  /**
56
62
  * Start the Hono server
57
63
  */
@@ -6,6 +6,7 @@
6
6
  import { logger } from "../../utils/logger.js";
7
7
  import { AlreadyRunningError, ServerAdapterError, ServerStopError, wrapError, } from "../errors.js";
8
8
  import { BaseServerAdapter } from "../abstract/baseServerAdapter.js";
9
+ import { markContextCarried } from "../utils/carriedContextMark.js";
9
10
  import { isErrorResponse } from "../utils/validation.js";
10
11
  import { isRawBodyContentType, isWebResponse, parseByteLimit, RawBodyTooLargeError, readBoundedRequestBody, } from "../utils/rawBody.js";
11
12
  /**
@@ -17,8 +18,17 @@ export class HonoServerAdapter extends BaseServerAdapter {
17
18
  server;
18
19
  rateLimitStore = new Map();
19
20
  rateLimitCleanupInterval;
20
- // Store context by request ID for sharing between middleware and route handlers
21
- requestContextStore = new Map();
21
+ // Context shared between middleware and the route handler. Keyed by the request
22
+ // object, never by the request id: the id can be the
23
+ // caller's own X-Request-ID, so two overlapping requests that send the same one
24
+ // would share a context, and the second would run with the first one's
25
+ // identity, headers and body. A WeakMap also frees an entry whose handler
26
+ // never ran.
27
+ requestContextStore = new WeakMap();
28
+ // One id per request, for logs and events: the caller's X-Request-ID when sent,
29
+ // otherwise generated once, so every step reports the same id. With no header
30
+ // every step used to invent its own.
31
+ requestIds = new WeakMap();
22
32
  // Populated by initializeFramework's dynamic imports; used by methods that
23
33
  // run after initialization (route registration, streaming) and so cannot
24
34
  // rely on initializeFramework's own local closure variables.
@@ -50,7 +60,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
50
60
  headers[name] = value;
51
61
  });
52
62
  const minCtx = {
53
- requestId: c.req.header("X-Request-ID") || this.generateRequestId(),
63
+ requestId: this.requestIdFor(c),
54
64
  method: c.req.method,
55
65
  path: c.req.path,
56
66
  headers,
@@ -162,7 +172,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
162
172
  }
163
173
  // Global error handler
164
174
  this.app.onError((error, c) => {
165
- const requestId = c.req.header("X-Request-ID") || this.generateRequestId();
175
+ const requestId = this.requestIdFor(c);
166
176
  this.logRequestError("HonoAdapter", requestId, error);
167
177
  this.emitRequestError({
168
178
  requestId,
@@ -217,8 +227,11 @@ export class HonoServerAdapter extends BaseServerAdapter {
217
227
  registerFrameworkRoute(route) {
218
228
  const method = route.method.toLowerCase();
219
229
  this.app[method](route.path, async (c) => {
220
- const requestId = c.req.header("X-Request-ID") || this.generateRequestId();
221
- const connectionId = `conn-${requestId}`;
230
+ const requestId = this.requestIdFor(c);
231
+ // Not derived from the request id, which a caller may repeat: two
232
+ // overlapping requests would then share one record and the first to
233
+ // finish would untrack the other.
234
+ const connectionId = `conn-${this.generateRequestId()}`;
222
235
  const startTime = Date.now();
223
236
  // Track connection for graceful shutdown
224
237
  this.trackConnection(connectionId, undefined, requestId);
@@ -226,12 +239,30 @@ export class HonoServerAdapter extends BaseServerAdapter {
226
239
  // Extract path parameters
227
240
  const params = c.req.param();
228
241
  // Reuse existing context from middleware if available, otherwise create new one
229
- const created = this.requestContextStore.get(requestId) ||
230
- (await this.createRequestContext(c, requestId, params));
231
- if (isWebResponse(created)) {
232
- return created;
242
+ const existingCtx = this.requestContextStore.get(c.req.raw);
243
+ // Middleware ran before the router matched this route, so the shared
244
+ // context carries no path parameters yet.
245
+ if (existingCtx) {
246
+ existingCtx.params = params;
247
+ // The one request the handler already got the middleware's context
248
+ // for before this adapter carried it on every request: the caller
249
+ // sent its own X-Request-ID, which the old store was keyed by. That
250
+ // context stays unmarked, so the agent routes keep trusting its
251
+ // ctx.user there. A context found for any other request exists only
252
+ // because the server carries middleware state, is new to the
253
+ // handler, and is marked.
254
+ if (!c.req.header("X-Request-ID")) {
255
+ markContextCarried(existingCtx);
256
+ }
257
+ }
258
+ let ctx = existingCtx;
259
+ if (!ctx) {
260
+ const created = await this.createRequestContext(c, requestId, params);
261
+ if (isWebResponse(created)) {
262
+ return created;
263
+ }
264
+ ctx = created;
233
265
  }
234
- const ctx = created;
235
266
  // Emit request event
236
267
  this.emit("request", {
237
268
  requestId,
@@ -325,7 +356,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
325
356
  // Untrack connection when request completes
326
357
  this.untrackConnection(connectionId);
327
358
  // Clean up context store to prevent memory leaks
328
- this.requestContextStore.delete(requestId);
359
+ this.requestContextStore.delete(c.req.raw);
329
360
  }
330
361
  });
331
362
  }
@@ -457,9 +488,9 @@ export class HonoServerAdapter extends BaseServerAdapter {
457
488
  // Extract path parameters
458
489
  const params = c.req.param();
459
490
  // Get or generate request ID for context lookup
460
- const requestId = c.req.header("X-Request-ID") || this.generateRequestId();
491
+ const requestId = this.requestIdFor(c);
461
492
  // Reuse existing context from previous middleware if available
462
- let ctx = this.requestContextStore.get(requestId);
493
+ let ctx = this.requestContextStore.get(c.req.raw);
463
494
  if (!ctx) {
464
495
  // Create new context for the first middleware
465
496
  const created = await this.createRequestContext(c, requestId, params);
@@ -467,14 +498,34 @@ export class HonoServerAdapter extends BaseServerAdapter {
467
498
  return created;
468
499
  }
469
500
  ctx = created;
470
- // Store context for sharing between middleware and route handlers
471
- this.requestContextStore.set(requestId, ctx);
501
+ // Share the context between the middleware steps and the route
502
+ // handler when the server carries middleware state, or when the
503
+ // caller sent its own X-Request-ID: the one case the old store, keyed
504
+ // by that header, ever found a context again. Otherwise every step
505
+ // and the handler build their own, and storing this one would only
506
+ // leave it behind.
507
+ if (this.carriesMiddlewareState() || c.req.header("X-Request-ID")) {
508
+ this.requestContextStore.set(c.req.raw, ctx);
509
+ }
472
510
  }
473
511
  // Execute middleware
474
512
  return middleware.handler(ctx, next);
475
513
  });
476
514
  }
477
515
  }
516
+ /**
517
+ * The id of this request: the caller's `X-Request-ID` when sent, otherwise
518
+ * one generated on first use, and the same one on every later use.
519
+ */
520
+ requestIdFor(c) {
521
+ const known = this.requestIds.get(c.req.raw);
522
+ if (known) {
523
+ return known;
524
+ }
525
+ const id = c.req.header("X-Request-ID") || this.generateRequestId();
526
+ this.requestIds.set(c.req.raw, id);
527
+ return id;
528
+ }
478
529
  /**
479
530
  * Start the Hono server
480
531
  */
@@ -261,6 +261,7 @@ export class KoaServerAdapter extends BaseServerAdapter {
261
261
  if (ctx.state.responseHeaders) {
262
262
  serverCtx.responseHeaders = { ...ctx.state.responseHeaders };
263
263
  }
264
+ this.restoreCarriedContext(serverCtx, ctx.state.carriedContext);
264
265
  // Emit request event
265
266
  this.emit("request", {
266
267
  requestId,
@@ -419,8 +420,12 @@ export class KoaServerAdapter extends BaseServerAdapter {
419
420
  });
420
421
  // Copy existing response headers to context
421
422
  serverCtx.responseHeaders = { ...ctx.state.responseHeaders };
423
+ this.restoreCarriedContext(serverCtx, ctx.state.carriedContext);
422
424
  // Execute middleware
423
425
  await middleware.handler(serverCtx, async () => {
426
+ // What this middleware set (ctx.user included) is for the next
427
+ // middleware and the route handler, which build their own contexts.
428
+ ctx.state.carriedContext = this.captureCarriedContext(serverCtx);
424
429
  // After middleware execution, merge response headers back to ctx.state
425
430
  if (serverCtx.responseHeaders) {
426
431
  Object.assign(ctx.state.responseHeaders, serverCtx.responseHeaders);