@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.
- package/CHANGELOG.md +2 -2
- package/README.md +1 -1
- package/dist/action/actionExecutor.d.ts +33 -3
- package/dist/action/actionExecutor.js +71 -15
- package/dist/action/actionInputs.js +4 -1
- package/dist/action/cliVersion.d.ts +25 -0
- package/dist/action/cliVersion.js +26 -0
- package/dist/action/index.d.ts +2 -1
- package/dist/action/index.js +2 -1
- package/dist/adapters/video/directorPipeline.js +46 -36
- package/dist/adapters/video/ffmpegAdapter.d.ts +17 -0
- package/dist/adapters/video/ffmpegAdapter.js +32 -5
- package/dist/adapters/video/frameExtractor.js +1 -1
- package/dist/adapters/video/videoMerger.js +5 -2
- package/dist/agent/arithmeticEvaluator.d.ts +32 -0
- package/dist/agent/arithmeticEvaluator.js +349 -0
- package/dist/agent/directTools.d.ts +6 -18
- package/dist/agent/directTools.js +17 -56
- package/dist/artifacts/artifactStore.d.ts +22 -1
- package/dist/artifacts/artifactStore.js +66 -6
- package/dist/auth/anthropicOAuth.d.ts +26 -1
- package/dist/auth/anthropicOAuth.js +81 -5
- package/dist/auth/secureFiles.d.ts +37 -0
- package/dist/auth/secureFiles.js +95 -0
- package/dist/auth/tokenStore.d.ts +43 -6
- package/dist/auth/tokenStore.js +260 -79
- package/dist/avatar/providers/DIDAvatar.js +49 -45
- package/dist/avatar/providers/ReplicateAvatar.js +33 -40
- package/dist/browser/neurolink.min.js +485 -558
- package/dist/cli/commands/auth.js +112 -70
- package/dist/cli/commands/proxy.d.ts +4 -0
- package/dist/cli/commands/proxy.js +128 -13
- package/dist/cli/commands/serve.d.ts +10 -1
- package/dist/cli/commands/serve.js +98 -17
- package/dist/cli/commands/server.js +17 -5
- package/dist/cli/index.js +146 -5
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/cli/utils/envManager.js +17 -3
- package/dist/cli/utils/serverUtils.d.ts +39 -3
- package/dist/cli/utils/serverUtils.js +63 -6
- package/dist/core/baseProvider.js +7 -1
- package/dist/core/modules/MessageBuilder.js +5 -0
- package/dist/core/modules/ToolsManager.d.ts +46 -1
- package/dist/core/modules/ToolsManager.js +190 -43
- package/dist/files/fileReferenceRegistry.d.ts +2 -23
- package/dist/files/fileReferenceRegistry.js +55 -5
- package/dist/hitl/hitlManager.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -1
- package/dist/mcp/auth/tokenStorage.d.ts +7 -0
- package/dist/mcp/auth/tokenStorage.js +23 -4
- package/dist/mcp/externalServerManager.d.ts +13 -3
- package/dist/mcp/externalServerManager.js +154 -136
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/mcpClientFactory.d.ts +14 -0
- package/dist/mcp/mcpClientFactory.js +70 -7
- package/dist/mcp/projectConfigTrust.d.ts +72 -0
- package/dist/mcp/projectConfigTrust.js +237 -0
- package/dist/mcp/transportUrlPolicy.d.ts +39 -0
- package/dist/mcp/transportUrlPolicy.js +172 -0
- package/dist/music/providers/BeatovenMusic.js +35 -46
- package/dist/music/providers/ReplicateMusic.js +33 -40
- package/dist/neurolink.d.ts +16 -1
- package/dist/neurolink.js +64 -11
- package/dist/processors/archive/ArchiveProcessor.js +15 -1
- package/dist/processors/archive/zipEntryIndex.d.ts +22 -0
- package/dist/processors/archive/zipEntryIndex.js +33 -0
- package/dist/processors/base/BaseFileProcessor.d.ts +9 -21
- package/dist/processors/base/BaseFileProcessor.js +40 -111
- package/dist/processors/data/YamlProcessor.d.ts +13 -1
- package/dist/processors/data/YamlProcessor.js +93 -2
- package/dist/processors/document/OpenDocumentProcessor.js +7 -2
- package/dist/processors/document/PptxProcessor.js +12 -7
- package/dist/processors/media/VideoProcessor.js +93 -15
- package/dist/proxy/inboundAuth.d.ts +89 -0
- package/dist/proxy/inboundAuth.js +219 -0
- package/dist/proxy/tokenRefresh.d.ts +6 -0
- package/dist/proxy/tokenRefresh.js +13 -1
- package/dist/proxy/updateState.js +4 -4
- package/dist/rag/document/loaders.d.ts +0 -6
- package/dist/rag/document/loaders.js +16 -10
- package/dist/rag/ragIntegration.d.ts +2 -2
- package/dist/rag/ragIntegration.js +35 -10
- package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
- package/dist/rag/retrieval/vectorQueryTool.js +54 -17
- package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
- package/dist/server/abstract/baseServerAdapter.js +119 -6
- package/dist/server/adapters/expressAdapter.d.ts +3 -0
- package/dist/server/adapters/expressAdapter.js +83 -8
- package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
- package/dist/server/adapters/fastifyAdapter.js +165 -70
- package/dist/server/adapters/honoAdapter.d.ts +9 -1
- package/dist/server/adapters/honoAdapter.js +99 -21
- package/dist/server/adapters/koaAdapter.d.ts +1 -0
- package/dist/server/adapters/koaAdapter.js +153 -74
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +4 -0
- package/dist/server/middleware/auth.js +6 -2
- package/dist/server/middleware/cache.js +6 -1
- package/dist/server/middleware/rateLimit.js +29 -4
- package/dist/server/routes/agentRoutes.js +24 -11
- package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/server/routes/claudeProxyRoutes.js +273 -84
- package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
- package/dist/server/routes/codexProxyRoutes.js +125 -40
- package/dist/server/routes/index.d.ts +4 -0
- package/dist/server/routes/index.js +35 -4
- package/dist/server/routes/mcpRoutes.d.ts +6 -2
- package/dist/server/routes/mcpRoutes.js +21 -7
- package/dist/server/routes/memoryRoutes.js +7 -6
- package/dist/server/routes/toolRoutes.d.ts +12 -2
- package/dist/server/routes/toolRoutes.js +33 -3
- package/dist/server/routes/transcribeRoutes.js +60 -13
- package/dist/server/utils/clientAddress.d.ts +23 -0
- package/dist/server/utils/clientAddress.js +65 -0
- package/dist/server/utils/clientError.d.ts +35 -0
- package/dist/server/utils/clientError.js +129 -0
- package/dist/server/utils/expressBody.d.ts +33 -0
- package/dist/server/utils/expressBody.js +353 -0
- package/dist/server/utils/pathMatch.d.ts +21 -0
- package/dist/server/utils/pathMatch.js +41 -0
- package/dist/server/utils/validation.d.ts +9 -0
- package/dist/server/utils/validation.js +9 -0
- package/dist/server/websocket/WebSocketHandler.js +22 -3
- package/dist/server/websocket/frameQueue.d.ts +19 -0
- package/dist/server/websocket/frameQueue.js +41 -3
- package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
- package/dist/server/websocket/transcribeWebSocket.js +223 -34
- package/dist/skills/skillStores.js +5 -0
- package/dist/tasks/store/fileTaskStore.d.ts +2 -0
- package/dist/tasks/store/fileTaskStore.js +11 -4
- package/dist/types/attachments.d.ts +117 -0
- package/dist/types/attachments.js +15 -0
- package/dist/types/auth.d.ts +6 -0
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/config.d.ts +56 -0
- package/dist/types/file.d.ts +8 -0
- package/dist/types/hitl.d.ts +8 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/mcp.d.ts +98 -0
- package/dist/types/multimodal.js +0 -41
- package/dist/types/processor.d.ts +14 -1
- package/dist/types/proxy.d.ts +62 -3
- package/dist/types/rag.d.ts +9 -0
- package/dist/types/safeFetch.d.ts +40 -0
- package/dist/types/server.d.ts +255 -10
- package/dist/types/tools.d.ts +57 -0
- package/dist/types/transcribe.d.ts +6 -0
- package/dist/utils/attachmentPolicy.d.ts +89 -0
- package/dist/utils/attachmentPolicy.js +327 -0
- package/dist/utils/dotenvBootstrap.d.ts +48 -19
- package/dist/utils/dotenvBootstrap.js +172 -31
- package/dist/utils/fileDetector.js +115 -65
- package/dist/utils/json/coerce.js +11 -1
- package/dist/utils/logSanitize.d.ts +5 -0
- package/dist/utils/logSanitize.js +8 -0
- package/dist/utils/logger.js +73 -1
- package/dist/utils/messageBuilder.d.ts +2 -2
- package/dist/utils/messageBuilder.js +118 -105
- package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
- package/dist/utils/multimodalOptionsBuilder.js +2 -0
- package/dist/utils/redis.d.ts +12 -0
- package/dist/utils/redis.js +72 -0
- package/dist/utils/safeFetch.d.ts +22 -1
- package/dist/utils/safeFetch.js +132 -38
- package/dist/utils/safeStoreId.d.ts +19 -0
- package/dist/utils/safeStoreId.js +29 -0
- package/dist/utils/schemaConversion.d.ts +30 -3
- package/dist/utils/schemaConversion.js +457 -84
- package/dist/utils/sizeGuard.js +2 -2
- package/dist/utils/ssrfGuard.d.ts +3 -3
- package/dist/utils/ssrfGuard.js +71 -39
- package/dist/voice/eval/sttEvalStore.d.ts +26 -3
- package/dist/voice/eval/sttEvalStore.js +77 -12
- package/dist/voice/livekit/agentDefinition.d.ts +18 -0
- package/dist/voice/livekit/agentDefinition.js +22 -0
- package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
- package/dist/voice/livekit/realtimeMcpTools.js +8 -7
- package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
- package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
- package/dist/voice/livekit/voiceAgent.d.ts +2 -2
- package/dist/voice/livekit/voiceAgent.js +2 -1
- package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
- package/dist/voice/livekit/xaiSessionWatch.js +30 -0
- package/dist/voice/providers/GeminiLive.js +21 -6
- package/dist/voice/providers/GoogleSTT.js +8 -5
- package/dist/voice/transcribe.d.ts +2 -2
- package/dist/voice/transcribe.js +34 -18
- package/dist/voice/whistle/audio.js +11 -5
- package/docs-site/static/search-index.json +75 -52
- package/package.json +171 -156
- 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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 () => ({
|
|
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;
|