@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
@@ -17,6 +17,7 @@ import { withSpan } from "../../telemetry/withSpan.js";
17
17
  import { tracers } from "../../telemetry/tracers.js";
18
18
  import { logger } from "../../utils/logger.js";
19
19
  import { MAX_AUDIO_BYTES } from "../../utils/sizeGuard.js";
20
+ import { clientFacingError, logClientFacingError, PROVIDER_NOT_CONFIGURED_MESSAGE, } from "../utils/clientError.js";
20
21
  import { RawBodyTooLargeError } from "../utils/rawBody.js";
21
22
  import { getSTTProviderDescriptor, listConfiguredSTTProviders, resolveDefaultSTTProvider, resolveSTTEndpoints, } from "../../factories/sttDescriptors.js";
22
23
  import { STTProcessor } from "../../utils/sttProcessor.js";
@@ -170,7 +171,15 @@ async function resolveAudio(request, options, limit) {
170
171
  });
171
172
  }
172
173
  catch (error) {
173
- throw new TranscribeRequestError(`could not download audioUrl: ${error instanceof Error ? error.message : String(error)}`, 400, "VALIDATION_ERROR");
174
+ // The guard's reason can name the address a host resolved to, which
175
+ // would let a caller map the server's network one URL at a time. The
176
+ // caller gets a fixed message; the operator gets the reason in the log,
177
+ // with the URL itself redacted (as agentInput.ts does).
178
+ const reason = error instanceof Error ? error.message : String(error);
179
+ logger.warn("[transcribeRoutes] refused an audioUrl", {
180
+ reason: reason.replaceAll(request.audioUrl, "<url>"),
181
+ });
182
+ throw new TranscribeRequestError("audioUrl could not be fetched (it must be a public https URL, answer without a redirect and fit the size limit)", 400, "VALIDATION_ERROR");
174
183
  }
175
184
  return {
176
185
  audio,
@@ -213,7 +222,14 @@ function toTranscribeOptions(config, audio, format, maxAudioBytes) {
213
222
  ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
214
223
  };
215
224
  }
216
- /** Status and code for an error thrown by `transcribe()` or by this file. */
225
+ /**
226
+ * Status, code and the message the client may see for an error thrown by
227
+ * `transcribe()` or by this file. The exception's own text is passed through
228
+ * only when this file composed it or when it describes the request (an empty
229
+ * or malformed recording); a missing engine is named by category, never by
230
+ * the variable that would configure it, and anything else is reduced to a
231
+ * fixed message. The detail is logged by the caller.
232
+ */
217
233
  function classifyError(error) {
218
234
  const message = error instanceof Error ? error.message : String(error);
219
235
  if (error instanceof TranscribeRequestError) {
@@ -222,10 +238,26 @@ function classifyError(error) {
222
238
  const code = typeof error === "object" && error !== null && "code" in error
223
239
  ? String(error.code)
224
240
  : undefined;
241
+ if (code === "STT_PROVIDER_NOT_CONFIGURED") {
242
+ return {
243
+ status: 503,
244
+ code: "PROVIDER_NOT_CONFIGURED",
245
+ message: PROVIDER_NOT_CONFIGURED_MESSAGE,
246
+ };
247
+ }
225
248
  if (code && CLIENT_ERROR_CODES.has(code)) {
226
249
  return { status: 400, code: "VALIDATION_ERROR", message };
227
250
  }
228
- return { status: 500, code: "EXECUTION_FAILED", message };
251
+ const shaped = clientFacingError(error);
252
+ return shaped.status === 500
253
+ ? { ...shaped, code: "EXECUTION_FAILED" }
254
+ : shaped;
255
+ }
256
+ /** `classifyError` plus the server-side log the client-facing message leaves out. */
257
+ function classifyAndLog(ctx, route, error) {
258
+ const classified = classifyError(error);
259
+ logClientFacingError(`transcribeRoutes ${route}`, ctx.requestId, error, classified);
260
+ return classified;
229
261
  }
230
262
  async function runTranscription(ctx, route, options) {
231
263
  return withSpan({
@@ -420,11 +452,7 @@ async function handleOpenAITranscription(ctx, options, limit, route) {
420
452
  result = await runTranscription(ctx, route, toTranscribeOptions(parsed.data, audio, format, limit));
421
453
  }
422
454
  catch (error) {
423
- const classified = classifyError(error);
424
- logger.warn("[transcribeRoutes] OpenAI-compatible transcription failed", {
425
- requestId: ctx.requestId,
426
- error: classified.message,
427
- });
455
+ const classified = classifyAndLog(ctx, route, error);
428
456
  return openAIError(classified.status, classified.message);
429
457
  }
430
458
  if (responseFormat === "text") {
@@ -541,7 +569,17 @@ export function createTranscribeRoutes(basePath = "/api", options = {}) {
541
569
  const nativePath = `${basePath}/agent/transcribe`;
542
570
  const openaiPath = `${options.openaiBasePath ?? ""}/v1/audio/transcriptions`;
543
571
  const evalPath = `${options.openaiBasePath ?? ""}/v1/evals/stt`;
544
- const evalStore = options.evals === false ? undefined : new STTEvalStore(options.evals);
572
+ // The collector keeps every transcript it is sent and reads them back to
573
+ // whoever asks, so it is mounted only when the operator asks for it: an
574
+ // options object, or NEUROLINK_STT_EVAL_ENABLED=1. It used to be on by
575
+ // default.
576
+ const evalStore = options.evals === false
577
+ ? undefined
578
+ : options.evals !== undefined
579
+ ? new STTEvalStore(options.evals)
580
+ : process.env.NEUROLINK_STT_EVAL_ENABLED?.trim() === "1"
581
+ ? new STTEvalStore()
582
+ : undefined;
545
583
  return {
546
584
  // Empty prefix: the two routes live under different roots, and every
547
585
  // route path already starts with "", so the adapters add nothing.
@@ -561,7 +599,7 @@ export function createTranscribeRoutes(basePath = "/api", options = {}) {
561
599
  return await runTranscription(ctx, nativePath, toTranscribeOptions(request, audio, format, limit));
562
600
  }
563
601
  catch (error) {
564
- const classified = classifyError(error);
602
+ const classified = classifyAndLog(ctx, nativePath, error);
565
603
  return createErrorResponse(classified.code, classified.message, undefined, ctx.requestId, classified.status);
566
604
  }
567
605
  },
@@ -593,6 +631,11 @@ export function createTranscribeRoutes(basePath = "/api", options = {}) {
593
631
  * apart they were per speaker.
594
632
  */
595
633
  function createEvalRoutes(evalPath, store) {
634
+ // A person the server identified owns the calls they post and sees only
635
+ // those; a shared key names no person, so its calls are visible to every
636
+ // holder, as before. The handler sees ctx.user only when the server carries
637
+ // middleware state (see ServerAdapterConfig.carryMiddlewareState).
638
+ const ownerOf = (ctx) => ctx.user && ctx.user.shared !== true ? ctx.user.id : undefined;
596
639
  return [
597
640
  {
598
641
  method: "POST",
@@ -603,7 +646,9 @@ function createEvalRoutes(evalPath, store) {
603
646
  return validation.error;
604
647
  }
605
648
  const batch = validation.data;
606
- return store.accept(batch);
649
+ const accepted = await store.accept(batch, ownerOf(ctx));
650
+ return (accepted ??
651
+ createErrorResponse("FORBIDDEN", "This call belongs to another caller", undefined, ctx.requestId, 403));
607
652
  },
608
653
  description: "Accept a batch of one call's segments from two STTs (primary shown, shadow for evaluation); a resent segment id replaces the earlier one",
609
654
  tags: ["transcribe", "evaluation"],
@@ -611,7 +656,9 @@ function createEvalRoutes(evalPath, store) {
611
656
  {
612
657
  method: "GET",
613
658
  path: evalPath,
614
- handler: async () => ({ calls: await store.list() }),
659
+ handler: async (ctx) => ({
660
+ calls: await store.list(ownerOf(ctx)),
661
+ }),
615
662
  description: "Calls collected for STT evaluation, most recent first",
616
663
  tags: ["transcribe", "evaluation"],
617
664
  },
@@ -622,7 +669,7 @@ function createEvalRoutes(evalPath, store) {
622
669
  // Adapters already decode path params; decoding again would turn a
623
670
  // literal "%" in an id into something else, or throw on "%E0%A4%A".
624
671
  const callId = ctx.params.callId ?? "";
625
- const report = await store.report(callId, ctx.query.segments === "true");
672
+ const report = await store.report(callId, ctx.query.segments === "true", ownerOf(ctx));
626
673
  return (report ??
627
674
  createErrorResponse("NOT_FOUND", `No STT evaluation for call ${callId}`, undefined, ctx.requestId, 404));
628
675
  },
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The address a request is counted against.
3
+ *
4
+ * The TCP peer is the only address a client cannot choose, so it is the
5
+ * default. `X-Forwarded-For` is read only when the operator says a reverse
6
+ * proxy stands in front of the server (`trustProxy`), and then from the right:
7
+ * the hops of the listed proxies are skipped and the first other entry is the
8
+ * client, because a proxy appends the address it saw and anything a client put
9
+ * in the header itself sits further left.
10
+ *
11
+ * @module server/utils/clientAddress
12
+ */
13
+ import type { ServerClientAddressInput } from "../../types/index.js";
14
+ /** Strip an IPv4-mapped IPv6 prefix and a port, so one client has one key. */
15
+ export declare function canonicalAddress(address: string): string | undefined;
16
+ /**
17
+ * The client's address, or `undefined` when the runtime reports no peer and
18
+ * no trusted header names one. The result is always a bare IP, so a header
19
+ * holding anything else cannot become a counter key.
20
+ */
21
+ export declare function resolveClientAddress(input: ServerClientAddressInput): string | undefined;
22
+ /** `true` for an address only this host can reach: 127/8, `::1`, or their mapped forms. */
23
+ export declare function isLoopbackAddress(address: string | undefined): boolean;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The address a request is counted against.
3
+ *
4
+ * The TCP peer is the only address a client cannot choose, so it is the
5
+ * default. `X-Forwarded-For` is read only when the operator says a reverse
6
+ * proxy stands in front of the server (`trustProxy`), and then from the right:
7
+ * the hops of the listed proxies are skipped and the first other entry is the
8
+ * client, because a proxy appends the address it saw and anything a client put
9
+ * in the header itself sits further left.
10
+ *
11
+ * @module server/utils/clientAddress
12
+ */
13
+ import { isIP } from "node:net";
14
+ /** Strip an IPv4-mapped IPv6 prefix and a port, so one client has one key. */
15
+ export function canonicalAddress(address) {
16
+ let value = address.trim();
17
+ if (value.startsWith("[")) {
18
+ const end = value.indexOf("]");
19
+ value = end === -1 ? value.slice(1) : value.slice(1, end);
20
+ }
21
+ else if (/^\d{1,3}(\.\d{1,3}){3}:\d+$/.test(value) ||
22
+ /^[^:]+:\d+$/.test(value)) {
23
+ value = value.slice(0, value.lastIndexOf(":"));
24
+ }
25
+ if (value.toLowerCase().startsWith("::ffff:") && isIP(value.slice(7)) === 4) {
26
+ value = value.slice(7);
27
+ }
28
+ return isIP(value) ? value : undefined;
29
+ }
30
+ /**
31
+ * The client's address, or `undefined` when the runtime reports no peer and
32
+ * no trusted header names one. The result is always a bare IP, so a header
33
+ * holding anything else cannot become a counter key.
34
+ */
35
+ export function resolveClientAddress(input) {
36
+ const peer = input.remoteAddress
37
+ ? canonicalAddress(input.remoteAddress)
38
+ : undefined;
39
+ if (!input.trustProxy || !input.forwardedFor) {
40
+ return peer;
41
+ }
42
+ const trusted = new Set(input.trustedProxies
43
+ .map(canonicalAddress)
44
+ .filter((value) => value !== undefined));
45
+ const hops = input.forwardedFor
46
+ .split(",")
47
+ .map(canonicalAddress)
48
+ .filter((value) => value !== undefined);
49
+ for (let index = hops.length - 1; index >= 0; index--) {
50
+ if (!trusted.has(hops[index])) {
51
+ return hops[index];
52
+ }
53
+ }
54
+ return peer;
55
+ }
56
+ /** `true` for an address only this host can reach: 127/8, `::1`, or their mapped forms. */
57
+ export function isLoopbackAddress(address) {
58
+ if (!address) {
59
+ return false;
60
+ }
61
+ const value = canonicalAddress(address) ?? address.trim().toLowerCase();
62
+ return (value === "::1" ||
63
+ value === "localhost" ||
64
+ /^127\.\d+\.\d+\.\d+$/.test(value));
65
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * What a failed request may tell the client.
3
+ *
4
+ * An exception's text names environment variables, provider wiring, file
5
+ * paths and upstream response bodies; none of that belongs in a response to
6
+ * an unknown caller. A route therefore answers with one of a few fixed
7
+ * messages plus the request id, and the detail goes to the server log under
8
+ * that id, where the operator can find it. The pattern is the one the OpenAI
9
+ * proxy route already followed.
10
+ *
11
+ * What is passed through: a `ServerAdapterError` below 500, whose message
12
+ * this server composed (a validation failure, a missing route), and the
13
+ * category of a provider that is missing credentials, without saying which
14
+ * credential.
15
+ *
16
+ * @module server/utils/clientError
17
+ */
18
+ import type { ServerClientFacingError } from "../../types/index.js";
19
+ export declare const GENERIC_ERROR_MESSAGE = "An internal error occurred";
20
+ export declare const PROVIDER_NOT_CONFIGURED_MESSAGE = "The requested provider is not configured on this server";
21
+ export declare const UPSTREAM_RATE_LIMITED_MESSAGE = "The upstream provider is rate limiting this server; retry later";
22
+ export declare const REQUEST_TIMEOUT_MESSAGE = "The request timed out";
23
+ /**
24
+ * The response for `error`. Never the exception's own text unless this
25
+ * server wrote it (a `ServerAdapterError` below 500).
26
+ */
27
+ export declare function clientFacingError(error: unknown): ServerClientFacingError;
28
+ /**
29
+ * Log the detail the client does not get, under the request id the client
30
+ * does get. A client error is routine traffic and logs as one line; anything
31
+ * else keeps its stack.
32
+ */
33
+ export declare function logClientFacingError(source: string, requestId: string | undefined, error: unknown, shaped: ServerClientFacingError): void;
34
+ /** {@link clientFacingError} and {@link logClientFacingError} in one step. */
35
+ export declare function shapeRouteError(source: string, requestId: string | undefined, error: unknown): ServerClientFacingError;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * What a failed request may tell the client.
3
+ *
4
+ * An exception's text names environment variables, provider wiring, file
5
+ * paths and upstream response bodies; none of that belongs in a response to
6
+ * an unknown caller. A route therefore answers with one of a few fixed
7
+ * messages plus the request id, and the detail goes to the server log under
8
+ * that id, where the operator can find it. The pattern is the one the OpenAI
9
+ * proxy route already followed.
10
+ *
11
+ * What is passed through: a `ServerAdapterError` below 500, whose message
12
+ * this server composed (a validation failure, a missing route), and the
13
+ * category of a provider that is missing credentials, without saying which
14
+ * credential.
15
+ *
16
+ * @module server/utils/clientError
17
+ */
18
+ import { AuthenticationError as ProviderAuthenticationError, RateLimitError as ProviderRateLimitError, } from "../../types/index.js";
19
+ import { logger } from "../../utils/logger.js";
20
+ import { ServerAdapterError } from "../errors.js";
21
+ export const GENERIC_ERROR_MESSAGE = "An internal error occurred";
22
+ export const PROVIDER_NOT_CONFIGURED_MESSAGE = "The requested provider is not configured on this server";
23
+ export const UPSTREAM_RATE_LIMITED_MESSAGE = "The upstream provider is rate limiting this server; retry later";
24
+ export const REQUEST_TIMEOUT_MESSAGE = "The request timed out";
25
+ /** Signs that a provider refused the server's own credentials, or has none. */
26
+ const PROVIDER_CONFIGURATION_PATTERN = /api[ _-]?key|credential|not configured|no (?:ai )?provider|authenticat|unauthori[sz]ed|forbidden|permission denied|invalid[ _-]?(?:key|token)|missing (?:required )?(?:environment|env|key|token)|environment variable/i;
27
+ /** Names the SDK's own error classes and codes use for the same thing. */
28
+ const PROVIDER_CONFIGURATION_CODES = new Set([
29
+ "AUTHENTICATION_ERROR",
30
+ "AUTHENTICATION_FAILED",
31
+ "AUTH_ERROR",
32
+ "INVALID_API_KEY",
33
+ "MISSING_API_KEY",
34
+ "PROVIDER_NOT_CONFIGURED",
35
+ "CONFIGURATION_ERROR",
36
+ "NO_PROVIDER",
37
+ "STT_PROVIDER_NOT_CONFIGURED",
38
+ ]);
39
+ function codeOf(error) {
40
+ if (typeof error !== "object" || error === null) {
41
+ return undefined;
42
+ }
43
+ const code = error.code;
44
+ return typeof code === "string" ? code : undefined;
45
+ }
46
+ function isAbortError(error) {
47
+ return (error instanceof Error &&
48
+ (error.name === "AbortError" || error.name === "TimeoutError"));
49
+ }
50
+ /**
51
+ * The response for `error`. Never the exception's own text unless this
52
+ * server wrote it (a `ServerAdapterError` below 500).
53
+ */
54
+ export function clientFacingError(error) {
55
+ if (error instanceof ServerAdapterError) {
56
+ // The request timeout is the server's deadline, not the client's slowness
57
+ // (the TIMEOUT code maps to 408 for the latter), so it answers 504.
58
+ if (error.name === "ServerTimeoutError") {
59
+ return {
60
+ code: "REQUEST_TIMEOUT",
61
+ message: REQUEST_TIMEOUT_MESSAGE,
62
+ status: 504,
63
+ };
64
+ }
65
+ const status = error.getHttpStatus();
66
+ if (status < 500) {
67
+ return { code: error.code, message: error.message, status };
68
+ }
69
+ return { code: "INTERNAL_ERROR", message: GENERIC_ERROR_MESSAGE, status };
70
+ }
71
+ if (isAbortError(error)) {
72
+ return {
73
+ code: "REQUEST_TIMEOUT",
74
+ message: REQUEST_TIMEOUT_MESSAGE,
75
+ status: 504,
76
+ };
77
+ }
78
+ if (error instanceof ProviderRateLimitError) {
79
+ return {
80
+ code: "UPSTREAM_RATE_LIMITED",
81
+ message: UPSTREAM_RATE_LIMITED_MESSAGE,
82
+ status: 503,
83
+ };
84
+ }
85
+ const code = codeOf(error);
86
+ const message = error instanceof Error ? error.message : String(error);
87
+ if (error instanceof ProviderAuthenticationError ||
88
+ (code !== undefined && PROVIDER_CONFIGURATION_CODES.has(code)) ||
89
+ PROVIDER_CONFIGURATION_PATTERN.test(message)) {
90
+ return {
91
+ code: "PROVIDER_NOT_CONFIGURED",
92
+ message: PROVIDER_NOT_CONFIGURED_MESSAGE,
93
+ status: 503,
94
+ };
95
+ }
96
+ return {
97
+ code: "INTERNAL_ERROR",
98
+ message: GENERIC_ERROR_MESSAGE,
99
+ status: 500,
100
+ };
101
+ }
102
+ /**
103
+ * Log the detail the client does not get, under the request id the client
104
+ * does get. A client error is routine traffic and logs as one line; anything
105
+ * else keeps its stack.
106
+ */
107
+ export function logClientFacingError(source, requestId, error, shaped) {
108
+ const detail = error instanceof Error ? error.message : String(error);
109
+ if (shaped.status < 500) {
110
+ logger.warn(`[${source}] Request rejected`, {
111
+ requestId,
112
+ code: shaped.code,
113
+ error: detail,
114
+ });
115
+ return;
116
+ }
117
+ logger.error(`[${source}] Request failed`, {
118
+ requestId,
119
+ code: shaped.code,
120
+ error: detail,
121
+ stack: error instanceof Error ? error.stack : undefined,
122
+ });
123
+ }
124
+ /** {@link clientFacingError} and {@link logClientFacingError} in one step. */
125
+ export function shapeRouteError(source, requestId, error) {
126
+ const shaped = clientFacingError(error);
127
+ logClientFacingError(source, requestId, error, shaped);
128
+ return shaped;
129
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Bounded JSON and form body reads for the Express adapter.
3
+ *
4
+ * `express.json()` and `express.urlencoded()` (body-parser) refuse a body over
5
+ * their limit with a 413, but only after the whole request has been taken off
6
+ * the socket: a declared Content-Length over the limit is seen at once and the
7
+ * body is still drained, and a chunked body is read to its end. Nothing has
8
+ * authenticated the request at that point, so a client could make the server
9
+ * receive any amount of data for the price of one 413.
10
+ *
11
+ * `createBoundedBodyParser` wraps a body-parser middleware so the bytes are
12
+ * read here, up to the limit, and handed to that same middleware to parse. Which
13
+ * requests are parsed, charset handling, strict JSON, the qs options and the
14
+ * error each failure carries all stay body-parser's, because body-parser still
15
+ * makes those decisions: it is asked first, from the headers alone, whether it
16
+ * would read the request, and then parses the bytes it is given. What changes is
17
+ * who reads the socket and what happens at the limit. A body over the limit is
18
+ * refused without reading it (declared Content-Length) or at the limit (chunked,
19
+ * or a compressed body that inflates past it), and the connection is closed so
20
+ * the client stops sending.
21
+ *
22
+ * `express.raw()` (uploads) is not wrapped.
23
+ *
24
+ * @module server/utils/expressBody
25
+ */
26
+ import type { ServerNodeMiddleware } from "../../types/index.js";
27
+ /**
28
+ * Wrap a body-parser middleware (`express.json()`, `express.urlencoded()`) so a
29
+ * body is read only as far as `limit`. `limit` is the one the middleware was
30
+ * built with. A limit that cannot be read as a byte count here is left to the
31
+ * middleware alone.
32
+ */
33
+ export declare function createBoundedBodyParser(parser: ServerNodeMiddleware, limit: string | number | undefined): ServerNodeMiddleware;