@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
@@ -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) {
@@ -2,6 +2,7 @@
2
2
  * Rate Limiting Middleware
3
3
  * Provides configurable rate limiting for server adapters
4
4
  */
5
+ import { logger } from "../../utils/logger.js";
5
6
  import { RateLimitError as ServerRateLimitError } from "../errors.js";
6
7
  /**
7
8
  * In-memory rate limit store
@@ -86,6 +87,9 @@ export function createRateLimitMiddleware(config) {
86
87
  excludePaths: skipPaths,
87
88
  handler: async (ctx, next) => {
88
89
  const key = keyGenerator(ctx);
90
+ if (!key) {
91
+ return next(); // Nothing to attribute the request to: see defaultKeyGenerator.
92
+ }
89
93
  const { count, resetAt } = await store.increment(key, windowMs);
90
94
  // Set rate limit headers in responseHeaders (adapters read from here)
91
95
  ctx.responseHeaders = ctx.responseHeaders || {};
@@ -107,12 +111,27 @@ export function createRateLimitMiddleware(config) {
107
111
  };
108
112
  }
109
113
  /**
110
- * Default key generator using IP address
114
+ * Default key: the TCP peer the adapter recorded on the context. Never a
115
+ * header, which the client writes: keyed on `X-Forwarded-For`, one shared
116
+ * bucket served every direct client and a rotating header escaped the limit.
117
+ * Behind a reverse proxy, pass a `keyGenerator` that reads the proxy's header
118
+ * (see `resolveClientAddress`). An empty key means "do not count".
111
119
  */
112
120
  function defaultKeyGenerator(ctx) {
113
- return (ctx.headers["x-forwarded-for"]?.split(",")[0].trim() ||
114
- ctx.headers["x-real-ip"] ||
115
- "unknown");
121
+ const address = ctx.remoteAddress?.trim();
122
+ if (!address) {
123
+ warnNoAddressOnce();
124
+ return "";
125
+ }
126
+ return address;
127
+ }
128
+ let warnedNoAddress = false;
129
+ function warnNoAddressOnce() {
130
+ if (warnedNoAddress) {
131
+ return;
132
+ }
133
+ warnedNoAddress = true;
134
+ logger.alwaysStderr("[rateLimit] A request carried no peer address (ctx.remoteAddress), so the rate limiter let it through uncounted; pass a keyGenerator to limit by another key.");
116
135
  }
117
136
  /**
118
137
  * Create a sliding window rate limiter
@@ -142,6 +161,9 @@ export function createSlidingWindowRateLimitMiddleware(config) {
142
161
  excludePaths: skipPaths,
143
162
  handler: async (ctx, next) => {
144
163
  const key = keyGenerator(ctx);
164
+ if (!key) {
165
+ return next();
166
+ }
145
167
  const now = Date.now();
146
168
  // Get existing timestamps and filter old ones
147
169
  let timestamps = store.get(key) || [];
@@ -201,6 +223,9 @@ export function createFixedWindowRateLimitMiddleware(config, store) {
201
223
  excludePaths: skipPaths,
202
224
  handler: async (ctx, next) => {
203
225
  const key = keyGenerator(ctx);
226
+ if (!key) {
227
+ return next();
228
+ }
204
229
  const { count, resetAt } = await rateLimitStore.increment(key, windowMs);
205
230
  // Set rate limit headers in responseHeaders (adapters read from here)
206
231
  ctx.responseHeaders = ctx.responseHeaders || {};
@@ -29,7 +29,13 @@ async function resolveRequestToolFilter(ctx, requestedTools) {
29
29
  return undefined;
30
30
  }
31
31
  import { AgentInputError, resolveAgentInput } from "../utils/agentInput.js";
32
+ import { shapeRouteError } from "../utils/clientError.js";
32
33
  import { createStreamRedactor } from "../utils/redaction.js";
34
+ /** The route's error response for an exception: a fixed message, the detail logged. */
35
+ function agentFailure(ctx, error, code = "EXECUTION_FAILED") {
36
+ const shaped = shapeRouteError("agentRoutes", ctx.requestId, error);
37
+ return createError(shaped.status === 500 ? code : shaped.code, shaped.message, undefined, ctx.requestId, shaped.status);
38
+ }
33
39
  import { AgentExecuteRequestSchema, createErrorResponse as createError, EmbedManyRequestSchema, EmbedRequestSchema, SkillCreateRequestSchema, SkillUpdateRequestSchema, validateRequest, } from "../utils/validation.js";
34
40
  /**
35
41
  * Resolve the skills manager from the server's NeuroLink instance, or a
@@ -244,6 +250,9 @@ export function createAgentRoutes(basePath = "/api", options) {
244
250
  return input;
245
251
  }
246
252
  const requestToolFilter = await resolveRequestToolFilter(ctx, request.tools);
253
+ // The adapter's signal: the client went away, or the request
254
+ // ran past the server's timeout. Either stops the generation
255
+ // instead of leaving it running for nobody.
247
256
  const result = await ctx.neurolink.generate({
248
257
  input,
249
258
  provider: request.provider,
@@ -252,6 +261,7 @@ export function createAgentRoutes(basePath = "/api", options) {
252
261
  temperature: request.temperature,
253
262
  maxTokens: request.maxTokens,
254
263
  ...(requestToolFilter ? { toolFilter: requestToolFilter } : {}),
264
+ ...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}),
255
265
  context: callerContext(ctx, request),
256
266
  });
257
267
  // Map tool calls from SDK format to API format
@@ -295,6 +305,7 @@ export function createAgentRoutes(basePath = "/api", options) {
295
305
  temperature: request.temperature,
296
306
  maxTokens: request.maxTokens,
297
307
  ...(streamToolFilter ? { toolFilter: streamToolFilter } : {}),
308
+ ...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}),
298
309
  context: callerContext(ctx, request),
299
310
  });
300
311
  // Create redactor (no-op if redaction is not enabled)
@@ -377,9 +388,7 @@ export function createAgentRoutes(basePath = "/api", options) {
377
388
  });
378
389
  }
379
390
  catch (error) {
380
- return createError("EXECUTION_FAILED", error instanceof Error
381
- ? error.message
382
- : "Embedding generation failed", undefined, ctx.requestId);
391
+ return agentFailure(ctx, error);
383
392
  }
384
393
  },
385
394
  description: "Generate embedding for a single text",
@@ -418,9 +427,7 @@ export function createAgentRoutes(basePath = "/api", options) {
418
427
  });
419
428
  }
420
429
  catch (error) {
421
- return createError("EXECUTION_FAILED", error instanceof Error
422
- ? error.message
423
- : "Batch embedding generation failed", undefined, ctx.requestId);
430
+ return agentFailure(ctx, error);
424
431
  }
425
432
  },
426
433
  description: "Generate embeddings for multiple texts in a batch",
@@ -439,7 +446,7 @@ export function createAgentRoutes(basePath = "/api", options) {
439
446
  return { skills, count: skills.length };
440
447
  }
441
448
  catch (error) {
442
- return createError("EXECUTION_FAILED", error instanceof Error ? error.message : "Skills listing failed", undefined, ctx.requestId);
449
+ return agentFailure(ctx, error);
443
450
  }
444
451
  },
445
452
  description: "List active skills (index only — no instructions). Optional ?scopeId= filter.",
@@ -461,7 +468,7 @@ export function createAgentRoutes(basePath = "/api", options) {
461
468
  return skill;
462
469
  }
463
470
  catch (error) {
464
- return createError("EXECUTION_FAILED", error instanceof Error ? error.message : "Skill lookup failed", undefined, ctx.requestId);
471
+ return agentFailure(ctx, error);
465
472
  }
466
473
  },
467
474
  description: "Fetch one skill (by id or exact name) with instructions",
@@ -490,7 +497,7 @@ export function createAgentRoutes(basePath = "/api", options) {
490
497
  return result;
491
498
  }
492
499
  catch (error) {
493
- return createError("EXECUTION_FAILED", error instanceof Error ? error.message : "Skill create failed", undefined, ctx.requestId);
500
+ return agentFailure(ctx, error);
494
501
  }
495
502
  },
496
503
  description: "Create a skill (routed through the host's onMutationRequest gate when configured)",
@@ -520,8 +527,12 @@ export function createAgentRoutes(basePath = "/api", options) {
520
527
  return result;
521
528
  }
522
529
  catch (error) {
530
+ // "not found" is this server's own wording for an unknown id, so
531
+ // it may be said; anything else is reduced to its category.
523
532
  const message = error instanceof Error ? error.message : "Skill update failed";
524
- return createError(message.includes("not found") ? "NOT_FOUND" : "EXECUTION_FAILED", message, undefined, ctx.requestId);
533
+ return message.includes("not found")
534
+ ? createError("NOT_FOUND", `Skill "${ctx.params.id}" not found`, undefined, ctx.requestId)
535
+ : agentFailure(ctx, error);
525
536
  }
526
537
  },
527
538
  description: "Update a skill (patch semantics; version is bumped)",
@@ -546,7 +557,9 @@ export function createAgentRoutes(basePath = "/api", options) {
546
557
  }
547
558
  catch (error) {
548
559
  const message = error instanceof Error ? error.message : "Skill delete failed";
549
- return createError(message.includes("not found") ? "NOT_FOUND" : "EXECUTION_FAILED", message, undefined, ctx.requestId);
560
+ return message.includes("not found")
561
+ ? createError("NOT_FOUND", `Skill "${ctx.params.id}" not found`, undefined, ctx.requestId)
562
+ : agentFailure(ctx, error);
550
563
  }
551
564
  },
552
565
  description: "Soft-delete (deprecate) a skill",
@@ -385,6 +385,7 @@ declare function handleAnthropicNonOkResponse(args: {
385
385
  } | null;
386
386
  entitlementFailure: AnthropicEntitlementFailure | null;
387
387
  allowConfiguredModelFallback?: boolean;
388
+ clientSuppliedBetaForwarded?: boolean;
388
389
  }): Promise<AnthropicNonOkResult>;
389
390
  /**
390
391
  * Detect Anthropic's anti-abuse / request-construction 429.