@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.
- package/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +398 -398
- package/dist/cli/utils/serverUtils.d.ts +4 -0
- package/dist/cli/utils/serverUtils.js +5 -1
- package/dist/core/conversationMemoryFactory.d.ts +2 -2
- package/dist/core/conversationMemoryFactory.js +2 -2
- package/dist/core/conversationMemoryInitializer.d.ts +2 -2
- package/dist/core/conversationMemoryInitializer.js +2 -2
- package/dist/core/redisConversationMemoryManager.d.ts +13 -2
- package/dist/core/redisConversationMemoryManager.js +31 -1
- package/dist/neurolink.js +1 -1
- package/dist/server/abstract/baseServerAdapter.d.ts +30 -1
- package/dist/server/abstract/baseServerAdapter.js +71 -0
- package/dist/server/adapters/expressAdapter.js +5 -0
- package/dist/server/adapters/fastifyAdapter.d.ts +1 -0
- package/dist/server/adapters/fastifyAdapter.js +16 -3
- package/dist/server/adapters/honoAdapter.d.ts +6 -0
- package/dist/server/adapters/honoAdapter.js +67 -16
- package/dist/server/adapters/koaAdapter.js +5 -0
- package/dist/server/middleware/auth.js +3 -0
- package/dist/server/routes/agentRoutes.d.ts +42 -1
- package/dist/server/routes/agentRoutes.js +140 -31
- package/dist/server/routes/index.d.ts +9 -0
- package/dist/server/routes/index.js +15 -1
- package/dist/server/utils/carriedContextMark.d.ts +28 -0
- package/dist/server/utils/carriedContextMark.js +32 -0
- package/dist/types/common.d.ts +2 -1
- package/dist/types/conversation.d.ts +12 -0
- package/dist/types/server.d.ts +140 -2
- package/docs-site/static/search-index.json +10 -6
- 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-${
|
|
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 =
|
|
439
|
-
|
|
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
|
-
//
|
|
21
|
-
|
|
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:
|
|
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 =
|
|
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 =
|
|
221
|
-
|
|
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
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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(
|
|
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 =
|
|
491
|
+
const requestId = this.requestIdFor(c);
|
|
461
492
|
// Reuse existing context from previous middleware if available
|
|
462
|
-
let ctx = this.requestContextStore.get(
|
|
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
|
-
//
|
|
471
|
-
|
|
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);
|