@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
@@ -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
  /**
@@ -6,6 +6,9 @@
6
6
  import { logger } from "../../utils/logger.js";
7
7
  import { AlreadyRunningError, ServerAdapterError, ServerStopError, wrapError, } from "../errors.js";
8
8
  import { BaseServerAdapter } from "../abstract/baseServerAdapter.js";
9
+ import { resolveClientAddress } from "../utils/clientAddress.js";
10
+ import { shapeRouteError } from "../utils/clientError.js";
11
+ import { pathMatchesAny } from "../utils/pathMatch.js";
9
12
  import { isErrorResponse } from "../utils/validation.js";
10
13
  import { isRawBodyContentType, isWebResponse, parseByteLimit, RawBodyTooLargeError, readRawBody, readWebResponse, } from "../utils/rawBody.js";
11
14
  /**
@@ -20,6 +23,8 @@ export class KoaServerAdapter extends BaseServerAdapter {
20
23
  rateLimitStore = new Map();
21
24
  rateLimitCleanupInterval;
22
25
  sockets = new Set();
26
+ // The rate limiter counts nothing it cannot attribute, and says so once.
27
+ warnedNoClientAddress = false;
23
28
  // Middleware from registerMiddleware(). It runs through one dispatcher that
24
29
  // is mounted ahead of the router: app.use() after the router is mounted
25
30
  // would only ever see requests no route matched.
@@ -72,12 +77,15 @@ export class KoaServerAdapter extends BaseServerAdapter {
72
77
  });
73
78
  const adapterError = error instanceof ServerAdapterError ? error : undefined;
74
79
  const statusCode = adapterError?.getHttpStatus() || err.status || err.statusCode || 500;
80
+ // A 4xx carries a message this server or Koa composed; any 5xx's
81
+ // text stays in the log.
82
+ const serverSide = statusCode >= 500;
75
83
  ctx.status = statusCode;
76
84
  ctx.body = {
77
85
  error: {
78
86
  code: adapterError?.code ??
79
- (statusCode === 500 ? "INTERNAL_ERROR" : `HTTP_${statusCode}`),
80
- message: statusCode === 500 ? "An internal error occurred" : err.message,
87
+ (serverSide ? "INTERNAL_ERROR" : `HTTP_${statusCode}`),
88
+ message: serverSide ? "An internal error occurred" : err.message,
81
89
  },
82
90
  metadata: {
83
91
  requestId,
@@ -92,10 +100,13 @@ export class KoaServerAdapter extends BaseServerAdapter {
92
100
  const cors = corsModule.default;
93
101
  this.app.use(cors({
94
102
  origin: (ctx) => {
95
- const origin = ctx.request.headers.origin || "*";
103
+ // The wildcard is sent as the literal "*": reflecting the
104
+ // request's Origin instead would grant any page a credentialed
105
+ // allowance the wildcard itself cannot carry.
96
106
  if (this.config.cors.origins.includes("*")) {
97
- return origin;
107
+ return "*";
98
108
  }
109
+ const origin = ctx.request.headers.origin ?? "";
99
110
  return this.config.cors.origins.includes(origin) ? origin : "";
100
111
  },
101
112
  allowMethods: this.config.cors.methods?.join(","),
@@ -165,18 +176,38 @@ export class KoaServerAdapter extends BaseServerAdapter {
165
176
  createRateLimiter() {
166
177
  return async (ctx, next) => {
167
178
  // Skip rate limiting for excluded paths
168
- if (this.config.rateLimit.skipPaths) {
169
- for (const skipPath of this.config.rateLimit.skipPaths) {
170
- if (ctx.path.startsWith(skipPath)) {
171
- await next();
172
- return;
173
- }
179
+ if (pathMatchesAny(ctx.path, this.config.rateLimit.skipPaths)) {
180
+ await next();
181
+ return;
182
+ }
183
+ // The TCP peer, or behind a declared proxy the client it reports. A
184
+ // header alone used to be the key, so a rotating X-Forwarded-For escaped
185
+ // the limit; a request with no attributable address is let through
186
+ // rather than pooled with every other such request.
187
+ const key = this.config.rateLimit.keyGenerator
188
+ ? this.config.rateLimit.keyGenerator(this.createContext({
189
+ requestId: ctx.state.requestId,
190
+ method: ctx.method,
191
+ path: ctx.path,
192
+ headers: ctx.headers,
193
+ query: ctx.query,
194
+ params: {},
195
+ remoteAddress: ctx.req.socket?.remoteAddress,
196
+ }))
197
+ : resolveClientAddress({
198
+ remoteAddress: ctx.req.socket?.remoteAddress,
199
+ forwardedFor: ctx.get("x-forwarded-for") || undefined,
200
+ trustProxy: this.config.rateLimit.trustProxy,
201
+ trustedProxies: this.config.rateLimit.trustedProxies,
202
+ });
203
+ if (key === undefined) {
204
+ if (!this.warnedNoClientAddress) {
205
+ this.warnedNoClientAddress = true;
206
+ logger.alwaysStderr("[KoaAdapter] A request arrived with no peer address, so the rate limiter cannot count it; configure rateLimit.keyGenerator to limit by another key.");
174
207
  }
208
+ await next();
209
+ return;
175
210
  }
176
- const key = ctx.get("x-forwarded-for") ||
177
- ctx.get("x-real-ip") ||
178
- ctx.ip ||
179
- "unknown";
180
211
  const now = Date.now();
181
212
  const windowMs = this.config.rateLimit.windowMs;
182
213
  const maxRequests = this.config.rateLimit.maxRequests;
@@ -256,12 +287,24 @@ export class KoaServerAdapter extends BaseServerAdapter {
256
287
  query: ctx.query,
257
288
  params: ctx.params,
258
289
  body: ctx.request.body,
290
+ remoteAddress: ctx.req.socket?.remoteAddress,
259
291
  });
260
292
  // Copy response headers from middleware (stored in ctx.state by middleware)
261
293
  if (ctx.state.responseHeaders) {
262
294
  serverCtx.responseHeaders = { ...ctx.state.responseHeaders };
263
295
  }
264
296
  this.restoreCarriedContext(serverCtx, ctx.state.carriedContext);
297
+ // The handler learns of a client that went away, and a request over
298
+ // `config.timeout` is answered 504 and its work aborted. Koa applied no
299
+ // request timeout at all.
300
+ const abort = this.attachRequestAbort(serverCtx, {
301
+ onClientClose: (listener) => ctx.req.on("close", () => {
302
+ if (!ctx.res.writableFinished) {
303
+ listener();
304
+ }
305
+ }),
306
+ applyTimeout: !route.streaming?.enabled,
307
+ });
265
308
  // Emit request event
266
309
  this.emit("request", {
267
310
  requestId,
@@ -269,39 +312,68 @@ export class KoaServerAdapter extends BaseServerAdapter {
269
312
  path: serverCtx.path,
270
313
  timestamp: new Date(),
271
314
  });
272
- // Handle streaming if configured
273
- if (route.streaming?.enabled) {
274
- return this.handleStreamingResponse(ctx, serverCtx, route);
275
- }
276
- // Execute handler
277
- const result = await route.handler(serverCtx);
278
- const duration = Date.now() - startTime;
279
- // A route that answers in its own wire format returns a web Response.
280
- if (isWebResponse(result)) {
281
- const raw = await readWebResponse(result);
282
- this.emit("response", {
283
- requestId,
284
- statusCode: raw.status,
285
- duration,
286
- timestamp: new Date(),
287
- });
288
- for (const [key, value] of Object.entries({
289
- ...serverCtx.responseHeaders,
290
- ...raw.headers,
291
- })) {
292
- ctx.set(key, value);
315
+ try {
316
+ // Handle streaming if configured
317
+ if (route.streaming?.enabled) {
318
+ return await this.handleStreamingResponse(ctx, serverCtx, route);
293
319
  }
294
- ctx.status = raw.status;
295
- ctx.body = raw.body;
296
- return;
297
- }
298
- // Check if result is an error response
299
- if (isErrorResponse(result)) {
300
- const statusCode = result.httpStatus ?? 500;
301
- // Emit response event with error status
320
+ // Execute handler
321
+ const result = abort.deadline
322
+ ? await Promise.race([route.handler(serverCtx), abort.deadline])
323
+ : await route.handler(serverCtx);
324
+ const duration = Date.now() - startTime;
325
+ // A route that answers in its own wire format returns a web Response.
326
+ if (isWebResponse(result)) {
327
+ const raw = await readWebResponse(result);
328
+ this.emit("response", {
329
+ requestId,
330
+ statusCode: raw.status,
331
+ duration,
332
+ timestamp: new Date(),
333
+ });
334
+ for (const [key, value] of Object.entries({
335
+ ...serverCtx.responseHeaders,
336
+ ...raw.headers,
337
+ })) {
338
+ ctx.set(key, value);
339
+ }
340
+ ctx.status = raw.status;
341
+ ctx.body = raw.body;
342
+ return;
343
+ }
344
+ // Check if result is an error response
345
+ if (isErrorResponse(result)) {
346
+ const statusCode = result.httpStatus ?? 500;
347
+ // Emit response event with error status
348
+ this.emit("response", {
349
+ requestId,
350
+ statusCode,
351
+ duration,
352
+ timestamp: new Date(),
353
+ });
354
+ // Apply response headers from middleware and handler
355
+ if (serverCtx.responseHeaders) {
356
+ for (const [key, value] of Object.entries(serverCtx.responseHeaders)) {
357
+ ctx.set(key, value);
358
+ }
359
+ }
360
+ // Return error response with proper status code
361
+ ctx.status = statusCode;
362
+ ctx.body = {
363
+ error: result.error,
364
+ metadata: {
365
+ ...result.metadata,
366
+ requestId,
367
+ timestamp: new Date().toISOString(),
368
+ duration,
369
+ },
370
+ };
371
+ return;
372
+ }
373
+ // Emit response event
302
374
  this.emit("response", {
303
375
  requestId,
304
- statusCode,
376
+ statusCode: 200,
305
377
  duration,
306
378
  timestamp: new Date(),
307
379
  });
@@ -311,41 +383,44 @@ export class KoaServerAdapter extends BaseServerAdapter {
311
383
  ctx.set(key, value);
312
384
  }
313
385
  }
314
- // Return error response with proper status code
315
- ctx.status = statusCode;
386
+ // Return formatted response
316
387
  ctx.body = {
317
- error: result.error,
388
+ data: result,
318
389
  metadata: {
319
- ...result.metadata,
320
390
  requestId,
321
391
  timestamp: new Date().toISOString(),
322
392
  duration,
323
393
  },
324
394
  };
325
- return;
326
395
  }
327
- // Emit response event
328
- this.emit("response", {
329
- requestId,
330
- statusCode: 200,
331
- duration,
332
- timestamp: new Date(),
333
- });
334
- // Apply response headers from middleware and handler
335
- if (serverCtx.responseHeaders) {
336
- for (const [key, value] of Object.entries(serverCtx.responseHeaders)) {
337
- ctx.set(key, value);
396
+ catch (error) {
397
+ // The exception's text stays in the log under this request id; the
398
+ // client gets a fixed message for its category.
399
+ const shaped = shapeRouteError("KoaAdapter", requestId, error);
400
+ this.emitRequestError({
401
+ requestId,
402
+ error: error instanceof Error ? error : new Error(String(error)),
403
+ timestamp: new Date(),
404
+ });
405
+ this.emit("response", {
406
+ requestId,
407
+ statusCode: shaped.status,
408
+ duration: Date.now() - startTime,
409
+ timestamp: new Date(),
410
+ });
411
+ if (ctx.res.headersSent) {
412
+ ctx.res.end();
413
+ return;
338
414
  }
415
+ ctx.status = shaped.status;
416
+ ctx.body = {
417
+ error: { code: shaped.code, message: shaped.message },
418
+ metadata: { requestId, timestamp: new Date().toISOString() },
419
+ };
420
+ }
421
+ finally {
422
+ abort.release();
339
423
  }
340
- // Return formatted response
341
- ctx.body = {
342
- data: result,
343
- metadata: {
344
- requestId,
345
- timestamp: new Date().toISOString(),
346
- duration,
347
- },
348
- };
349
424
  });
350
425
  }
351
426
  /**
@@ -382,9 +457,13 @@ export class KoaServerAdapter extends BaseServerAdapter {
382
457
  stream.end();
383
458
  }
384
459
  catch (error) {
460
+ const shaped = shapeRouteError("KoaAdapter", serverCtx.requestId, error);
385
461
  stream.write(`event: error\n`);
386
462
  stream.write(`data: ${JSON.stringify({
387
- error: error instanceof Error ? error.message : "Stream error",
463
+ error: shaped.message,
464
+ code: shaped.code,
465
+ status: shaped.status,
466
+ requestId: serverCtx.requestId,
388
467
  })}\n\n`);
389
468
  stream.end();
390
469
  }
@@ -395,13 +474,11 @@ export class KoaServerAdapter extends BaseServerAdapter {
395
474
  registerFrameworkMiddleware(middleware) {
396
475
  this.registeredMiddleware.push(async (ctx, next) => {
397
476
  // Skip excluded paths
398
- if (middleware.excludePaths?.some((p) => ctx.path.startsWith(p))) {
477
+ if (pathMatchesAny(ctx.path, middleware.excludePaths)) {
399
478
  return next();
400
479
  }
401
480
  // Check if path matches
402
- const paths = middleware.paths || ["/"];
403
- const matches = paths.some((p) => ctx.path.startsWith(p) || p === "*");
404
- if (!matches) {
481
+ if (!pathMatchesAny(ctx.path, middleware.paths || ["/"])) {
405
482
  return next();
406
483
  }
407
484
  // Initialize response headers storage in ctx.state if not present
@@ -417,6 +494,7 @@ export class KoaServerAdapter extends BaseServerAdapter {
417
494
  query: ctx.query,
418
495
  params: ctx.params,
419
496
  body: ctx.request.body,
497
+ remoteAddress: ctx.req.socket?.remoteAddress,
420
498
  });
421
499
  // Copy existing response headers to context
422
500
  serverCtx.responseHeaders = { ...ctx.state.responseHeaders };
@@ -470,6 +548,7 @@ export class KoaServerAdapter extends BaseServerAdapter {
470
548
  this.isRunning = true;
471
549
  this.startTime = new Date();
472
550
  this.lifecycleState = "running";
551
+ this.warnIfSensitiveRoutesUnauthenticated();
473
552
  // Track connections for graceful shutdown
474
553
  this.server?.on("connection", (socket) => {
475
554
  const connectionId = `conn-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
@@ -42,6 +42,8 @@ export { createAgentRoutes, createAllRoutes, createHealthRoutes, createMCPRoutes
42
42
  export { BaseDataStreamWriter, createDataStreamResponse, createDataStreamWriter, createNDJSONHeaders, createSSEHeaders, DataStreamResponse, formatSSEEvent, pipeAsyncIterableToDataStream, WebStreamWriter, } from "./streaming/index.js";
43
43
  export { ErrorCategory, ErrorSeverity, ServerAdapterErrorCode, } from "../types/index.js";
44
44
  export { createStreamRedactor, redactStreamChunk } from "./utils/redaction.js";
45
+ export { isLoopbackAddress, resolveClientAddress, } from "./utils/clientAddress.js";
46
+ export { clientFacingError } from "./utils/clientError.js";
45
47
  export { AgentExecuteRequestSchema, createErrorResponse, ServerNameParamSchema, SessionIdParamSchema, ToolArgumentsSchema, ToolExecuteRequestSchema, ToolNameParamSchema, ToolSearchQuerySchema, TranscribeConfigSchema, TranscribeRequestSchema, TranscribeStreamConfigSchema, validateParams, validateQuery, validateRequest, } from "./utils/validation.js";
46
48
  export { createAgentWebSocketHandler, WebSocketConnectionManager, WebSocketMessageRouter, } from "./websocket/index.js";
47
49
  export { attachTranscribeWebSocket } from "./websocket/transcribeWebSocket.js";
@@ -117,6 +117,10 @@ pipeAsyncIterableToDataStream, WebStreamWriter, } from "./streaming/index.js";
117
117
  // Export error constants
118
118
  export { ErrorCategory, ErrorSeverity, ServerAdapterErrorCode, } from "../types/index.js";
119
119
  export { createStreamRedactor, redactStreamChunk } from "./utils/redaction.js";
120
+ // The address a request is counted against (for a custom rate-limit
121
+ // keyGenerator behind a reverse proxy) and the loopback test the adapters use.
122
+ export { isLoopbackAddress, resolveClientAddress, } from "./utils/clientAddress.js";
123
+ export { clientFacingError } from "./utils/clientError.js";
120
124
  // ============================================
121
125
  // Validation Utilities
122
126
  // ============================================
@@ -65,6 +65,7 @@ export function createAuthMiddleware(config) {
65
65
  name: "authentication",
66
66
  order: 10, // Run early but after request ID
67
67
  excludePaths: skipPaths,
68
+ authenticates: true,
68
69
  handler: async (ctx, next) => {
69
70
  // The playground header is a development convenience, so it is honoured
70
71
  // only when NODE_ENV is exactly "development". An unset NODE_ENV (the
@@ -286,12 +287,15 @@ export function createBearerAuthMiddleware(validate, options = {}) {
286
287
  name: "bearer-auth",
287
288
  order: 10,
288
289
  excludePaths: skipPaths,
290
+ authenticates: true,
289
291
  handler: async (ctx, next) => {
290
292
  const authHeader = ctx.headers[headerName.toLowerCase()];
291
- // Extract bearer token
293
+ // Extract bearer token. The token must open with a non-whitespace
294
+ // character, as in extractToken(): with `\s+(.+)` a header that failed
295
+ // to match was retried at every split, quadratic in its length.
292
296
  let token = null;
293
297
  if (authHeader) {
294
- const match = authHeader.match(/^Bearer\s+(.+)$/i);
298
+ const match = authHeader.match(/^Bearer\s+(\S.*)$/i);
295
299
  token = match ? match[1] : null;
296
300
  }
297
301
  // Handle missing token
@@ -136,10 +136,15 @@ export function createCacheMiddleware(config) {
136
136
  };
137
137
  }
138
138
  /**
139
- * Default cache key generator
139
+ * Default cache key generator. An authenticated user's id is part of the key,
140
+ * so one caller's cached response is never served to another; without a user
141
+ * the key is the request alone, as before.
140
142
  */
141
143
  function defaultKeyGenerator(ctx) {
142
144
  const parts = [ctx.method, ctx.path];
145
+ if (ctx.user?.id) {
146
+ parts.push(`user=${ctx.user.id}`);
147
+ }
143
148
  // Include sorted query params
144
149
  const queryKeys = Object.keys(ctx.query).sort();
145
150
  if (queryKeys.length > 0) {