@juspay/neurolink 13.8.17 → 13.8.19

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 (214) hide show
  1. package/CHANGELOG.md +5 -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/providers/openaiChatCompletionsClient.js +12 -3
  77. package/dist/proxy/accountLedger.js +6 -8
  78. package/dist/proxy/geminiFormat.d.ts +3 -11
  79. package/dist/proxy/geminiFormat.js +11 -2
  80. package/dist/proxy/inboundAuth.d.ts +89 -0
  81. package/dist/proxy/inboundAuth.js +219 -0
  82. package/dist/proxy/openaiFormat.d.ts +3 -9
  83. package/dist/proxy/openaiFormat.js +21 -15
  84. package/dist/proxy/proxyTranslationEngine.d.ts +2 -6
  85. package/dist/proxy/proxyTranslationEngine.js +25 -14
  86. package/dist/proxy/routingPolicy.d.ts +3 -1
  87. package/dist/proxy/routingPolicy.js +7 -8
  88. package/dist/proxy/tokenRefresh.d.ts +6 -0
  89. package/dist/proxy/tokenRefresh.js +13 -1
  90. package/dist/proxy/updateState.js +4 -4
  91. package/dist/rag/document/loaders.d.ts +0 -6
  92. package/dist/rag/document/loaders.js +16 -10
  93. package/dist/rag/ragIntegration.d.ts +2 -2
  94. package/dist/rag/ragIntegration.js +35 -10
  95. package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
  96. package/dist/rag/retrieval/vectorQueryTool.js +54 -17
  97. package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
  98. package/dist/server/abstract/baseServerAdapter.js +119 -6
  99. package/dist/server/adapters/expressAdapter.d.ts +3 -0
  100. package/dist/server/adapters/expressAdapter.js +78 -6
  101. package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
  102. package/dist/server/adapters/fastifyAdapter.js +165 -70
  103. package/dist/server/adapters/honoAdapter.d.ts +9 -1
  104. package/dist/server/adapters/honoAdapter.js +99 -21
  105. package/dist/server/adapters/koaAdapter.d.ts +1 -0
  106. package/dist/server/adapters/koaAdapter.js +153 -74
  107. package/dist/server/index.d.ts +2 -0
  108. package/dist/server/index.js +4 -0
  109. package/dist/server/middleware/auth.js +6 -2
  110. package/dist/server/middleware/cache.js +6 -1
  111. package/dist/server/middleware/rateLimit.js +29 -4
  112. package/dist/server/routes/agentRoutes.js +24 -11
  113. package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
  114. package/dist/server/routes/claudeProxyRoutes.js +273 -84
  115. package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
  116. package/dist/server/routes/codexProxyRoutes.js +125 -40
  117. package/dist/server/routes/index.d.ts +4 -0
  118. package/dist/server/routes/index.js +35 -4
  119. package/dist/server/routes/mcpRoutes.d.ts +6 -2
  120. package/dist/server/routes/mcpRoutes.js +21 -7
  121. package/dist/server/routes/memoryRoutes.js +7 -6
  122. package/dist/server/routes/toolRoutes.d.ts +12 -2
  123. package/dist/server/routes/toolRoutes.js +33 -3
  124. package/dist/server/routes/transcribeRoutes.js +60 -13
  125. package/dist/server/utils/clientAddress.d.ts +23 -0
  126. package/dist/server/utils/clientAddress.js +65 -0
  127. package/dist/server/utils/clientError.d.ts +35 -0
  128. package/dist/server/utils/clientError.js +129 -0
  129. package/dist/server/utils/pathMatch.d.ts +21 -0
  130. package/dist/server/utils/pathMatch.js +41 -0
  131. package/dist/server/utils/validation.d.ts +9 -0
  132. package/dist/server/utils/validation.js +9 -0
  133. package/dist/server/websocket/WebSocketHandler.js +22 -3
  134. package/dist/server/websocket/frameQueue.d.ts +19 -0
  135. package/dist/server/websocket/frameQueue.js +41 -3
  136. package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
  137. package/dist/server/websocket/transcribeWebSocket.js +223 -34
  138. package/dist/skills/skillStores.js +5 -0
  139. package/dist/tasks/store/fileTaskStore.d.ts +2 -0
  140. package/dist/tasks/store/fileTaskStore.js +11 -4
  141. package/dist/types/attachments.d.ts +117 -0
  142. package/dist/types/attachments.js +15 -0
  143. package/dist/types/auth.d.ts +6 -0
  144. package/dist/types/cli.d.ts +4 -0
  145. package/dist/types/config.d.ts +56 -0
  146. package/dist/types/file.d.ts +8 -0
  147. package/dist/types/hitl.d.ts +8 -0
  148. package/dist/types/index.d.ts +1 -0
  149. package/dist/types/index.js +1 -0
  150. package/dist/types/mcp.d.ts +98 -0
  151. package/dist/types/multimodal.js +0 -41
  152. package/dist/types/openaiCompatible.d.ts +1 -1
  153. package/dist/types/processor.d.ts +14 -1
  154. package/dist/types/proxy.d.ts +71 -16
  155. package/dist/types/proxyClient.d.ts +2 -0
  156. package/dist/types/rag.d.ts +9 -0
  157. package/dist/types/safeFetch.d.ts +40 -0
  158. package/dist/types/server.d.ts +206 -10
  159. package/dist/types/tools.d.ts +57 -0
  160. package/dist/types/transcribe.d.ts +6 -0
  161. package/dist/utils/attachmentPolicy.d.ts +89 -0
  162. package/dist/utils/attachmentPolicy.js +327 -0
  163. package/dist/utils/dotenvBootstrap.d.ts +48 -19
  164. package/dist/utils/dotenvBootstrap.js +172 -31
  165. package/dist/utils/fileDetector.js +115 -65
  166. package/dist/utils/json/coerce.js +11 -1
  167. package/dist/utils/logSanitize.d.ts +5 -0
  168. package/dist/utils/logSanitize.js +8 -0
  169. package/dist/utils/logger.js +73 -1
  170. package/dist/utils/messageBuilder.d.ts +2 -2
  171. package/dist/utils/messageBuilder.js +118 -105
  172. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
  173. package/dist/utils/multimodalOptionsBuilder.js +2 -0
  174. package/dist/utils/redis.d.ts +12 -0
  175. package/dist/utils/redis.js +72 -0
  176. package/dist/utils/retryAfter.d.ts +32 -0
  177. package/dist/utils/retryAfter.js +135 -11
  178. package/dist/utils/safeFetch.d.ts +22 -1
  179. package/dist/utils/safeFetch.js +132 -38
  180. package/dist/utils/safeStoreId.d.ts +19 -0
  181. package/dist/utils/safeStoreId.js +29 -0
  182. package/dist/utils/schemaConversion.d.ts +30 -3
  183. package/dist/utils/schemaConversion.js +457 -84
  184. package/dist/utils/sizeGuard.js +2 -2
  185. package/dist/utils/ssrfGuard.d.ts +3 -3
  186. package/dist/utils/ssrfGuard.js +71 -39
  187. package/dist/voice/eval/sttEvalStore.d.ts +26 -3
  188. package/dist/voice/eval/sttEvalStore.js +77 -12
  189. package/dist/voice/livekit/agentDefinition.d.ts +18 -0
  190. package/dist/voice/livekit/agentDefinition.js +22 -0
  191. package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
  192. package/dist/voice/livekit/realtimeMcpTools.js +8 -7
  193. package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
  194. package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
  195. package/dist/voice/livekit/voiceAgent.d.ts +2 -2
  196. package/dist/voice/livekit/voiceAgent.js +2 -1
  197. package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
  198. package/dist/voice/livekit/xaiSessionWatch.js +30 -0
  199. package/dist/voice/providers/GeminiLive.js +21 -6
  200. package/dist/voice/providers/GoogleSTT.js +8 -5
  201. package/dist/voice/transcribe.d.ts +2 -2
  202. package/dist/voice/transcribe.js +34 -18
  203. package/dist/voice/whistle/audio.js +11 -5
  204. package/dist/workflow/workflows/adaptiveWorkflow.d.ts +2 -2
  205. package/dist/workflow/workflows/adaptiveWorkflow.js +10 -10
  206. package/dist/workflow/workflows/consensusWorkflow.d.ts +4 -4
  207. package/dist/workflow/workflows/consensusWorkflow.js +12 -12
  208. package/dist/workflow/workflows/fallbackWorkflow.d.ts +3 -3
  209. package/dist/workflow/workflows/fallbackWorkflow.js +7 -7
  210. package/dist/workflow/workflows/multiJudgeWorkflow.d.ts +7 -7
  211. package/dist/workflow/workflows/multiJudgeWorkflow.js +31 -31
  212. package/docs-site/static/search-index.json +74 -52
  213. package/package.json +173 -156
  214. package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
@@ -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
  */
@@ -7,8 +7,13 @@ import { logger } from "../../utils/logger.js";
7
7
  import { AlreadyRunningError, ServerAdapterError, ServerStartError, ServerStopError, wrapError, } from "../errors.js";
8
8
  import { withTimeout } from "../../utils/errorHandling.js";
9
9
  import { BaseServerAdapter } from "../abstract/baseServerAdapter.js";
10
+ import { resolveClientAddress } from "../utils/clientAddress.js";
11
+ import { shapeRouteError } from "../utils/clientError.js";
12
+ import { pathMatchesAny, stripQuery } from "../utils/pathMatch.js";
10
13
  import { isErrorResponse } from "../utils/validation.js";
11
14
  import { isRawBodyContentType, isWebResponse, readWebResponse, } from "../utils/rawBody.js";
15
+ /** How long `listen()` may take; `config.timeout` is the per-request budget. */
16
+ const STARTUP_TIMEOUT_MS = 30_000;
12
17
  /**
13
18
  * Fastify-specific server adapter
14
19
  * Provides high-performance HTTP server with schema validation
@@ -16,6 +21,8 @@ import { isRawBodyContentType, isWebResponse, readWebResponse, } from "../utils/
16
21
  export class FastifyServerAdapter extends BaseServerAdapter {
17
22
  app = null;
18
23
  frameworkInitialized = false;
24
+ // The rate limiter counts nothing it cannot attribute, and says so once.
25
+ warnedNoClientAddress = false;
19
26
  // The shutdown drain's record for each in-flight request. Fastify takes
20
27
  // `request.id` from the caller's X-Request-ID, so that id can repeat across
21
28
  // overlapping requests: a record named after it would be shared, and the
@@ -87,15 +94,19 @@ export class FastifyServerAdapter extends BaseServerAdapter {
87
94
  await this.app.register(rateLimitModule.default, {
88
95
  max: this.config.rateLimit.maxRequests,
89
96
  timeWindow: windowMs,
97
+ // The key is resolved here, from the TCP peer or, behind a declared
98
+ // proxy, the client it reports. A request with no attributable
99
+ // address passes rather than share a bucket.
100
+ keyGenerator: (request) => this.clientKeyFor(request) ?? "",
101
+ allowList: (request) => this.clientKeyFor(request) === undefined,
102
+ // The plugin throws what this builds, so the error handler below
103
+ // needs `statusCode` to answer 429; without it every exceeded limit
104
+ // was a 500.
90
105
  errorResponseBuilder: (_request, context) => ({
91
- error: {
92
- code: "RATE_LIMIT_EXCEEDED",
93
- message: this.config.rateLimit.message,
94
- },
95
- metadata: {
96
- retryAfter: Math.ceil(context.ttl / 1000),
97
- timestamp: new Date().toISOString(),
98
- },
106
+ statusCode: 429,
107
+ code: "RATE_LIMIT_EXCEEDED",
108
+ message: this.config.rateLimit.message,
109
+ retryAfter: Math.ceil(context.ttl / 1000),
99
110
  }),
100
111
  });
101
112
  // Add hook to set Retry-After header for 429 responses
@@ -131,7 +142,25 @@ export class FastifyServerAdapter extends BaseServerAdapter {
131
142
  });
132
143
  return;
133
144
  }
134
- if (error.statusCode) {
145
+ // An exceeded rate limit, thrown by @fastify/rate-limit as the object
146
+ // errorResponseBuilder built.
147
+ if (error.statusCode === 429) {
148
+ reply.status(429).send({
149
+ error: {
150
+ code: "RATE_LIMIT_EXCEEDED",
151
+ message: this.config.rateLimit.message,
152
+ },
153
+ metadata: {
154
+ requestId,
155
+ timestamp: new Date().toISOString(),
156
+ retryAfter: error.retryAfter,
157
+ },
158
+ });
159
+ return;
160
+ }
161
+ // Fastify's own client errors (an unparsable body, an unsupported
162
+ // media type) carry a message the client may see; a 5xx's does not.
163
+ if (error.statusCode && error.statusCode < 500) {
135
164
  reply.status(error.statusCode).send({
136
165
  error: {
137
166
  code: `HTTP_${error.statusCode}`,
@@ -170,6 +199,21 @@ export class FastifyServerAdapter extends BaseServerAdapter {
170
199
  });
171
200
  this.frameworkInitialized = true;
172
201
  }
202
+ /** The address a request is counted against; `undefined` when there is none. */
203
+ clientKeyFor(request) {
204
+ const forwarded = request.headers["x-forwarded-for"];
205
+ const key = resolveClientAddress({
206
+ remoteAddress: request.socket?.remoteAddress ?? request.ip,
207
+ forwardedFor: Array.isArray(forwarded) ? forwarded.join(",") : forwarded,
208
+ trustProxy: this.config.rateLimit.trustProxy,
209
+ trustedProxies: this.config.rateLimit.trustedProxies,
210
+ });
211
+ if (key === undefined && !this.warnedNoClientAddress) {
212
+ this.warnedNoClientAddress = true;
213
+ logger.alwaysStderr("[FastifyAdapter] A request arrived with no peer address, so the rate limiter cannot count it; configure rateLimit.keyGenerator to limit by another key.");
214
+ }
215
+ return key;
216
+ }
173
217
  /**
174
218
  * Parse body limit string to number (bytes)
175
219
  */
@@ -233,11 +277,12 @@ export class FastifyServerAdapter extends BaseServerAdapter {
233
277
  const ctx = this.createContext({
234
278
  requestId,
235
279
  method: request.method,
236
- path: request.url.split("?")[0], // Remove query string
280
+ path: stripQuery(request.url),
237
281
  headers: request.headers,
238
282
  query: request.query,
239
283
  params: request.params,
240
284
  body: request.body,
285
+ remoteAddress: request.socket?.remoteAddress ?? request.ip,
241
286
  });
242
287
  // Copy response headers from middleware (stored in request by middleware)
243
288
  const reqWithHeaders = request;
@@ -245,6 +290,17 @@ export class FastifyServerAdapter extends BaseServerAdapter {
245
290
  ctx.responseHeaders = { ...reqWithHeaders.responseHeaders };
246
291
  }
247
292
  this.restoreCarriedContext(ctx, reqWithHeaders.carriedContext);
293
+ // The handler learns of a client that went away, and a request over
294
+ // `config.timeout` is answered 504 and its work aborted. Fastify used
295
+ // to apply that timeout only to its own listen().
296
+ const abort = this.attachRequestAbort(ctx, {
297
+ onClientClose: (listener) => request.raw.on("close", () => {
298
+ if (!reply.raw.writableFinished) {
299
+ listener();
300
+ }
301
+ }),
302
+ applyTimeout: !route.streaming?.enabled,
303
+ });
248
304
  // Emit request event
249
305
  this.emit("request", {
250
306
  requestId,
@@ -252,38 +308,66 @@ export class FastifyServerAdapter extends BaseServerAdapter {
252
308
  path: ctx.path,
253
309
  timestamp: new Date(),
254
310
  });
255
- // Handle streaming if configured
256
- if (route.streaming?.enabled) {
257
- return this.handleStreamingResponse(reply, ctx, route);
258
- }
259
- // Execute handler
260
- const result = await route.handler(ctx);
261
- const duration = Date.now() - startTime;
262
- // A route that answers in its own wire format returns a web Response.
263
- if (isWebResponse(result)) {
264
- const raw = await readWebResponse(result);
265
- this.emit("response", {
266
- requestId,
267
- statusCode: raw.status,
268
- duration,
269
- timestamp: new Date(),
270
- });
271
- for (const [key, value] of Object.entries({
272
- ...ctx.responseHeaders,
273
- ...raw.headers,
274
- })) {
275
- reply.header(key, value);
311
+ try {
312
+ // Handle streaming if configured
313
+ if (route.streaming?.enabled) {
314
+ return await this.handleStreamingResponse(reply, ctx, route);
276
315
  }
277
- reply.status(raw.status);
278
- return reply.send(raw.body);
279
- }
280
- // Check if result is an error response
281
- if (isErrorResponse(result)) {
282
- const statusCode = result.httpStatus ?? 500;
283
- // Emit response event with error status
316
+ // Execute handler
317
+ const result = abort.deadline
318
+ ? await Promise.race([route.handler(ctx), abort.deadline])
319
+ : await route.handler(ctx);
320
+ const duration = Date.now() - startTime;
321
+ // A route that answers in its own wire format returns a web Response.
322
+ if (isWebResponse(result)) {
323
+ const raw = await readWebResponse(result);
324
+ this.emit("response", {
325
+ requestId,
326
+ statusCode: raw.status,
327
+ duration,
328
+ timestamp: new Date(),
329
+ });
330
+ for (const [key, value] of Object.entries({
331
+ ...ctx.responseHeaders,
332
+ ...raw.headers,
333
+ })) {
334
+ reply.header(key, value);
335
+ }
336
+ reply.status(raw.status);
337
+ return reply.send(raw.body);
338
+ }
339
+ // Check if result is an error response
340
+ if (isErrorResponse(result)) {
341
+ const statusCode = result.httpStatus ?? 500;
342
+ // Emit response event with error status
343
+ this.emit("response", {
344
+ requestId,
345
+ statusCode,
346
+ duration,
347
+ timestamp: new Date(),
348
+ });
349
+ // Apply response headers from middleware and handler
350
+ if (ctx.responseHeaders) {
351
+ for (const [key, value] of Object.entries(ctx.responseHeaders)) {
352
+ reply.header(key, value);
353
+ }
354
+ }
355
+ // Return error response with proper status code
356
+ reply.status(statusCode);
357
+ return {
358
+ error: result.error,
359
+ metadata: {
360
+ ...result.metadata,
361
+ requestId,
362
+ timestamp: new Date().toISOString(),
363
+ duration,
364
+ },
365
+ };
366
+ }
367
+ // Emit response event
284
368
  this.emit("response", {
285
369
  requestId,
286
- statusCode,
370
+ statusCode: 200,
287
371
  duration,
288
372
  timestamp: new Date(),
289
373
  });
@@ -293,40 +377,43 @@ export class FastifyServerAdapter extends BaseServerAdapter {
293
377
  reply.header(key, value);
294
378
  }
295
379
  }
296
- // Return error response with proper status code
297
- reply.status(statusCode);
380
+ // Return formatted response
298
381
  return {
299
- error: result.error,
382
+ data: result,
300
383
  metadata: {
301
- ...result.metadata,
302
384
  requestId,
303
385
  timestamp: new Date().toISOString(),
304
386
  duration,
305
387
  },
306
388
  };
307
389
  }
308
- // Emit response event
309
- this.emit("response", {
310
- requestId,
311
- statusCode: 200,
312
- duration,
313
- timestamp: new Date(),
314
- });
315
- // Apply response headers from middleware and handler
316
- if (ctx.responseHeaders) {
317
- for (const [key, value] of Object.entries(ctx.responseHeaders)) {
318
- reply.header(key, value);
390
+ catch (error) {
391
+ // The exception's text stays in the log under this request id; the
392
+ // client gets a fixed message for its category.
393
+ const shaped = shapeRouteError("FastifyAdapter", requestId, error);
394
+ this.emitRequestError({
395
+ requestId,
396
+ error: error instanceof Error ? error : new Error(String(error)),
397
+ timestamp: new Date(),
398
+ });
399
+ this.emit("response", {
400
+ requestId,
401
+ statusCode: shaped.status,
402
+ duration: Date.now() - startTime,
403
+ timestamp: new Date(),
404
+ });
405
+ if (reply.sent || reply.raw.headersSent) {
406
+ return;
319
407
  }
408
+ reply.status(shaped.status);
409
+ return {
410
+ error: { code: shaped.code, message: shaped.message },
411
+ metadata: { requestId, timestamp: new Date().toISOString() },
412
+ };
413
+ }
414
+ finally {
415
+ abort.release();
320
416
  }
321
- // Return formatted response
322
- return {
323
- data: result,
324
- metadata: {
325
- requestId,
326
- timestamp: new Date().toISOString(),
327
- duration,
328
- },
329
- };
330
417
  },
331
418
  });
332
419
  }
@@ -363,9 +450,13 @@ export class FastifyServerAdapter extends BaseServerAdapter {
363
450
  reply.raw.end();
364
451
  }
365
452
  catch (error) {
453
+ const shaped = shapeRouteError("FastifyAdapter", ctx.requestId, error);
366
454
  reply.raw.write(`event: error\n`);
367
455
  reply.raw.write(`data: ${JSON.stringify({
368
- error: error instanceof Error ? error.message : "Stream error",
456
+ error: shaped.message,
457
+ code: shaped.code,
458
+ status: shaped.status,
459
+ requestId: ctx.requestId,
369
460
  })}\n\n`);
370
461
  reply.raw.end();
371
462
  }
@@ -378,14 +469,16 @@ export class FastifyServerAdapter extends BaseServerAdapter {
378
469
  throw new Error("Fastify app not initialized. Call initialize() before registering middleware.");
379
470
  }
380
471
  this.app.addHook("preHandler", async (request, _reply) => {
472
+ // Matched on the path alone, at segment boundaries: `request.url`
473
+ // carries the query string, so `/api/health?x` used to miss an
474
+ // exemption for `/api/health` and `/api/healthz` used to take it.
475
+ const requestPath = stripQuery(request.url);
381
476
  // Skip excluded paths
382
- if (middleware.excludePaths?.some((p) => request.url.startsWith(p))) {
477
+ if (pathMatchesAny(requestPath, middleware.excludePaths)) {
383
478
  return;
384
479
  }
385
480
  // Check if path matches
386
- const paths = middleware.paths || ["/"];
387
- const matches = paths.some((p) => request.url.startsWith(p) || p === "*");
388
- if (!matches) {
481
+ if (!pathMatchesAny(requestPath, middleware.paths || ["/"])) {
389
482
  return;
390
483
  }
391
484
  // Initialize response headers storage if not present
@@ -397,11 +490,12 @@ export class FastifyServerAdapter extends BaseServerAdapter {
397
490
  const ctx = this.createContext({
398
491
  requestId: request.id,
399
492
  method: request.method,
400
- path: request.url.split("?")[0],
493
+ path: requestPath,
401
494
  headers: request.headers,
402
495
  query: request.query,
403
496
  params: request.params,
404
497
  body: request.body,
498
+ remoteAddress: request.socket?.remoteAddress ?? request.ip,
405
499
  });
406
500
  // Copy existing response headers to context
407
501
  ctx.responseHeaders = { ...reqWithHeaders.responseHeaders };
@@ -438,7 +532,7 @@ export class FastifyServerAdapter extends BaseServerAdapter {
438
532
  const app = this.app;
439
533
  this.lifecycleState = "starting";
440
534
  const { port, host } = this.config;
441
- const startupTimeout = this.config.timeout || 30000;
535
+ const startupTimeout = STARTUP_TIMEOUT_MS;
442
536
  // Track connections via Fastify hooks (must be registered before listen)
443
537
  app.addHook("onRequest", async (request) => {
444
538
  const connectionId = `conn-${this.generateRequestId()}`;
@@ -457,6 +551,7 @@ export class FastifyServerAdapter extends BaseServerAdapter {
457
551
  this.startTime = new Date();
458
552
  this.lifecycleState = "running";
459
553
  logger.info(`[FastifyAdapter] Server started on ${host}:${port}`);
554
+ this.warnIfSensitiveRoutesUnauthenticated();
460
555
  this.emit("started", {
461
556
  port,
462
557
  host,
@@ -17,9 +17,10 @@ export declare class HonoServerAdapter extends BaseServerAdapter {
17
17
  private rateLimitCleanupInterval?;
18
18
  private requestContextStore;
19
19
  private requestIds;
20
- private HTTPExceptionCtor;
21
20
  private streamSSEFn;
22
21
  private parseFormBody;
22
+ private getConnInfoFn?;
23
+ private warnedNoClientAddress;
23
24
  constructor(neurolink: NeuroLink, config?: ServerAdapterConfig);
24
25
  /**
25
26
  * Create rate limiter middleware for Hono
@@ -84,6 +85,13 @@ export declare class HonoServerAdapter extends BaseServerAdapter {
84
85
  getFrameworkInstance(): unknown;
85
86
  getNativeServer(): unknown;
86
87
  private extractHeaders;
88
+ /**
89
+ * The runtime's connection-info helper: `@hono/node-server`'s on Node, the
90
+ * Bun or Deno adapter's there. Absent on a runtime that offers none.
91
+ */
92
+ private loadConnInfo;
93
+ /** The TCP peer of this request, when the runtime reports one. */
94
+ private remoteAddressFor;
87
95
  private extractQuery;
88
96
  /**
89
97
  * A request's context, or the 413 that answers a JSON or form body over its