@juspay/neurolink 13.8.17 → 13.8.18

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 (192) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +1 -1
  3. package/dist/action/actionExecutor.d.ts +33 -3
  4. package/dist/action/actionExecutor.js +71 -15
  5. package/dist/action/actionInputs.js +4 -1
  6. package/dist/action/cliVersion.d.ts +25 -0
  7. package/dist/action/cliVersion.js +26 -0
  8. package/dist/action/index.d.ts +2 -1
  9. package/dist/action/index.js +2 -1
  10. package/dist/adapters/video/directorPipeline.js +46 -36
  11. package/dist/adapters/video/ffmpegAdapter.d.ts +17 -0
  12. package/dist/adapters/video/ffmpegAdapter.js +32 -5
  13. package/dist/adapters/video/frameExtractor.js +1 -1
  14. package/dist/adapters/video/videoMerger.js +5 -2
  15. package/dist/agent/arithmeticEvaluator.d.ts +32 -0
  16. package/dist/agent/arithmeticEvaluator.js +349 -0
  17. package/dist/agent/directTools.d.ts +6 -18
  18. package/dist/agent/directTools.js +17 -56
  19. package/dist/artifacts/artifactStore.d.ts +22 -1
  20. package/dist/artifacts/artifactStore.js +66 -6
  21. package/dist/auth/anthropicOAuth.d.ts +26 -1
  22. package/dist/auth/anthropicOAuth.js +81 -5
  23. package/dist/auth/secureFiles.d.ts +37 -0
  24. package/dist/auth/secureFiles.js +95 -0
  25. package/dist/auth/tokenStore.d.ts +43 -6
  26. package/dist/auth/tokenStore.js +260 -79
  27. package/dist/avatar/providers/DIDAvatar.js +49 -45
  28. package/dist/avatar/providers/ReplicateAvatar.js +33 -40
  29. package/dist/browser/neurolink.min.js +485 -558
  30. package/dist/cli/commands/auth.js +112 -70
  31. package/dist/cli/commands/proxy.d.ts +4 -0
  32. package/dist/cli/commands/proxy.js +128 -13
  33. package/dist/cli/commands/serve.d.ts +10 -1
  34. package/dist/cli/commands/serve.js +98 -17
  35. package/dist/cli/commands/server.js +17 -5
  36. package/dist/cli/index.js +146 -5
  37. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  38. package/dist/cli/utils/envManager.js +17 -3
  39. package/dist/cli/utils/serverUtils.d.ts +39 -3
  40. package/dist/cli/utils/serverUtils.js +63 -6
  41. package/dist/core/baseProvider.js +7 -1
  42. package/dist/core/modules/MessageBuilder.js +5 -0
  43. package/dist/core/modules/ToolsManager.d.ts +46 -1
  44. package/dist/core/modules/ToolsManager.js +190 -43
  45. package/dist/files/fileReferenceRegistry.d.ts +2 -23
  46. package/dist/files/fileReferenceRegistry.js +55 -5
  47. package/dist/hitl/hitlManager.js +1 -0
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.js +6 -1
  50. package/dist/mcp/auth/tokenStorage.d.ts +7 -0
  51. package/dist/mcp/auth/tokenStorage.js +23 -4
  52. package/dist/mcp/externalServerManager.d.ts +13 -3
  53. package/dist/mcp/externalServerManager.js +154 -136
  54. package/dist/mcp/index.d.ts +2 -0
  55. package/dist/mcp/index.js +3 -0
  56. package/dist/mcp/mcpClientFactory.d.ts +14 -0
  57. package/dist/mcp/mcpClientFactory.js +70 -7
  58. package/dist/mcp/projectConfigTrust.d.ts +72 -0
  59. package/dist/mcp/projectConfigTrust.js +237 -0
  60. package/dist/mcp/transportUrlPolicy.d.ts +39 -0
  61. package/dist/mcp/transportUrlPolicy.js +172 -0
  62. package/dist/music/providers/BeatovenMusic.js +35 -46
  63. package/dist/music/providers/ReplicateMusic.js +33 -40
  64. package/dist/neurolink.d.ts +16 -1
  65. package/dist/neurolink.js +64 -11
  66. package/dist/processors/archive/ArchiveProcessor.js +15 -1
  67. package/dist/processors/archive/zipEntryIndex.d.ts +22 -0
  68. package/dist/processors/archive/zipEntryIndex.js +33 -0
  69. package/dist/processors/base/BaseFileProcessor.d.ts +9 -21
  70. package/dist/processors/base/BaseFileProcessor.js +40 -111
  71. package/dist/processors/data/YamlProcessor.d.ts +13 -1
  72. package/dist/processors/data/YamlProcessor.js +93 -2
  73. package/dist/processors/document/OpenDocumentProcessor.js +7 -2
  74. package/dist/processors/document/PptxProcessor.js +12 -7
  75. package/dist/processors/media/VideoProcessor.js +93 -15
  76. package/dist/proxy/inboundAuth.d.ts +89 -0
  77. package/dist/proxy/inboundAuth.js +219 -0
  78. package/dist/proxy/tokenRefresh.d.ts +6 -0
  79. package/dist/proxy/tokenRefresh.js +13 -1
  80. package/dist/proxy/updateState.js +4 -4
  81. package/dist/rag/document/loaders.d.ts +0 -6
  82. package/dist/rag/document/loaders.js +16 -10
  83. package/dist/rag/ragIntegration.d.ts +2 -2
  84. package/dist/rag/ragIntegration.js +35 -10
  85. package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
  86. package/dist/rag/retrieval/vectorQueryTool.js +54 -17
  87. package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
  88. package/dist/server/abstract/baseServerAdapter.js +119 -6
  89. package/dist/server/adapters/expressAdapter.d.ts +3 -0
  90. package/dist/server/adapters/expressAdapter.js +78 -6
  91. package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
  92. package/dist/server/adapters/fastifyAdapter.js +165 -70
  93. package/dist/server/adapters/honoAdapter.d.ts +9 -1
  94. package/dist/server/adapters/honoAdapter.js +99 -21
  95. package/dist/server/adapters/koaAdapter.d.ts +1 -0
  96. package/dist/server/adapters/koaAdapter.js +153 -74
  97. package/dist/server/index.d.ts +2 -0
  98. package/dist/server/index.js +4 -0
  99. package/dist/server/middleware/auth.js +6 -2
  100. package/dist/server/middleware/cache.js +6 -1
  101. package/dist/server/middleware/rateLimit.js +29 -4
  102. package/dist/server/routes/agentRoutes.js +24 -11
  103. package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
  104. package/dist/server/routes/claudeProxyRoutes.js +273 -84
  105. package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
  106. package/dist/server/routes/codexProxyRoutes.js +125 -40
  107. package/dist/server/routes/index.d.ts +4 -0
  108. package/dist/server/routes/index.js +35 -4
  109. package/dist/server/routes/mcpRoutes.d.ts +6 -2
  110. package/dist/server/routes/mcpRoutes.js +21 -7
  111. package/dist/server/routes/memoryRoutes.js +7 -6
  112. package/dist/server/routes/toolRoutes.d.ts +12 -2
  113. package/dist/server/routes/toolRoutes.js +33 -3
  114. package/dist/server/routes/transcribeRoutes.js +60 -13
  115. package/dist/server/utils/clientAddress.d.ts +23 -0
  116. package/dist/server/utils/clientAddress.js +65 -0
  117. package/dist/server/utils/clientError.d.ts +35 -0
  118. package/dist/server/utils/clientError.js +129 -0
  119. package/dist/server/utils/pathMatch.d.ts +21 -0
  120. package/dist/server/utils/pathMatch.js +41 -0
  121. package/dist/server/utils/validation.d.ts +9 -0
  122. package/dist/server/utils/validation.js +9 -0
  123. package/dist/server/websocket/WebSocketHandler.js +22 -3
  124. package/dist/server/websocket/frameQueue.d.ts +19 -0
  125. package/dist/server/websocket/frameQueue.js +41 -3
  126. package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
  127. package/dist/server/websocket/transcribeWebSocket.js +223 -34
  128. package/dist/skills/skillStores.js +5 -0
  129. package/dist/tasks/store/fileTaskStore.d.ts +2 -0
  130. package/dist/tasks/store/fileTaskStore.js +11 -4
  131. package/dist/types/attachments.d.ts +117 -0
  132. package/dist/types/attachments.js +15 -0
  133. package/dist/types/auth.d.ts +6 -0
  134. package/dist/types/cli.d.ts +4 -0
  135. package/dist/types/config.d.ts +56 -0
  136. package/dist/types/file.d.ts +8 -0
  137. package/dist/types/hitl.d.ts +8 -0
  138. package/dist/types/index.d.ts +1 -0
  139. package/dist/types/index.js +1 -0
  140. package/dist/types/mcp.d.ts +98 -0
  141. package/dist/types/multimodal.js +0 -41
  142. package/dist/types/processor.d.ts +14 -1
  143. package/dist/types/proxy.d.ts +62 -3
  144. package/dist/types/rag.d.ts +9 -0
  145. package/dist/types/safeFetch.d.ts +40 -0
  146. package/dist/types/server.d.ts +206 -10
  147. package/dist/types/tools.d.ts +57 -0
  148. package/dist/types/transcribe.d.ts +6 -0
  149. package/dist/utils/attachmentPolicy.d.ts +89 -0
  150. package/dist/utils/attachmentPolicy.js +327 -0
  151. package/dist/utils/dotenvBootstrap.d.ts +48 -19
  152. package/dist/utils/dotenvBootstrap.js +172 -31
  153. package/dist/utils/fileDetector.js +115 -65
  154. package/dist/utils/json/coerce.js +11 -1
  155. package/dist/utils/logSanitize.d.ts +5 -0
  156. package/dist/utils/logSanitize.js +8 -0
  157. package/dist/utils/logger.js +73 -1
  158. package/dist/utils/messageBuilder.d.ts +2 -2
  159. package/dist/utils/messageBuilder.js +118 -105
  160. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
  161. package/dist/utils/multimodalOptionsBuilder.js +2 -0
  162. package/dist/utils/redis.d.ts +12 -0
  163. package/dist/utils/redis.js +72 -0
  164. package/dist/utils/safeFetch.d.ts +22 -1
  165. package/dist/utils/safeFetch.js +132 -38
  166. package/dist/utils/safeStoreId.d.ts +19 -0
  167. package/dist/utils/safeStoreId.js +29 -0
  168. package/dist/utils/schemaConversion.d.ts +30 -3
  169. package/dist/utils/schemaConversion.js +457 -84
  170. package/dist/utils/sizeGuard.js +2 -2
  171. package/dist/utils/ssrfGuard.d.ts +3 -3
  172. package/dist/utils/ssrfGuard.js +71 -39
  173. package/dist/voice/eval/sttEvalStore.d.ts +26 -3
  174. package/dist/voice/eval/sttEvalStore.js +77 -12
  175. package/dist/voice/livekit/agentDefinition.d.ts +18 -0
  176. package/dist/voice/livekit/agentDefinition.js +22 -0
  177. package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
  178. package/dist/voice/livekit/realtimeMcpTools.js +8 -7
  179. package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
  180. package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
  181. package/dist/voice/livekit/voiceAgent.d.ts +2 -2
  182. package/dist/voice/livekit/voiceAgent.js +2 -1
  183. package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
  184. package/dist/voice/livekit/xaiSessionWatch.js +30 -0
  185. package/dist/voice/providers/GeminiLive.js +21 -6
  186. package/dist/voice/providers/GoogleSTT.js +8 -5
  187. package/dist/voice/transcribe.d.ts +2 -2
  188. package/dist/voice/transcribe.js +34 -18
  189. package/dist/voice/whistle/audio.js +11 -5
  190. package/docs-site/static/search-index.json +71 -49
  191. package/package.json +171 -156
  192. package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
@@ -10,6 +10,7 @@ import { ProviderFactory } from "../../factories/providerFactory.js";
10
10
  import { withSpan } from "../../telemetry/withSpan.js";
11
11
  import { tracers } from "../../telemetry/tracers.js";
12
12
  import { logger } from "../../utils/logger.js";
13
+ import { vetRegexPattern } from "../../middleware/utils/regexSafety.js";
13
14
  import { rerank } from "../reranker/reranker.js";
14
15
  /**
15
16
  * Creates a vector query tool for semantic search
@@ -132,11 +133,19 @@ export function createVectorQueryTool(config, vectorStore) {
132
133
  },
133
134
  };
134
135
  }
136
+ /** Longest `$regex` filter pattern accepted; a secondary cap behind vetting. */
137
+ const MAX_FILTER_REGEX_LENGTH = 200;
138
+ /** Longest metadata value a `$regex` filter is run against. */
139
+ const MAX_FILTER_REGEX_SUBJECT = 10_000;
140
+ /** Distinct patterns remembered per store before the verdict cache is reset. */
141
+ const MAX_FILTER_REGEX_CACHE = 256;
135
142
  /**
136
143
  * In-memory vector store implementation for testing and development
137
144
  */
138
145
  export class InMemoryVectorStore {
139
146
  vectors = new Map();
147
+ /** Vetted `$regex` patterns: the compiled expression, or null when refused. */
148
+ regexFilterCache = new Map();
140
149
  /**
141
150
  * Add vectors to an index
142
151
  */
@@ -272,23 +281,14 @@ export class InMemoryVectorStore {
272
281
  return false;
273
282
  }
274
283
  if ("$regex" in ops) {
275
- const pattern = ops.$regex;
276
- let regexMatches = false;
277
- // Guard against ReDoS: reject excessively long patterns and limit
278
- // the tested string length to prevent pathological backtracking.
279
- if (pattern.length <= 200) {
280
- try {
281
- const re = new RegExp(pattern);
282
- const testValue = typeof fieldValue === "string"
283
- ? fieldValue.slice(0, 10_000)
284
- : "";
285
- regexMatches = re.test(testValue);
286
- }
287
- catch {
288
- // Invalid regex pattern — treat as non-match
289
- regexMatches = false;
290
- }
291
- }
284
+ // The pattern is chosen by the model and the value it runs on
285
+ // comes from ingested documents, so both halves of a ReDoS are
286
+ // attacker-influenced. A length cap alone does not bound
287
+ // backtracking (`^(a+)+$` is 7 characters); `vetRegexPattern`
288
+ // reads the pattern's structure and refuses the ones that can
289
+ // backtrack catastrophically. A refused or invalid pattern is a
290
+ // non-match, as an invalid one always was.
291
+ const regexMatches = this.matchesRegexFilter(ops.$regex, fieldValue);
292
292
  if (!regexMatches) {
293
293
  return false;
294
294
  }
@@ -304,6 +304,43 @@ export class InMemoryVectorStore {
304
304
  }
305
305
  return true;
306
306
  }
307
+ /**
308
+ * Evaluate a `$regex` filter operator. Patterns are vetted once per store
309
+ * and the verdict cached, so a refused pattern costs one analysis.
310
+ */
311
+ matchesRegexFilter(pattern, fieldValue) {
312
+ if (typeof pattern !== "string" ||
313
+ pattern.length > MAX_FILTER_REGEX_LENGTH) {
314
+ return false;
315
+ }
316
+ let compiled = this.regexFilterCache.get(pattern);
317
+ if (compiled === undefined) {
318
+ const verdict = vetRegexPattern(pattern);
319
+ if (verdict.safe) {
320
+ try {
321
+ compiled = new RegExp(pattern);
322
+ }
323
+ catch {
324
+ compiled = null;
325
+ }
326
+ }
327
+ else {
328
+ logger.debug("[InMemoryVectorStore] Refused a $regex filter pattern that could stall the process", { reason: verdict.reason });
329
+ compiled = null;
330
+ }
331
+ if (this.regexFilterCache.size >= MAX_FILTER_REGEX_CACHE) {
332
+ this.regexFilterCache.clear();
333
+ }
334
+ this.regexFilterCache.set(pattern, compiled);
335
+ }
336
+ if (compiled === null) {
337
+ return false;
338
+ }
339
+ const testValue = typeof fieldValue === "string"
340
+ ? fieldValue.slice(0, MAX_FILTER_REGEX_SUBJECT)
341
+ : "";
342
+ return compiled.test(testValue);
343
+ }
307
344
  /**
308
345
  * Calculate cosine similarity between two vectors
309
346
  */
@@ -7,6 +7,8 @@ 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
+ /** The interface an adapter binds unless told otherwise: this host only. */
11
+ export declare const DEFAULT_SERVER_HOST = "127.0.0.1";
10
12
  import type { MiddlewareDefinition, RedactionConfig, RequiredServerAdapterConfig, RequiredShutdownConfig, RouteDefinition, ServerAdapterConfig, ServerAdapterEvents, ServerCarriedContext, ServerContext, ServerLifecycleState, ServerStatus, TrackedConnection } from "../../types/index.js";
11
13
  /**
12
14
  * Abstract base class for server adapters
@@ -29,7 +31,47 @@ export declare abstract class BaseServerAdapter extends EventEmitter {
29
31
  protected activeConnections: Map<string, TrackedConnection>;
30
32
  protected readonly shutdownConfig: RequiredShutdownConfig;
31
33
  private carryRequestedByRoutes;
34
+ private sensitiveRoutesRegistered;
35
+ private sensitiveRoutesAllowedOpen;
32
36
  constructor(neurolink: NeuroLink, config?: ServerAdapterConfig);
37
+ /**
38
+ * Whether middleware marked `authenticates: true` is registered (the
39
+ * built-in authentication middleware marks itself). The tool and MCP
40
+ * execution routes ask on every request; `registerAllRoutes()` wires them.
41
+ */
42
+ hasAuthenticationMiddleware(): boolean;
43
+ /**
44
+ * Called by `registerAllRoutes()`: routes that act on the host (tool and
45
+ * MCP execution) are mounted, so `start()` warns when no authentication
46
+ * middleware guards them. The check runs at start, when the middleware set
47
+ * is final, so registering authentication after the routes is fine.
48
+ */
49
+ noteSensitiveRoutes(options?: {
50
+ allowUnauthenticated?: boolean;
51
+ }): void;
52
+ /**
53
+ * The warning promised by `noteSensitiveRoutes()`, always on stderr: a
54
+ * `logger.warn` is dropped unless debug logging is on, which would hide a
55
+ * notice meant for the operator who just started the server.
56
+ */
57
+ protected warnIfSensitiveRoutesUnauthenticated(): void;
58
+ /**
59
+ * Give the handler a way to notice that the client is gone or the request
60
+ * is over time. The controller is this request's own unless middleware
61
+ * already attached one that the adapter carried; `applyTimeout` arms the
62
+ * configured request timeout, which the caller leaves off for a streaming
63
+ * route (a stream legitimately outlives it, as under Hono's own timeout
64
+ * middleware). `release()` disarms the timer once the response is written.
65
+ */
66
+ protected attachRequestAbort(ctx: ServerContext, options: {
67
+ onClientClose: (listener: () => void) => void;
68
+ applyTimeout: boolean;
69
+ }): {
70
+ controller: AbortController;
71
+ deadline: Promise<never> | undefined;
72
+ timedOut: () => boolean;
73
+ release: () => void;
74
+ };
33
75
  /**
34
76
  * Initialize the underlying server framework
35
77
  * May be async: adapters that dynamically import their framework package
@@ -146,6 +188,7 @@ export declare abstract class BaseServerAdapter extends EventEmitter {
146
188
  query?: Record<string, string>;
147
189
  params?: Record<string, string>;
148
190
  body?: unknown;
191
+ remoteAddress?: string;
149
192
  }): ServerContext;
150
193
  /**
151
194
  * Register built-in middleware
@@ -6,10 +6,15 @@
6
6
  import { EventEmitter } from "events";
7
7
  import { getMetricsAggregator, SpanSerializer, SpanStatus, SpanType, } from "../../observability/index.js";
8
8
  import { withTimeout } from "../../utils/errorHandling.js";
9
+ import { MAX_TIMER_MS } from "../../constants/timeouts.js";
9
10
  import { logger } from "../../utils/logger.js";
10
11
  import { markContextCarried } from "../utils/carriedContextMark.js";
12
+ import { isLoopbackAddress } from "../utils/clientAddress.js";
13
+ import { pathStartsWithSegment } from "../utils/pathMatch.js";
11
14
  import { tryImport } from "../../utils/tryImport.js";
12
- import { DrainTimeoutError, InvalidLifecycleStateError, MissingDependencyError, ServerAdapterError, ShutdownTimeoutError, } from "../errors.js";
15
+ import { ConfigurationError, DrainTimeoutError, InvalidLifecycleStateError, MissingDependencyError, ServerAdapterError, ServerTimeoutError, ShutdownTimeoutError, } from "../errors.js";
16
+ /** The interface an adapter binds unless told otherwise: this host only. */
17
+ export const DEFAULT_SERVER_HOST = "127.0.0.1";
13
18
  /**
14
19
  * Abstract base class for server adapters
15
20
  * Provides common functionality and defines the interface for framework-specific implementations
@@ -37,6 +42,9 @@ export class BaseServerAdapter extends EventEmitter {
37
42
  // Set by enableMiddlewareStateCarry(); the carryMiddlewareState option is the
38
43
  // other way in.
39
44
  carryRequestedByRoutes = false;
45
+ // Set by noteSensitiveRoutes(); read when the server starts.
46
+ sensitiveRoutesRegistered = false;
47
+ sensitiveRoutesAllowedOpen = false;
40
48
  constructor(neurolink, config = {}) {
41
49
  super();
42
50
  this.neurolink = neurolink;
@@ -53,7 +61,9 @@ export class BaseServerAdapter extends EventEmitter {
53
61
  // Apply defaults
54
62
  this.config = {
55
63
  port: config.port ?? 3000,
56
- host: config.host ?? "0.0.0.0",
64
+ // Loopback unless the operator names an interface: the routes run tools
65
+ // and remove MCP servers, so reachability is opt-in.
66
+ host: config.host ?? DEFAULT_SERVER_HOST,
57
67
  basePath: config.basePath ?? "/api",
58
68
  cors: {
59
69
  enabled: config.cors?.enabled ?? true,
@@ -85,6 +95,8 @@ export class BaseServerAdapter extends EventEmitter {
85
95
  "Too many requests, please try again later",
86
96
  skipPaths: config.rateLimit?.skipPaths,
87
97
  keyGenerator: config.rateLimit?.keyGenerator,
98
+ trustProxy: config.rateLimit?.trustProxy ?? false,
99
+ trustedProxies: config.rateLimit?.trustedProxies ?? [],
88
100
  },
89
101
  bodyParser: {
90
102
  enabled: config.bodyParser?.enabled ?? true,
@@ -107,6 +119,100 @@ export class BaseServerAdapter extends EventEmitter {
107
119
  shutdown: this.shutdownConfig,
108
120
  carryMiddlewareState: config.carryMiddlewareState ?? false,
109
121
  };
122
+ // A wildcard origin with credentials is what the CORS specification
123
+ // forbids and what an adapter reflecting the request's Origin would grant:
124
+ // any page could make credentialed calls. Refuse it for every adapter.
125
+ if (this.config.cors.enabled &&
126
+ this.config.cors.credentials &&
127
+ this.config.cors.origins.includes("*")) {
128
+ throw new ConfigurationError('cors.credentials cannot be combined with the wildcard origin "*"; list the origins that may send credentials', { origins: this.config.cors.origins });
129
+ }
130
+ }
131
+ /**
132
+ * Whether middleware marked `authenticates: true` is registered (the
133
+ * built-in authentication middleware marks itself). The tool and MCP
134
+ * execution routes ask on every request; `registerAllRoutes()` wires them.
135
+ */
136
+ hasAuthenticationMiddleware() {
137
+ return this.middlewares.some((middleware) => middleware.authenticates === true);
138
+ }
139
+ /**
140
+ * Called by `registerAllRoutes()`: routes that act on the host (tool and
141
+ * MCP execution) are mounted, so `start()` warns when no authentication
142
+ * middleware guards them. The check runs at start, when the middleware set
143
+ * is final, so registering authentication after the routes is fine.
144
+ */
145
+ noteSensitiveRoutes(options) {
146
+ this.sensitiveRoutesRegistered = true;
147
+ if (options?.allowUnauthenticated) {
148
+ this.sensitiveRoutesAllowedOpen = true;
149
+ }
150
+ }
151
+ /**
152
+ * The warning promised by `noteSensitiveRoutes()`, always on stderr: a
153
+ * `logger.warn` is dropped unless debug logging is on, which would hide a
154
+ * notice meant for the operator who just started the server.
155
+ */
156
+ warnIfSensitiveRoutesUnauthenticated() {
157
+ if (!this.sensitiveRoutesRegistered ||
158
+ this.sensitiveRoutesAllowedOpen ||
159
+ this.hasAuthenticationMiddleware()) {
160
+ return;
161
+ }
162
+ const where = isLoopbackAddress(this.config.host)
163
+ ? `loopback only (${this.config.host})`
164
+ : `${this.config.host}, reachable from the network`;
165
+ logger.alwaysStderr(`[ServerAdapter] WARNING: no authentication middleware is registered and the server listens on ${where}. ` +
166
+ "The tool and MCP execution routes answer 403 until authentication middleware (marked authenticates: true) is registered, " +
167
+ "or allowUnauthenticated: true / NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED=1 opts out of that gate.");
168
+ }
169
+ /**
170
+ * Give the handler a way to notice that the client is gone or the request
171
+ * is over time. The controller is this request's own unless middleware
172
+ * already attached one that the adapter carried; `applyTimeout` arms the
173
+ * configured request timeout, which the caller leaves off for a streaming
174
+ * route (a stream legitimately outlives it, as under Hono's own timeout
175
+ * middleware). `release()` disarms the timer once the response is written.
176
+ */
177
+ attachRequestAbort(ctx, options) {
178
+ const controller = ctx.abortController ?? new AbortController();
179
+ ctx.abortController = controller;
180
+ ctx.abortSignal = controller.signal;
181
+ let timedOut = false;
182
+ let timer;
183
+ const timeoutMs = this.config.timeout;
184
+ // Node turns an over-range delay into 1 ms; keep long request budgets
185
+ // at the timer limit, as the readiness deadline does through withTimeout.
186
+ const delayMs = timeoutMs > MAX_TIMER_MS ? MAX_TIMER_MS : timeoutMs;
187
+ const deadline = options.applyTimeout && timeoutMs > 0
188
+ ? new Promise((_, reject) => {
189
+ timer = setTimeout(() => {
190
+ timedOut = true;
191
+ const error = new ServerTimeoutError(timeoutMs, `${ctx.method} ${ctx.path}`, ctx.requestId);
192
+ controller.abort(error);
193
+ reject(error);
194
+ }, delayMs);
195
+ })
196
+ : undefined;
197
+ // A rejection nobody awaits (the handler won on the race) must not
198
+ // surface as unhandled.
199
+ deadline?.catch(() => undefined);
200
+ options.onClientClose(() => {
201
+ if (!controller.signal.aborted) {
202
+ controller.abort(new Error("Client disconnected"));
203
+ }
204
+ });
205
+ return {
206
+ controller,
207
+ deadline,
208
+ timedOut: () => timedOut,
209
+ release: () => {
210
+ if (timer) {
211
+ clearTimeout(timer);
212
+ timer = undefined;
213
+ }
214
+ },
215
+ };
110
216
  }
111
217
  /**
112
218
  * The listening Node `http.Server`, once `start()` has run and the runtime
@@ -237,11 +343,17 @@ export class BaseServerAdapter extends EventEmitter {
237
343
  }
238
344
  }
239
345
  // Register all routes in the group with prefix applied
346
+ const prefix = this.normalizePath(group.prefix);
347
+ // The group's root segment ("/api" of "/api/health"): a route whose path
348
+ // already lives under it is a full path, as every built-in route is.
349
+ // Testing only the whole prefix put `/api/version`, declared in the health
350
+ // group, at `/api/health/api/version`.
351
+ const root = `/${prefix.split("/").filter(Boolean)[0] ?? ""}`;
240
352
  for (const route of group.routes) {
241
- // Only prepend prefix if route path doesn't already start with it
242
- // (route definitions include full paths like /api/agent/execute)
243
- const needsPrefix = !route.path.startsWith(group.prefix);
244
- const prefixedPath = this.normalizePath(needsPrefix ? `${group.prefix}${route.path}` : route.path);
353
+ const needsPrefix = prefix !== "/" &&
354
+ !pathStartsWithSegment(route.path, prefix) &&
355
+ !pathStartsWithSegment(route.path, root);
356
+ const prefixedPath = this.normalizePath(needsPrefix ? `${prefix}${route.path}` : route.path);
245
357
  const prefixedRoute = {
246
358
  ...route,
247
359
  path: prefixedPath,
@@ -336,6 +448,7 @@ export class BaseServerAdapter extends EventEmitter {
336
448
  query: options.query ?? {},
337
449
  params: options.params ?? {},
338
450
  body: options.body,
451
+ remoteAddress: options.remoteAddress,
339
452
  neurolink: this.neurolink,
340
453
  toolRegistry: this.toolRegistry,
341
454
  externalServerManager: this.externalServerManager,
@@ -13,6 +13,7 @@ export declare class ExpressServerAdapter extends BaseServerAdapter {
13
13
  private server?;
14
14
  private frameworkInitialized;
15
15
  private sockets;
16
+ private warnedNoClientAddress;
16
17
  constructor(neurolink: NeuroLink, config?: ServerAdapterConfig);
17
18
  /**
18
19
  * Initialize Express framework asynchronously
@@ -22,6 +23,8 @@ export declare class ExpressServerAdapter extends BaseServerAdapter {
22
23
  * Initialize Express framework with async imports
23
24
  */
24
25
  private initializeFrameworkAsync;
26
+ /** The address a request is counted against; `undefined` when there is none. */
27
+ private clientKeyFor;
25
28
  /**
26
29
  * Override initialize to ensure async framework setup
27
30
  */
@@ -5,9 +5,14 @@
5
5
  import { logger } from "../../utils/logger.js";
6
6
  import { AlreadyRunningError, ServerAdapterError, ServerStartError, ServerStopError, wrapError, } from "../errors.js";
7
7
  import { BaseServerAdapter } from "../abstract/baseServerAdapter.js";
8
+ import { resolveClientAddress } from "../utils/clientAddress.js";
9
+ import { shapeRouteError } from "../utils/clientError.js";
10
+ import { pathMatchesAny } from "../utils/pathMatch.js";
8
11
  import { isErrorResponse } from "../utils/validation.js";
9
12
  import { createBoundedBodyParser } from "../utils/expressBody.js";
10
13
  import { isRawBodyContentType, isWebResponse, readWebResponse, } from "../utils/rawBody.js";
14
+ /** How long `listen()` may take; `config.timeout` is the per-request budget. */
15
+ const STARTUP_TIMEOUT_MS = 30_000;
11
16
  /**
12
17
  * Express-specific server adapter
13
18
  */
@@ -16,6 +21,8 @@ export class ExpressServerAdapter extends BaseServerAdapter {
16
21
  server;
17
22
  frameworkInitialized = false;
18
23
  sockets = new Set();
24
+ // The rate limiter counts nothing it cannot attribute, and says so once.
25
+ warnedNoClientAddress = false;
19
26
  constructor(neurolink, config = {}) {
20
27
  super(neurolink, config);
21
28
  }
@@ -77,6 +84,13 @@ export class ExpressServerAdapter extends BaseServerAdapter {
77
84
  const limiter = rateLimit({
78
85
  windowMs,
79
86
  max: this.config.rateLimit.maxRequests,
87
+ // The key is resolved here, from the TCP peer or, behind a declared
88
+ // proxy, the client it reports; the library's own reading of the
89
+ // request (and its warnings about it) is not used. A request with no
90
+ // attributable address passes rather than share a bucket.
91
+ validate: false,
92
+ keyGenerator: (req) => this.clientKeyFor(req) ?? "",
93
+ skip: (req) => this.clientKeyFor(req) === undefined,
80
94
  message: {
81
95
  error: {
82
96
  code: "RATE_LIMIT_EXCEEDED",
@@ -128,6 +142,20 @@ export class ExpressServerAdapter extends BaseServerAdapter {
128
142
  }
129
143
  this.frameworkInitialized = true;
130
144
  }
145
+ /** The address a request is counted against; `undefined` when there is none. */
146
+ clientKeyFor(req) {
147
+ const key = resolveClientAddress({
148
+ remoteAddress: req.socket?.remoteAddress,
149
+ forwardedFor: req.headers["x-forwarded-for"],
150
+ trustProxy: this.config.rateLimit.trustProxy,
151
+ trustedProxies: this.config.rateLimit.trustedProxies,
152
+ });
153
+ if (key === undefined && !this.warnedNoClientAddress) {
154
+ this.warnedNoClientAddress = true;
155
+ logger.alwaysStderr("[ExpressAdapter] A request arrived with no peer address, so the rate limiter cannot count it; configure rateLimit.keyGenerator to limit by another key.");
156
+ }
157
+ return key;
158
+ }
131
159
  /**
132
160
  * Override initialize to ensure async framework setup
133
161
  */
@@ -152,7 +180,7 @@ export class ExpressServerAdapter extends BaseServerAdapter {
152
180
  */
153
181
  registerFrameworkRoute(route) {
154
182
  const method = route.method.toLowerCase();
155
- this.app[method](route.path, async (req, res, next) => {
183
+ this.app[method](route.path, async (req, res, _next) => {
156
184
  const requestId = req.headers["x-request-id"];
157
185
  const startTime = Date.now();
158
186
  // Create server context
@@ -164,12 +192,24 @@ export class ExpressServerAdapter extends BaseServerAdapter {
164
192
  query: req.query,
165
193
  params: req.params,
166
194
  body: req.body,
195
+ remoteAddress: req.socket?.remoteAddress,
167
196
  });
168
197
  // Copy response headers from middleware (stored in res.locals by middleware)
169
198
  if (res.locals.responseHeaders) {
170
199
  ctx.responseHeaders = { ...res.locals.responseHeaders };
171
200
  }
172
201
  this.restoreCarriedContext(ctx, res.locals.carriedContext);
202
+ // The handler learns of a client that went away, and a request over
203
+ // `config.timeout` is answered 504 and its work aborted. Express used
204
+ // to apply that timeout only to its own listen().
205
+ const abort = this.attachRequestAbort(ctx, {
206
+ onClientClose: (listener) => res.on("close", () => {
207
+ if (!res.writableFinished) {
208
+ listener();
209
+ }
210
+ }),
211
+ applyTimeout: !route.streaming?.enabled,
212
+ });
173
213
  // Emit request event
174
214
  this.emit("request", {
175
215
  requestId,
@@ -183,7 +223,9 @@ export class ExpressServerAdapter extends BaseServerAdapter {
183
223
  return this.handleStreamingResponse(res, ctx, route);
184
224
  }
185
225
  // Execute handler
186
- const result = await route.handler(ctx);
226
+ const result = abort.deadline
227
+ ? await Promise.race([route.handler(ctx), abort.deadline])
228
+ : await route.handler(ctx);
187
229
  const duration = Date.now() - startTime;
188
230
  // A route that answers in its own wire format returns a web Response.
189
231
  if (isWebResponse(result)) {
@@ -255,7 +297,31 @@ export class ExpressServerAdapter extends BaseServerAdapter {
255
297
  });
256
298
  }
257
299
  catch (error) {
258
- next(error);
300
+ // The exception's text stays in the log under this request id; the
301
+ // client gets a fixed message for its category.
302
+ const shaped = shapeRouteError("ExpressAdapter", requestId, error);
303
+ this.emitRequestError({
304
+ requestId,
305
+ error: error instanceof Error ? error : new Error(String(error)),
306
+ timestamp: new Date(),
307
+ });
308
+ this.emit("response", {
309
+ requestId,
310
+ statusCode: shaped.status,
311
+ duration: Date.now() - startTime,
312
+ timestamp: new Date(),
313
+ });
314
+ if (res.headersSent) {
315
+ res.end();
316
+ return;
317
+ }
318
+ res.status(shaped.status).json({
319
+ error: { code: shaped.code, message: shaped.message },
320
+ metadata: { requestId, timestamp: new Date().toISOString() },
321
+ });
322
+ }
323
+ finally {
324
+ abort.release();
259
325
  }
260
326
  });
261
327
  // Error handling middleware (should be registered once after all routes)
@@ -335,9 +401,13 @@ export class ExpressServerAdapter extends BaseServerAdapter {
335
401
  res.end();
336
402
  }
337
403
  catch (error) {
404
+ const shaped = shapeRouteError("ExpressAdapter", ctx.requestId, error);
338
405
  res.write(`event: error\n`);
339
406
  res.write(`data: ${JSON.stringify({
340
- error: error instanceof Error ? error.message : "Stream error",
407
+ error: shaped.message,
408
+ code: shaped.code,
409
+ status: shaped.status,
410
+ requestId: ctx.requestId,
341
411
  })}\n\n`);
342
412
  res.end();
343
413
  }
@@ -350,7 +420,7 @@ export class ExpressServerAdapter extends BaseServerAdapter {
350
420
  for (const path of paths) {
351
421
  this.app.use(path, async (req, res, next) => {
352
422
  // Skip excluded paths
353
- if (middleware.excludePaths?.some((p) => req.path.startsWith(p))) {
423
+ if (pathMatchesAny(req.path, middleware.excludePaths)) {
354
424
  return next();
355
425
  }
356
426
  // Initialize response headers storage in res.locals if not present
@@ -367,6 +437,7 @@ export class ExpressServerAdapter extends BaseServerAdapter {
367
437
  query: req.query,
368
438
  params: req.params,
369
439
  body: req.body,
440
+ remoteAddress: req.socket?.remoteAddress,
370
441
  });
371
442
  // Copy existing response headers to context
372
443
  ctx.responseHeaders = { ...res.locals.responseHeaders };
@@ -408,13 +479,14 @@ export class ExpressServerAdapter extends BaseServerAdapter {
408
479
  }
409
480
  this.lifecycleState = "starting";
410
481
  const { port, host } = this.config;
411
- const startupTimeout = this.config.timeout || 30000;
482
+ const startupTimeout = STARTUP_TIMEOUT_MS;
412
483
  const startPromise = new Promise((resolve, reject) => {
413
484
  try {
414
485
  this.server = this.app.listen(port, host, () => {
415
486
  this.isRunning = true;
416
487
  this.startTime = new Date();
417
488
  this.lifecycleState = "running";
489
+ this.warnIfSensitiveRoutesUnauthenticated();
418
490
  // Track connections for graceful shutdown
419
491
  this.server?.on("connection", (socket) => {
420
492
  const connectionId = `conn-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
@@ -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 warnedNoClientAddress;
16
17
  private connectionIds;
17
18
  constructor(neurolink: NeuroLink, config?: ServerAdapterConfig);
18
19
  /**
@@ -24,6 +25,8 @@ export declare class FastifyServerAdapter extends BaseServerAdapter {
24
25
  * Initialize Fastify framework with async imports
25
26
  */
26
27
  private initializeFrameworkAsync;
28
+ /** The address a request is counted against; `undefined` when there is none. */
29
+ private clientKeyFor;
27
30
  /**
28
31
  * Parse body limit string to number (bytes)
29
32
  */