@juspay/neurolink 13.8.16 → 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 (194) 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 +83 -8
  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/expressBody.d.ts +33 -0
  120. package/dist/server/utils/expressBody.js +353 -0
  121. package/dist/server/utils/pathMatch.d.ts +21 -0
  122. package/dist/server/utils/pathMatch.js +41 -0
  123. package/dist/server/utils/validation.d.ts +9 -0
  124. package/dist/server/utils/validation.js +9 -0
  125. package/dist/server/websocket/WebSocketHandler.js +22 -3
  126. package/dist/server/websocket/frameQueue.d.ts +19 -0
  127. package/dist/server/websocket/frameQueue.js +41 -3
  128. package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
  129. package/dist/server/websocket/transcribeWebSocket.js +223 -34
  130. package/dist/skills/skillStores.js +5 -0
  131. package/dist/tasks/store/fileTaskStore.d.ts +2 -0
  132. package/dist/tasks/store/fileTaskStore.js +11 -4
  133. package/dist/types/attachments.d.ts +117 -0
  134. package/dist/types/attachments.js +15 -0
  135. package/dist/types/auth.d.ts +6 -0
  136. package/dist/types/cli.d.ts +4 -0
  137. package/dist/types/config.d.ts +56 -0
  138. package/dist/types/file.d.ts +8 -0
  139. package/dist/types/hitl.d.ts +8 -0
  140. package/dist/types/index.d.ts +1 -0
  141. package/dist/types/index.js +1 -0
  142. package/dist/types/mcp.d.ts +98 -0
  143. package/dist/types/multimodal.js +0 -41
  144. package/dist/types/processor.d.ts +14 -1
  145. package/dist/types/proxy.d.ts +62 -3
  146. package/dist/types/rag.d.ts +9 -0
  147. package/dist/types/safeFetch.d.ts +40 -0
  148. package/dist/types/server.d.ts +255 -10
  149. package/dist/types/tools.d.ts +57 -0
  150. package/dist/types/transcribe.d.ts +6 -0
  151. package/dist/utils/attachmentPolicy.d.ts +89 -0
  152. package/dist/utils/attachmentPolicy.js +327 -0
  153. package/dist/utils/dotenvBootstrap.d.ts +48 -19
  154. package/dist/utils/dotenvBootstrap.js +172 -31
  155. package/dist/utils/fileDetector.js +115 -65
  156. package/dist/utils/json/coerce.js +11 -1
  157. package/dist/utils/logSanitize.d.ts +5 -0
  158. package/dist/utils/logSanitize.js +8 -0
  159. package/dist/utils/logger.js +73 -1
  160. package/dist/utils/messageBuilder.d.ts +2 -2
  161. package/dist/utils/messageBuilder.js +118 -105
  162. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
  163. package/dist/utils/multimodalOptionsBuilder.js +2 -0
  164. package/dist/utils/redis.d.ts +12 -0
  165. package/dist/utils/redis.js +72 -0
  166. package/dist/utils/safeFetch.d.ts +22 -1
  167. package/dist/utils/safeFetch.js +132 -38
  168. package/dist/utils/safeStoreId.d.ts +19 -0
  169. package/dist/utils/safeStoreId.js +29 -0
  170. package/dist/utils/schemaConversion.d.ts +30 -3
  171. package/dist/utils/schemaConversion.js +457 -84
  172. package/dist/utils/sizeGuard.js +2 -2
  173. package/dist/utils/ssrfGuard.d.ts +3 -3
  174. package/dist/utils/ssrfGuard.js +71 -39
  175. package/dist/voice/eval/sttEvalStore.d.ts +26 -3
  176. package/dist/voice/eval/sttEvalStore.js +77 -12
  177. package/dist/voice/livekit/agentDefinition.d.ts +18 -0
  178. package/dist/voice/livekit/agentDefinition.js +22 -0
  179. package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
  180. package/dist/voice/livekit/realtimeMcpTools.js +8 -7
  181. package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
  182. package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
  183. package/dist/voice/livekit/voiceAgent.d.ts +2 -2
  184. package/dist/voice/livekit/voiceAgent.js +2 -1
  185. package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
  186. package/dist/voice/livekit/xaiSessionWatch.js +30 -0
  187. package/dist/voice/providers/GeminiLive.js +21 -6
  188. package/dist/voice/providers/GoogleSTT.js +8 -5
  189. package/dist/voice/transcribe.d.ts +2 -2
  190. package/dist/voice/transcribe.js +34 -18
  191. package/dist/voice/whistle/audio.js +11 -5
  192. package/docs-site/static/search-index.json +75 -52
  193. package/package.json +171 -156
  194. package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
@@ -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
@@ -7,6 +7,9 @@ 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
9
  import { markContextCarried } from "../utils/carriedContextMark.js";
10
+ import { resolveClientAddress } from "../utils/clientAddress.js";
11
+ import { shapeRouteError } from "../utils/clientError.js";
12
+ import { pathMatchesAny } from "../utils/pathMatch.js";
10
13
  import { isErrorResponse } from "../utils/validation.js";
11
14
  import { isRawBodyContentType, isWebResponse, parseByteLimit, RawBodyTooLargeError, readBoundedRequestBody, } from "../utils/rawBody.js";
12
15
  /**
@@ -32,9 +35,13 @@ export class HonoServerAdapter extends BaseServerAdapter {
32
35
  // Populated by initializeFramework's dynamic imports; used by methods that
33
36
  // run after initialization (route registration, streaming) and so cannot
34
37
  // rely on initializeFramework's own local closure variables.
35
- HTTPExceptionCtor;
36
38
  streamSSEFn;
37
39
  parseFormBody;
40
+ // The runtime's view of the TCP peer (Node, Bun or Deno); undefined where the
41
+ // runtime offers none, and then requests carry no address.
42
+ getConnInfoFn;
43
+ // The rate limiter counts nothing it cannot attribute, and says so once.
44
+ warnedNoClientAddress = false;
38
45
  constructor(neurolink, config = {}) {
39
46
  super(neurolink, config);
40
47
  }
@@ -44,12 +51,8 @@ export class HonoServerAdapter extends BaseServerAdapter {
44
51
  createRateLimiter() {
45
52
  return async (c, next) => {
46
53
  // Skip rate limiting for excluded paths
47
- if (this.config.rateLimit.skipPaths) {
48
- for (const skipPath of this.config.rateLimit.skipPaths) {
49
- if (c.req.path.startsWith(skipPath)) {
50
- return next();
51
- }
52
- }
54
+ if (pathMatchesAny(c.req.path, this.config.rateLimit.skipPaths)) {
55
+ return next();
53
56
  }
54
57
  // Use custom keyGenerator if provided, otherwise default to IP-based key
55
58
  let key;
@@ -71,14 +74,29 @@ export class HonoServerAdapter extends BaseServerAdapter {
71
74
  externalServerManager: this.externalServerManager,
72
75
  timestamp: Date.now(),
73
76
  metadata: {},
77
+ remoteAddress: this.remoteAddressFor(c),
74
78
  };
75
79
  key = this.config.rateLimit.keyGenerator(minCtx);
76
80
  }
77
81
  else {
78
- key =
79
- c.req.header("x-forwarded-for") ||
80
- c.req.header("x-real-ip") ||
81
- "unknown";
82
+ // The TCP peer, or behind a declared proxy the client it reports. A
83
+ // header alone used to be the key, so one shared "unknown" bucket
84
+ // served every direct client and a rotating X-Forwarded-For escaped
85
+ // the limit; a request with no attributable address is now let
86
+ // through rather than pooled with every other such request.
87
+ key = resolveClientAddress({
88
+ remoteAddress: this.remoteAddressFor(c),
89
+ forwardedFor: c.req.header("x-forwarded-for"),
90
+ trustProxy: this.config.rateLimit.trustProxy,
91
+ trustedProxies: this.config.rateLimit.trustedProxies,
92
+ });
93
+ if (key === undefined) {
94
+ if (!this.warnedNoClientAddress) {
95
+ this.warnedNoClientAddress = true;
96
+ logger.alwaysStderr("[HonoAdapter] The runtime reports no peer address for requests, so the rate limiter cannot count them; configure rateLimit.keyGenerator to limit by another key.");
97
+ }
98
+ return next();
99
+ }
82
100
  }
83
101
  const now = Date.now();
84
102
  const windowMs = this.config.rateLimit.windowMs;
@@ -137,9 +155,9 @@ export class HonoServerAdapter extends BaseServerAdapter {
137
155
  this.importFrameworkDependency("hono/timeout", "hono"),
138
156
  this.importFrameworkDependency("hono/utils/body", "hono"),
139
157
  ]);
140
- this.HTTPExceptionCtor = HTTPException;
141
158
  this.streamSSEFn = streamSSE;
142
159
  this.parseFormBody = parseBody;
160
+ this.getConnInfoFn = await this.loadConnInfo();
143
161
  this.app = new Hono();
144
162
  // Add secure headers
145
163
  this.app.use("*", secureHeaders());
@@ -235,6 +253,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
235
253
  const startTime = Date.now();
236
254
  // Track connection for graceful shutdown
237
255
  this.trackConnection(connectionId, undefined, requestId);
256
+ let abort;
238
257
  try {
239
258
  // Extract path parameters
240
259
  const params = c.req.param();
@@ -263,6 +282,15 @@ export class HonoServerAdapter extends BaseServerAdapter {
263
282
  }
264
283
  ctx = created;
265
284
  }
285
+ // The handler learns of a client that went away through the request's
286
+ // own signal, and of the request timeout through the same controller
287
+ // (Hono's timeout middleware answers 504; this stops the work).
288
+ abort = this.attachRequestAbort(ctx, {
289
+ onClientClose: (listener) => c.req.raw.signal.addEventListener("abort", listener, {
290
+ once: true,
291
+ }),
292
+ applyTimeout: !route.streaming?.enabled,
293
+ });
266
294
  // Emit request event
267
295
  this.emit("request", {
268
296
  requestId,
@@ -344,15 +372,23 @@ export class HonoServerAdapter extends BaseServerAdapter {
344
372
  });
345
373
  }
346
374
  catch (error) {
347
- const errorMessage = error instanceof Error ? error.message : String(error);
348
- logger.error("[HonoAdapter] Handler error", {
375
+ // The exception's text stays in the log under this request id; the
376
+ // client gets a fixed message for its category. It used to get the
377
+ // text itself, environment variable names and upstream bodies included.
378
+ const shaped = shapeRouteError("HonoAdapter", requestId, error);
379
+ this.emit("response", {
349
380
  requestId,
350
- route: route.path,
351
- error: errorMessage,
381
+ statusCode: shaped.status,
382
+ duration: Date.now() - startTime,
383
+ timestamp: new Date(),
352
384
  });
353
- throw new this.HTTPExceptionCtor(500, { message: errorMessage });
385
+ return c.json({
386
+ error: { code: shaped.code, message: shaped.message },
387
+ metadata: { requestId, timestamp: new Date().toISOString() },
388
+ }, shaped.status);
354
389
  }
355
390
  finally {
391
+ abort?.release();
356
392
  // Untrack connection when request completes
357
393
  this.untrackConnection(connectionId);
358
394
  // Clean up context store to prevent memory leaks
@@ -417,13 +453,14 @@ export class HonoServerAdapter extends BaseServerAdapter {
417
453
  });
418
454
  }
419
455
  catch (error) {
456
+ const shaped = shapeRouteError("HonoAdapter", ctx.requestId, error);
420
457
  await stream.writeSSE({
421
458
  data: JSON.stringify({
422
459
  type: "error",
423
460
  error: {
424
- code: "STREAM_ERROR",
425
- message: error instanceof Error ? error.message : "Stream error",
426
- status: 500,
461
+ code: shaped.code,
462
+ message: shaped.message,
463
+ status: shaped.status,
427
464
  },
428
465
  timestamp: Date.now(),
429
466
  }),
@@ -482,7 +519,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
482
519
  for (const path of paths) {
483
520
  this.app.use(path, async (c, next) => {
484
521
  // Skip excluded paths
485
- if (middleware.excludePaths?.some((p) => c.req.path.startsWith(p))) {
522
+ if (pathMatchesAny(c.req.path, middleware.excludePaths)) {
486
523
  return next();
487
524
  }
488
525
  // Extract path parameters
@@ -566,6 +603,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
566
603
  this.startTime = new Date();
567
604
  this.lifecycleState = "running";
568
605
  logger.info(`[HonoAdapter] Server started on ${host}:${port}`);
606
+ this.warnIfSensitiveRoutesUnauthenticated();
569
607
  this.emit("started", {
570
608
  port,
571
609
  host,
@@ -684,6 +722,45 @@ export class HonoServerAdapter extends BaseServerAdapter {
684
722
  });
685
723
  return headers;
686
724
  }
725
+ /**
726
+ * The runtime's connection-info helper: `@hono/node-server`'s on Node, the
727
+ * Bun or Deno adapter's there. Absent on a runtime that offers none.
728
+ */
729
+ async loadConnInfo() {
730
+ try {
731
+ if (typeof Bun !== "undefined") {
732
+ const bun = await import("hono/bun");
733
+ return bun.getConnInfo;
734
+ }
735
+ if (typeof Deno !== "undefined") {
736
+ const deno = await import("hono/deno");
737
+ return deno.getConnInfo;
738
+ }
739
+ const node = await import("@hono/node-server/conninfo");
740
+ return node.getConnInfo;
741
+ }
742
+ catch (error) {
743
+ logger.debug("[HonoAdapter] No connection-info helper for this runtime", {
744
+ error: error instanceof Error ? error.message : String(error),
745
+ });
746
+ return undefined;
747
+ }
748
+ }
749
+ /** The TCP peer of this request, when the runtime reports one. */
750
+ remoteAddressFor(c) {
751
+ if (!this.getConnInfoFn) {
752
+ return undefined;
753
+ }
754
+ try {
755
+ const address = this.getConnInfoFn(c).remote.address;
756
+ return typeof address === "string" && address.length > 0
757
+ ? address
758
+ : undefined;
759
+ }
760
+ catch {
761
+ return undefined;
762
+ }
763
+ }
687
764
  extractQuery(c) {
688
765
  const query = {};
689
766
  const url = new URL(c.req.url);
@@ -726,6 +803,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
726
803
  query: this.extractQuery(c),
727
804
  params,
728
805
  body,
806
+ remoteAddress: this.remoteAddressFor(c),
729
807
  });
730
808
  }
731
809
  async extractBody(c) {
@@ -18,6 +18,7 @@ export declare class KoaServerAdapter extends BaseServerAdapter {
18
18
  private rateLimitStore;
19
19
  private rateLimitCleanupInterval?;
20
20
  private sockets;
21
+ private warnedNoClientAddress;
21
22
  private registeredMiddleware;
22
23
  constructor(neurolink: NeuroLink, config?: ServerAdapterConfig);
23
24
  /**