@juspay/neurolink 9.63.1 → 9.65.0
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 +12 -0
- package/README.md +18 -17
- package/dist/adapters/providerImageAdapter.js +29 -1
- package/dist/adapters/replicate/auth.d.ts +19 -0
- package/dist/adapters/replicate/auth.js +32 -0
- package/dist/adapters/replicate/predictionLifecycle.d.ts +46 -0
- package/dist/adapters/replicate/predictionLifecycle.js +283 -0
- package/dist/adapters/video/klingVideoHandler.d.ts +37 -0
- package/dist/adapters/video/klingVideoHandler.js +305 -0
- package/dist/adapters/video/replicateVideoHandler.d.ts +29 -0
- package/dist/adapters/video/replicateVideoHandler.js +157 -0
- package/dist/adapters/video/runwayVideoHandler.d.ts +32 -0
- package/dist/adapters/video/runwayVideoHandler.js +316 -0
- package/dist/adapters/video/vertexVideoHandler.d.ts +19 -1
- package/dist/adapters/video/vertexVideoHandler.js +42 -11
- package/dist/autoresearch/runner.js +8 -2
- package/dist/avatar/index.d.ts +13 -0
- package/dist/avatar/index.js +13 -0
- package/dist/avatar/providers/DIDAvatar.d.ts +49 -0
- package/dist/avatar/providers/DIDAvatar.js +501 -0
- package/dist/avatar/providers/HeyGenAvatar.d.ts +30 -0
- package/dist/avatar/providers/HeyGenAvatar.js +337 -0
- package/dist/avatar/providers/ReplicateAvatar.d.ts +36 -0
- package/dist/avatar/providers/ReplicateAvatar.js +267 -0
- package/dist/browser/neurolink.min.js +573 -554
- package/dist/cli/commands/mcp.js +29 -0
- package/dist/cli/commands/proxy.js +24 -5
- package/dist/cli/factories/commandFactory.d.ts +25 -1
- package/dist/cli/factories/commandFactory.js +341 -63
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/cli/loop/optionsSchema.js +12 -0
- package/dist/constants/contextWindows.js +101 -0
- package/dist/constants/enums.d.ts +273 -2
- package/dist/constants/enums.js +290 -1
- package/dist/constants/videoErrors.d.ts +4 -0
- package/dist/constants/videoErrors.js +4 -0
- package/dist/core/baseProvider.d.ts +23 -3
- package/dist/core/baseProvider.js +217 -11
- package/dist/core/constants.d.ts +11 -0
- package/dist/core/constants.js +69 -1
- package/dist/core/modules/MessageBuilder.js +20 -0
- package/dist/core/redisConversationMemoryManager.js +6 -0
- package/dist/evaluation/index.d.ts +2 -0
- package/dist/evaluation/index.js +4 -0
- package/dist/factories/providerFactory.js +7 -1
- package/dist/factories/providerRegistry.js +203 -2
- package/dist/features/ppt/contentPlanner.js +42 -14
- package/dist/index.d.ts +9 -1
- package/dist/index.js +16 -1
- package/dist/lib/adapters/providerImageAdapter.js +29 -1
- package/dist/lib/adapters/replicate/auth.d.ts +19 -0
- package/dist/lib/adapters/replicate/auth.js +33 -0
- package/dist/lib/adapters/replicate/predictionLifecycle.d.ts +46 -0
- package/dist/lib/adapters/replicate/predictionLifecycle.js +284 -0
- package/dist/lib/adapters/video/klingVideoHandler.d.ts +37 -0
- package/dist/lib/adapters/video/klingVideoHandler.js +306 -0
- package/dist/lib/adapters/video/replicateVideoHandler.d.ts +29 -0
- package/dist/lib/adapters/video/replicateVideoHandler.js +158 -0
- package/dist/lib/adapters/video/runwayVideoHandler.d.ts +32 -0
- package/dist/lib/adapters/video/runwayVideoHandler.js +317 -0
- package/dist/lib/adapters/video/vertexVideoHandler.d.ts +19 -1
- package/dist/lib/adapters/video/vertexVideoHandler.js +42 -11
- package/dist/lib/autoresearch/runner.js +8 -2
- package/dist/lib/avatar/index.d.ts +13 -0
- package/dist/lib/avatar/index.js +14 -0
- package/dist/lib/avatar/providers/DIDAvatar.d.ts +49 -0
- package/dist/lib/avatar/providers/DIDAvatar.js +502 -0
- package/dist/lib/avatar/providers/HeyGenAvatar.d.ts +30 -0
- package/dist/lib/avatar/providers/HeyGenAvatar.js +338 -0
- package/dist/lib/avatar/providers/ReplicateAvatar.d.ts +36 -0
- package/dist/lib/avatar/providers/ReplicateAvatar.js +268 -0
- package/dist/lib/constants/contextWindows.js +101 -0
- package/dist/lib/constants/enums.d.ts +273 -2
- package/dist/lib/constants/enums.js +290 -1
- package/dist/lib/constants/videoErrors.d.ts +4 -0
- package/dist/lib/constants/videoErrors.js +4 -0
- package/dist/lib/core/baseProvider.d.ts +23 -3
- package/dist/lib/core/baseProvider.js +217 -11
- package/dist/lib/core/constants.d.ts +11 -0
- package/dist/lib/core/constants.js +69 -1
- package/dist/lib/core/modules/MessageBuilder.js +20 -0
- package/dist/lib/core/redisConversationMemoryManager.js +6 -0
- package/dist/lib/evaluation/index.d.ts +2 -0
- package/dist/lib/evaluation/index.js +4 -0
- package/dist/lib/factories/providerFactory.js +7 -1
- package/dist/lib/factories/providerRegistry.js +203 -2
- package/dist/lib/features/ppt/contentPlanner.js +42 -14
- package/dist/lib/index.d.ts +9 -1
- package/dist/lib/index.js +16 -1
- package/dist/lib/memory/hippocampusInitializer.d.ts +2 -2
- package/dist/lib/memory/hippocampusInitializer.js +32 -2
- package/dist/lib/middleware/builtin/lifecycle.js +52 -51
- package/dist/lib/music/index.d.ts +13 -0
- package/dist/lib/music/index.js +14 -0
- package/dist/lib/music/providers/BeatovenMusic.d.ts +31 -0
- package/dist/lib/music/providers/BeatovenMusic.js +334 -0
- package/dist/lib/music/providers/ElevenLabsMusic.d.ts +30 -0
- package/dist/lib/music/providers/ElevenLabsMusic.js +169 -0
- package/dist/lib/music/providers/LyriaMusic.d.ts +29 -0
- package/dist/lib/music/providers/LyriaMusic.js +173 -0
- package/dist/lib/music/providers/ReplicateMusic.d.ts +31 -0
- package/dist/lib/music/providers/ReplicateMusic.js +262 -0
- package/dist/lib/neurolink.d.ts +30 -0
- package/dist/lib/neurolink.js +342 -49
- package/dist/lib/providers/amazonBedrock.d.ts +10 -0
- package/dist/lib/providers/amazonBedrock.js +94 -39
- package/dist/lib/providers/anthropic.js +55 -7
- package/dist/lib/providers/anthropicBaseProvider.js +1 -1
- package/dist/lib/providers/azureOpenai.js +66 -17
- package/dist/lib/providers/cloudflare.d.ts +35 -0
- package/dist/lib/providers/cloudflare.js +174 -0
- package/dist/lib/providers/cohere.d.ts +52 -0
- package/dist/lib/providers/cohere.js +253 -0
- package/dist/lib/providers/deepseek.js +72 -17
- package/dist/lib/providers/fireworks.d.ts +33 -0
- package/dist/lib/providers/fireworks.js +164 -0
- package/dist/lib/providers/googleAiStudio.d.ts +11 -3
- package/dist/lib/providers/googleAiStudio.js +336 -344
- package/dist/lib/providers/googleNativeGemini3.d.ts +107 -2
- package/dist/lib/providers/googleNativeGemini3.js +381 -25
- package/dist/lib/providers/googleVertex.d.ts +116 -129
- package/dist/lib/providers/googleVertex.js +3002 -1988
- package/dist/lib/providers/groq.d.ts +33 -0
- package/dist/lib/providers/groq.js +181 -0
- package/dist/lib/providers/huggingFace.js +9 -8
- package/dist/lib/providers/ideogram.d.ts +34 -0
- package/dist/lib/providers/ideogram.js +184 -0
- package/dist/lib/providers/index.d.ts +13 -0
- package/dist/lib/providers/index.js +13 -0
- package/dist/lib/providers/jina.d.ts +59 -0
- package/dist/lib/providers/jina.js +218 -0
- package/dist/lib/providers/llamaCpp.js +14 -46
- package/dist/lib/providers/lmStudio.js +14 -47
- package/dist/lib/providers/mistral.js +7 -7
- package/dist/lib/providers/nvidiaNim.js +160 -19
- package/dist/lib/providers/ollama.js +7 -7
- package/dist/lib/providers/openAI.d.ts +22 -1
- package/dist/lib/providers/openAI.js +181 -0
- package/dist/lib/providers/openRouter.js +38 -22
- package/dist/lib/providers/openaiCompatible.js +9 -8
- package/dist/lib/providers/perplexity.d.ts +33 -0
- package/dist/lib/providers/perplexity.js +179 -0
- package/dist/lib/providers/recraft.d.ts +34 -0
- package/dist/lib/providers/recraft.js +197 -0
- package/dist/lib/providers/replicate.d.ts +75 -0
- package/dist/lib/providers/replicate.js +403 -0
- package/dist/lib/providers/stability.d.ts +37 -0
- package/dist/lib/providers/stability.js +191 -0
- package/dist/lib/providers/togetherAi.d.ts +33 -0
- package/dist/lib/providers/togetherAi.js +176 -0
- package/dist/lib/providers/voyage.d.ts +47 -0
- package/dist/lib/providers/voyage.js +177 -0
- package/dist/lib/providers/xai.d.ts +33 -0
- package/dist/lib/providers/xai.js +172 -0
- package/dist/lib/telemetry/index.d.ts +1 -1
- package/dist/lib/telemetry/index.js +1 -1
- package/dist/lib/telemetry/tracers.d.ts +19 -0
- package/dist/lib/telemetry/tracers.js +19 -0
- package/dist/lib/telemetry/withSpan.d.ts +35 -0
- package/dist/lib/telemetry/withSpan.js +103 -0
- package/dist/lib/types/aliases.d.ts +14 -0
- package/dist/lib/types/avatar.d.ts +143 -0
- package/dist/lib/types/avatar.js +20 -0
- package/dist/lib/types/cli.d.ts +6 -0
- package/dist/lib/types/common.d.ts +0 -3
- package/dist/lib/types/conversation.d.ts +10 -3
- package/dist/lib/types/generate.d.ts +76 -5
- package/dist/lib/types/index.d.ts +6 -0
- package/dist/lib/types/index.js +8 -0
- package/dist/lib/types/memory.d.ts +96 -0
- package/dist/lib/types/memory.js +23 -0
- package/dist/lib/types/middleware.d.ts +27 -0
- package/dist/lib/types/multimodal.d.ts +35 -2
- package/dist/lib/types/music.d.ts +165 -0
- package/dist/lib/types/music.js +21 -0
- package/dist/lib/types/providers.d.ts +284 -3
- package/dist/lib/types/replicate.d.ts +67 -0
- package/dist/lib/types/replicate.js +10 -0
- package/dist/lib/types/safeFetch.d.ts +15 -0
- package/dist/lib/types/safeFetch.js +7 -0
- package/dist/lib/types/stream.d.ts +8 -1
- package/dist/lib/types/tools.d.ts +13 -0
- package/dist/lib/types/video.d.ts +89 -0
- package/dist/lib/types/video.js +15 -0
- package/dist/lib/utils/avatarProcessor.d.ts +68 -0
- package/dist/lib/utils/avatarProcessor.js +172 -0
- package/dist/lib/utils/cloneOptions.d.ts +36 -0
- package/dist/lib/utils/cloneOptions.js +62 -0
- package/dist/lib/utils/lifecycleCallbacks.d.ts +56 -0
- package/dist/lib/utils/lifecycleCallbacks.js +100 -0
- package/dist/lib/utils/lifecycleTimeout.d.ts +25 -0
- package/dist/lib/utils/lifecycleTimeout.js +39 -0
- package/dist/lib/utils/logSanitize.d.ts +49 -0
- package/dist/lib/utils/logSanitize.js +170 -0
- package/dist/lib/utils/loggingFetch.d.ts +29 -0
- package/dist/lib/utils/loggingFetch.js +60 -0
- package/dist/lib/utils/messageBuilder.d.ts +10 -0
- package/dist/lib/utils/messageBuilder.js +83 -30
- package/dist/lib/utils/modelChoices.js +236 -3
- package/dist/lib/utils/modelDetection.d.ts +11 -0
- package/dist/lib/utils/modelDetection.js +27 -0
- package/dist/lib/utils/musicProcessor.d.ts +67 -0
- package/dist/lib/utils/musicProcessor.js +189 -0
- package/dist/lib/utils/optionsConversion.js +3 -2
- package/dist/lib/utils/parameterValidation.js +14 -4
- package/dist/lib/utils/pricing.js +193 -0
- package/dist/lib/utils/providerConfig.d.ts +55 -0
- package/dist/lib/utils/providerConfig.js +224 -0
- package/dist/lib/utils/providerHealth.js +7 -7
- package/dist/lib/utils/safeFetch.d.ts +26 -0
- package/dist/lib/utils/safeFetch.js +83 -0
- package/dist/lib/utils/schemaConversion.d.ts +1 -1
- package/dist/lib/utils/schemaConversion.js +59 -4
- package/dist/lib/utils/sizeGuard.d.ts +34 -0
- package/dist/lib/utils/sizeGuard.js +45 -0
- package/dist/lib/utils/ssrfGuard.d.ts +52 -0
- package/dist/lib/utils/ssrfGuard.js +411 -0
- package/dist/lib/utils/tokenLimits.js +23 -32
- package/dist/lib/utils/videoProcessor.d.ts +60 -0
- package/dist/lib/utils/videoProcessor.js +201 -0
- package/dist/lib/voice/providers/FishAudioTTS.d.ts +27 -0
- package/dist/lib/voice/providers/FishAudioTTS.js +183 -0
- package/dist/lib/workflow/core/ensembleExecutor.js +26 -9
- package/dist/memory/hippocampusInitializer.d.ts +2 -2
- package/dist/memory/hippocampusInitializer.js +32 -2
- package/dist/middleware/builtin/lifecycle.js +52 -51
- package/dist/music/index.d.ts +13 -0
- package/dist/music/index.js +13 -0
- package/dist/music/providers/BeatovenMusic.d.ts +31 -0
- package/dist/music/providers/BeatovenMusic.js +333 -0
- package/dist/music/providers/ElevenLabsMusic.d.ts +30 -0
- package/dist/music/providers/ElevenLabsMusic.js +168 -0
- package/dist/music/providers/LyriaMusic.d.ts +29 -0
- package/dist/music/providers/LyriaMusic.js +172 -0
- package/dist/music/providers/ReplicateMusic.d.ts +31 -0
- package/dist/music/providers/ReplicateMusic.js +261 -0
- package/dist/neurolink.d.ts +30 -0
- package/dist/neurolink.js +342 -49
- package/dist/providers/amazonBedrock.d.ts +10 -0
- package/dist/providers/amazonBedrock.js +94 -39
- package/dist/providers/anthropic.js +55 -7
- package/dist/providers/anthropicBaseProvider.js +1 -1
- package/dist/providers/azureOpenai.js +66 -17
- package/dist/providers/cloudflare.d.ts +35 -0
- package/dist/providers/cloudflare.js +173 -0
- package/dist/providers/cohere.d.ts +52 -0
- package/dist/providers/cohere.js +252 -0
- package/dist/providers/deepseek.js +72 -17
- package/dist/providers/fireworks.d.ts +33 -0
- package/dist/providers/fireworks.js +163 -0
- package/dist/providers/googleAiStudio.d.ts +11 -3
- package/dist/providers/googleAiStudio.js +335 -344
- package/dist/providers/googleNativeGemini3.d.ts +107 -2
- package/dist/providers/googleNativeGemini3.js +381 -25
- package/dist/providers/googleVertex.d.ts +116 -129
- package/dist/providers/googleVertex.js +3000 -1987
- package/dist/providers/groq.d.ts +33 -0
- package/dist/providers/groq.js +180 -0
- package/dist/providers/huggingFace.js +9 -8
- package/dist/providers/ideogram.d.ts +34 -0
- package/dist/providers/ideogram.js +183 -0
- package/dist/providers/index.d.ts +13 -0
- package/dist/providers/index.js +13 -0
- package/dist/providers/jina.d.ts +59 -0
- package/dist/providers/jina.js +217 -0
- package/dist/providers/llamaCpp.js +14 -46
- package/dist/providers/lmStudio.js +14 -47
- package/dist/providers/mistral.js +7 -7
- package/dist/providers/nvidiaNim.js +160 -19
- package/dist/providers/ollama.js +7 -7
- package/dist/providers/openAI.d.ts +22 -1
- package/dist/providers/openAI.js +181 -0
- package/dist/providers/openRouter.js +38 -22
- package/dist/providers/openaiCompatible.js +9 -8
- package/dist/providers/perplexity.d.ts +33 -0
- package/dist/providers/perplexity.js +178 -0
- package/dist/providers/recraft.d.ts +34 -0
- package/dist/providers/recraft.js +196 -0
- package/dist/providers/replicate.d.ts +75 -0
- package/dist/providers/replicate.js +402 -0
- package/dist/providers/stability.d.ts +37 -0
- package/dist/providers/stability.js +190 -0
- package/dist/providers/togetherAi.d.ts +33 -0
- package/dist/providers/togetherAi.js +175 -0
- package/dist/providers/voyage.d.ts +47 -0
- package/dist/providers/voyage.js +176 -0
- package/dist/providers/xai.d.ts +33 -0
- package/dist/providers/xai.js +171 -0
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +1 -1
- package/dist/telemetry/tracers.d.ts +19 -0
- package/dist/telemetry/tracers.js +19 -0
- package/dist/telemetry/withSpan.d.ts +35 -0
- package/dist/telemetry/withSpan.js +103 -0
- package/dist/types/aliases.d.ts +14 -0
- package/dist/types/avatar.d.ts +143 -0
- package/dist/types/avatar.js +19 -0
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/common.d.ts +0 -3
- package/dist/types/conversation.d.ts +10 -3
- package/dist/types/generate.d.ts +76 -5
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.js +8 -0
- package/dist/types/memory.d.ts +96 -0
- package/dist/types/memory.js +22 -0
- package/dist/types/middleware.d.ts +27 -0
- package/dist/types/multimodal.d.ts +35 -2
- package/dist/types/music.d.ts +165 -0
- package/dist/types/music.js +20 -0
- package/dist/types/providers.d.ts +284 -3
- package/dist/types/replicate.d.ts +67 -0
- package/dist/types/replicate.js +9 -0
- package/dist/types/safeFetch.d.ts +15 -0
- package/dist/types/safeFetch.js +6 -0
- package/dist/types/stream.d.ts +8 -1
- package/dist/types/tools.d.ts +13 -0
- package/dist/types/video.d.ts +89 -0
- package/dist/types/video.js +14 -0
- package/dist/utils/avatarProcessor.d.ts +68 -0
- package/dist/utils/avatarProcessor.js +171 -0
- package/dist/utils/cloneOptions.d.ts +36 -0
- package/dist/utils/cloneOptions.js +61 -0
- package/dist/utils/lifecycleCallbacks.d.ts +56 -0
- package/dist/utils/lifecycleCallbacks.js +99 -0
- package/dist/utils/lifecycleTimeout.d.ts +25 -0
- package/dist/utils/lifecycleTimeout.js +38 -0
- package/dist/utils/logSanitize.d.ts +49 -0
- package/dist/utils/logSanitize.js +169 -0
- package/dist/utils/loggingFetch.d.ts +29 -0
- package/dist/utils/loggingFetch.js +59 -0
- package/dist/utils/messageBuilder.d.ts +10 -0
- package/dist/utils/messageBuilder.js +83 -30
- package/dist/utils/modelChoices.js +236 -3
- package/dist/utils/modelDetection.d.ts +11 -0
- package/dist/utils/modelDetection.js +27 -0
- package/dist/utils/musicProcessor.d.ts +67 -0
- package/dist/utils/musicProcessor.js +188 -0
- package/dist/utils/optionsConversion.js +3 -2
- package/dist/utils/parameterValidation.js +14 -4
- package/dist/utils/pricing.js +193 -0
- package/dist/utils/providerConfig.d.ts +55 -0
- package/dist/utils/providerConfig.js +224 -0
- package/dist/utils/providerHealth.js +7 -7
- package/dist/utils/safeFetch.d.ts +26 -0
- package/dist/utils/safeFetch.js +82 -0
- package/dist/utils/schemaConversion.d.ts +1 -1
- package/dist/utils/schemaConversion.js +59 -4
- package/dist/utils/sizeGuard.d.ts +34 -0
- package/dist/utils/sizeGuard.js +44 -0
- package/dist/utils/ssrfGuard.d.ts +52 -0
- package/dist/utils/ssrfGuard.js +410 -0
- package/dist/utils/tokenLimits.js +23 -32
- package/dist/utils/videoProcessor.d.ts +60 -0
- package/dist/utils/videoProcessor.js +200 -0
- package/dist/voice/providers/FishAudioTTS.d.ts +27 -0
- package/dist/voice/providers/FishAudioTTS.js +182 -0
- package/dist/workflow/core/ensembleExecutor.js +26 -9
- package/package.json +42 -8
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
2
2
|
import { jsonSchemaToZod } from "json-schema-to-zod";
|
|
3
|
+
import * as zodModule from "zod";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { logger } from "./logger.js";
|
|
6
|
+
// Zod 4 ships a built-in `z.toJSONSchema(...)`. Zod 3 does not — it returned
|
|
7
|
+
// nothing of the sort and we relied entirely on `zod-to-json-schema`. The
|
|
8
|
+
// `zod-to-json-schema` package only understands Zod 3's internal `_def`
|
|
9
|
+
// shape, so feeding it a Zod 4 schema yields an empty `{}` (it silently
|
|
10
|
+
// produces `definitions: { ToolParameters: {} }`). When this happens
|
|
11
|
+
// downstream callers send an empty `responseSchema` to the model and get
|
|
12
|
+
// back arbitrary JSON, which is exactly how the Vertex Structured-Output
|
|
13
|
+
// regressions surfaced. Detect the Zod 4 helper at module load and prefer
|
|
14
|
+
// it for actual Zod schemas.
|
|
15
|
+
// Zod 4 spells the OpenAPI target as "openapi-3.0" (with a dot) while the
|
|
16
|
+
// third-party zod-to-json-schema package uses "openApi3". Internally we use
|
|
17
|
+
// the latter for backwards compatibility with existing call sites; this map
|
|
18
|
+
// translates to the dialect Zod 4 actually accepts. The Zod4Native* types
|
|
19
|
+
// live in src/lib/types/aliases.ts per project rule 2.
|
|
20
|
+
const zodToJsonSchemaV4 = typeof zodModule.toJSONSchema === "function"
|
|
21
|
+
? zodModule.toJSONSchema
|
|
22
|
+
: undefined;
|
|
5
23
|
/**
|
|
6
24
|
* Resolve a deep JSON pointer path within a schema.
|
|
7
25
|
* Handles paths like "#/definitions/ToolParameters/properties/foo/properties/bar"
|
|
@@ -96,7 +114,12 @@ export function inlineJsonSchema(schema, definitions, visited = new Set(), rootS
|
|
|
96
114
|
visited.delete(refPath);
|
|
97
115
|
return inlined;
|
|
98
116
|
}
|
|
99
|
-
|
|
117
|
+
// Unresolved $ref: warn and preserve the original node verbatim. Falling
|
|
118
|
+
// through to the copy loop below would strip the $ref key and silently
|
|
119
|
+
// turn a ref-only node into an empty {}, which broadens validation
|
|
120
|
+
// instead of failing closed.
|
|
121
|
+
logger.warn(`[SCHEMA-INLINE] Could not resolve $ref: ${refPath}`);
|
|
122
|
+
return { ...schema };
|
|
100
123
|
}
|
|
101
124
|
// Create result without $ref and definitions
|
|
102
125
|
const result = {};
|
|
@@ -279,7 +302,12 @@ export function ensureNestedSchemaTypes(schema) {
|
|
|
279
302
|
* 2. AI SDK `jsonSchema()` wrappers (have `.jsonSchema` property) -- extracted directly
|
|
280
303
|
* 3. Plain JSON Schema objects (have `type`/`properties` but no `_def`) -- returned as-is
|
|
281
304
|
*/
|
|
282
|
-
export function convertZodToJsonSchema(zodSchema
|
|
305
|
+
export function convertZodToJsonSchema(zodSchema,
|
|
306
|
+
// Default to JSON Schema draft-07 so non-Vertex consumers (Bedrock, MCP
|
|
307
|
+
// tool registration, etc.) keep their pre-migration dialect. Vertex/Gemini
|
|
308
|
+
// callers opt into "openApi3" explicitly to get `nullable: true` instead
|
|
309
|
+
// of `anyOf: [..., {type: "null"}]`.
|
|
310
|
+
target = "jsonSchema7") {
|
|
283
311
|
const schema = zodSchema;
|
|
284
312
|
if (!schema || typeof schema !== "object") {
|
|
285
313
|
return { type: "object", properties: {} };
|
|
@@ -295,14 +323,41 @@ export function convertZodToJsonSchema(zodSchema) {
|
|
|
295
323
|
if (!isZodSchema(schema)) {
|
|
296
324
|
return ensureNestedSchemaTypes(ensureTypeField(schema));
|
|
297
325
|
}
|
|
298
|
-
// Actual Zod schema —
|
|
326
|
+
// Actual Zod schema — prefer Zod 4's native `z.toJSONSchema` when
|
|
327
|
+
// available (the runtime version of `zod` here is Zod 4), then fall
|
|
328
|
+
// back to `zod-to-json-schema` for Zod 3 schemas that external callers
|
|
329
|
+
// might still pass in.
|
|
330
|
+
//
|
|
331
|
+
// Translate our `target` to Zod 4's native dialect identifier so the
|
|
332
|
+
// openApi3 path emits the OpenAPI 3 schema shape Vertex/Gemini expect
|
|
333
|
+
// (and not the default draft-07 anyOf/null union).
|
|
334
|
+
if (zodToJsonSchemaV4) {
|
|
335
|
+
const nativeTarget = target === "openApi3" ? "openapi-3.0" : "draft-07";
|
|
336
|
+
try {
|
|
337
|
+
const native = zodToJsonSchemaV4(zodSchema, {
|
|
338
|
+
target: nativeTarget,
|
|
339
|
+
});
|
|
340
|
+
// Drop the $schema metadata Vertex/Gemini doesn't need, then walk to
|
|
341
|
+
// backfill any missing nested types (Zod 4's output is already flat
|
|
342
|
+
// — no $defs/$ref by default — but the helper is cheap and matches
|
|
343
|
+
// the Zod 3 path's contract).
|
|
344
|
+
const flat = { ...native };
|
|
345
|
+
delete flat.$schema;
|
|
346
|
+
const inlined = inlineJsonSchema(flat);
|
|
347
|
+
return ensureNestedSchemaTypes(ensureTypeField(inlined));
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
logger.warn("Native z.toJSONSchema failed; falling back to zod-to-json-schema", { error: error instanceof Error ? error.message : String(error) });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// Zod 3 fallback path
|
|
299
354
|
try {
|
|
300
355
|
// Zod 4→3 boundary: zodToJsonSchema types reference Zod 3's ZodSchema via zod/v3.
|
|
301
356
|
// Runtime compatible — cast through unknown at this third-party boundary only.
|
|
302
357
|
const zodV3Schema = zodSchema;
|
|
303
358
|
const jsonSchema = zodToJsonSchema(zodV3Schema, {
|
|
304
359
|
name: "ToolParameters",
|
|
305
|
-
target
|
|
360
|
+
target,
|
|
306
361
|
errorMessages: true,
|
|
307
362
|
});
|
|
308
363
|
// zodToJsonSchema with 'name' produces { $ref: "#/definitions/ToolParameters", definitions: {...} }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Size Guard Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides bounded binary downloads to prevent OOM when fetching generated
|
|
5
|
+
* media from external providers. Applies a Content-Length pre-check and a
|
|
6
|
+
* post-buffer guard so multi-GB responses are rejected before they fully
|
|
7
|
+
* materialise in process memory.
|
|
8
|
+
*
|
|
9
|
+
* @module utils/sizeGuard
|
|
10
|
+
*/
|
|
11
|
+
/** 256 MiB — suitable for video output (MP4). */
|
|
12
|
+
export declare const MAX_VIDEO_BYTES: number;
|
|
13
|
+
/** 50 MiB — suitable for audio output (MP3/WAV). */
|
|
14
|
+
export declare const MAX_AUDIO_BYTES: number;
|
|
15
|
+
/** 25 MiB — suitable for image output (PNG/JPEG/WebP). */
|
|
16
|
+
export declare const MAX_IMAGE_BYTES: number;
|
|
17
|
+
/**
|
|
18
|
+
* Download the body of a {@link Response} into a {@link Buffer}, enforcing an
|
|
19
|
+
* upper-bound on the number of bytes consumed.
|
|
20
|
+
*
|
|
21
|
+
* Two checks are performed:
|
|
22
|
+
* 1. If the response includes a `Content-Length` header that exceeds
|
|
23
|
+
* `maxBytes`, the download is rejected immediately (no data is read).
|
|
24
|
+
* 2. After buffering, the actual buffer size is verified against `maxBytes`.
|
|
25
|
+
* This catches chunked transfers where no `Content-Length` was provided.
|
|
26
|
+
*
|
|
27
|
+
* @param response The fetch {@link Response} to drain.
|
|
28
|
+
* @param maxBytes Maximum number of bytes allowed.
|
|
29
|
+
* @param label Human-readable identifier used in error messages
|
|
30
|
+
* (e.g. "Kling video", "D-ID result").
|
|
31
|
+
* @returns The response body as a {@link Buffer}.
|
|
32
|
+
* @throws {@link Error} when either size check fails.
|
|
33
|
+
*/
|
|
34
|
+
export declare function readBoundedBuffer(response: Response, maxBytes: number, label: string): Promise<Buffer>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Size Guard Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides bounded binary downloads to prevent OOM when fetching generated
|
|
5
|
+
* media from external providers. Applies a Content-Length pre-check and a
|
|
6
|
+
* post-buffer guard so multi-GB responses are rejected before they fully
|
|
7
|
+
* materialise in process memory.
|
|
8
|
+
*
|
|
9
|
+
* @module utils/sizeGuard
|
|
10
|
+
*/
|
|
11
|
+
/** 256 MiB — suitable for video output (MP4). */
|
|
12
|
+
export const MAX_VIDEO_BYTES = 256 * 1024 * 1024;
|
|
13
|
+
/** 50 MiB — suitable for audio output (MP3/WAV). */
|
|
14
|
+
export const MAX_AUDIO_BYTES = 50 * 1024 * 1024;
|
|
15
|
+
/** 25 MiB — suitable for image output (PNG/JPEG/WebP). */
|
|
16
|
+
export const MAX_IMAGE_BYTES = 25 * 1024 * 1024;
|
|
17
|
+
/**
|
|
18
|
+
* Download the body of a {@link Response} into a {@link Buffer}, enforcing an
|
|
19
|
+
* upper-bound on the number of bytes consumed.
|
|
20
|
+
*
|
|
21
|
+
* Two checks are performed:
|
|
22
|
+
* 1. If the response includes a `Content-Length` header that exceeds
|
|
23
|
+
* `maxBytes`, the download is rejected immediately (no data is read).
|
|
24
|
+
* 2. After buffering, the actual buffer size is verified against `maxBytes`.
|
|
25
|
+
* This catches chunked transfers where no `Content-Length` was provided.
|
|
26
|
+
*
|
|
27
|
+
* @param response The fetch {@link Response} to drain.
|
|
28
|
+
* @param maxBytes Maximum number of bytes allowed.
|
|
29
|
+
* @param label Human-readable identifier used in error messages
|
|
30
|
+
* (e.g. "Kling video", "D-ID result").
|
|
31
|
+
* @returns The response body as a {@link Buffer}.
|
|
32
|
+
* @throws {@link Error} when either size check fails.
|
|
33
|
+
*/
|
|
34
|
+
export async function readBoundedBuffer(response, maxBytes, label) {
|
|
35
|
+
const contentLength = parseInt(response.headers.get("content-length") ?? "0", 10);
|
|
36
|
+
if (contentLength > 0 && contentLength > maxBytes) {
|
|
37
|
+
throw new Error(`${label} download too large: ${contentLength} bytes (max ${maxBytes})`);
|
|
38
|
+
}
|
|
39
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
40
|
+
if (buffer.length > maxBytes) {
|
|
41
|
+
throw new Error(`${label} download exceeded size cap after fetch: ${buffer.length} bytes (max ${maxBytes})`);
|
|
42
|
+
}
|
|
43
|
+
return buffer;
|
|
44
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF Guard — Safe URL Validation Utility
|
|
3
|
+
*
|
|
4
|
+
* Prevents Server-Side Request Forgery by:
|
|
5
|
+
* 1. Enforcing HTTPS-only (no plain HTTP).
|
|
6
|
+
* 2. Normalising encoded IPv4 forms (octal, hex, decimal integer, IPv4-mapped IPv6)
|
|
7
|
+
* to canonical dotted-decimal before rangechecking.
|
|
8
|
+
* 3. Resolving the hostname for **both** A and AAAA families and rejecting
|
|
9
|
+
* requests to RFC 1918 private ranges, loopback, link-local, CGNAT,
|
|
10
|
+
* IPv6 link-local/ULA, and cloud metadata endpoints
|
|
11
|
+
* (AWS / GCP / Azure / Alibaba).
|
|
12
|
+
* 4. Re-throwing on DNS failure rather than silently allowing the request.
|
|
13
|
+
*
|
|
14
|
+
* **DNS rebinding residual race:** `assertSafeUrl` validates the IP at the
|
|
15
|
+
* moment of the lookup. If the resolver returns a public IP here and a private
|
|
16
|
+
* IP at the actual `fetch()` call, the guard is bypassed. To eliminate the
|
|
17
|
+
* race, use the companion `safeDownload` helper in `safeFetch.ts` which pins
|
|
18
|
+
* the resolved IP onto the request via an undici Agent dispatcher.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* await assertSafeUrl(url);
|
|
22
|
+
* // ... or, for actual downloads: ...
|
|
23
|
+
* await safeDownload(url, { maxBytes, label });
|
|
24
|
+
*
|
|
25
|
+
* @module utils/ssrfGuard
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Assert that `url` is safe to fetch server-side.
|
|
29
|
+
*
|
|
30
|
+
* @throws {Error} when the URL is non-HTTPS, parses as a blocked IP literal,
|
|
31
|
+
* or resolves (A or AAAA) to a blocked IP. **Also throws on DNS lookup
|
|
32
|
+
* failure** (the previous behaviour of silently allowing was a bypass —
|
|
33
|
+
* an attacker-controlled resolver could force NXDOMAIN here and a private
|
|
34
|
+
* IP at the actual fetch).
|
|
35
|
+
*/
|
|
36
|
+
export declare function assertSafeUrl(url: string): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Validate `url` and return the resolved IP that should be used for the
|
|
39
|
+
* actual fetch (companion to `safeFetch.ts:safeDownload`).
|
|
40
|
+
*
|
|
41
|
+
* For IP-literal hosts, returns the normalised IP and family. For hostnames,
|
|
42
|
+
* returns the first acceptable IP from the resolver. Same throw semantics as
|
|
43
|
+
* {@link assertSafeUrl}.
|
|
44
|
+
*
|
|
45
|
+
* This is the canonical entry point for binary downloads where DNS-rebinding
|
|
46
|
+
* pinning matters — see `safeFetch.ts`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function validateAndResolveUrl(url: string): Promise<{
|
|
49
|
+
url: string;
|
|
50
|
+
ip: string;
|
|
51
|
+
family: 4 | 6;
|
|
52
|
+
}>;
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF Guard — Safe URL Validation Utility
|
|
3
|
+
*
|
|
4
|
+
* Prevents Server-Side Request Forgery by:
|
|
5
|
+
* 1. Enforcing HTTPS-only (no plain HTTP).
|
|
6
|
+
* 2. Normalising encoded IPv4 forms (octal, hex, decimal integer, IPv4-mapped IPv6)
|
|
7
|
+
* to canonical dotted-decimal before rangechecking.
|
|
8
|
+
* 3. Resolving the hostname for **both** A and AAAA families and rejecting
|
|
9
|
+
* requests to RFC 1918 private ranges, loopback, link-local, CGNAT,
|
|
10
|
+
* IPv6 link-local/ULA, and cloud metadata endpoints
|
|
11
|
+
* (AWS / GCP / Azure / Alibaba).
|
|
12
|
+
* 4. Re-throwing on DNS failure rather than silently allowing the request.
|
|
13
|
+
*
|
|
14
|
+
* **DNS rebinding residual race:** `assertSafeUrl` validates the IP at the
|
|
15
|
+
* moment of the lookup. If the resolver returns a public IP here and a private
|
|
16
|
+
* IP at the actual `fetch()` call, the guard is bypassed. To eliminate the
|
|
17
|
+
* race, use the companion `safeDownload` helper in `safeFetch.ts` which pins
|
|
18
|
+
* the resolved IP onto the request via an undici Agent dispatcher.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* await assertSafeUrl(url);
|
|
22
|
+
* // ... or, for actual downloads: ...
|
|
23
|
+
* await safeDownload(url, { maxBytes, label });
|
|
24
|
+
*
|
|
25
|
+
* @module utils/ssrfGuard
|
|
26
|
+
*/
|
|
27
|
+
import { lookup } from "node:dns/promises";
|
|
28
|
+
import { isIP } from "node:net";
|
|
29
|
+
/**
|
|
30
|
+
* Blocked IPv4 CIDRs.
|
|
31
|
+
*
|
|
32
|
+
* Each entry is a `[network, prefix]` pair. Membership is computed by
|
|
33
|
+
* bitwise comparison of the 32-bit address vs the masked network.
|
|
34
|
+
*/
|
|
35
|
+
const BLOCKED_V4_CIDRS = [
|
|
36
|
+
["0.0.0.0", 8], // "this network"
|
|
37
|
+
["10.0.0.0", 8], // RFC 1918
|
|
38
|
+
["100.64.0.0", 10], // CGNAT (RFC 6598)
|
|
39
|
+
["127.0.0.0", 8], // loopback
|
|
40
|
+
["169.254.0.0", 16], // link-local (AWS/GCP/Azure metadata + APIPA)
|
|
41
|
+
["172.16.0.0", 12], // RFC 1918
|
|
42
|
+
["192.0.0.0", 24], // protocol assignments
|
|
43
|
+
["192.168.0.0", 16], // RFC 1918
|
|
44
|
+
["198.18.0.0", 15], // benchmarking
|
|
45
|
+
["100.100.100.200", 32], // Alibaba Cloud metadata (NOT in 100.64/10 CGNAT)
|
|
46
|
+
["224.0.0.0", 4], // multicast
|
|
47
|
+
["240.0.0.0", 4], // reserved
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* Blocked IPv6 prefixes.
|
|
51
|
+
*
|
|
52
|
+
* Compared by lowercase prefix match on the expanded address form.
|
|
53
|
+
* (`expandIPv6` normalizes `::1` to `0000:0000:...:0001` for unambiguous
|
|
54
|
+
* prefix matching.)
|
|
55
|
+
*/
|
|
56
|
+
const BLOCKED_V6_PREFIXES = [
|
|
57
|
+
"0000:0000:0000:0000:0000:0000:0000:0000", // :: (unspecified)
|
|
58
|
+
"0000:0000:0000:0000:0000:0000:0000:0001", // ::1 (loopback)
|
|
59
|
+
"fc", // fc00::/7 unique-local (covers fc and fd prefixes)
|
|
60
|
+
"fd", // fd00::/8
|
|
61
|
+
"fe8", // fe80::/10 link-local (covers fe8/fe9/fea/feb)
|
|
62
|
+
"fe9",
|
|
63
|
+
"fea",
|
|
64
|
+
"feb",
|
|
65
|
+
];
|
|
66
|
+
function parseOctet(s) {
|
|
67
|
+
if (s.length === 0) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (/^0x[0-9a-f]+$/i.test(s)) {
|
|
71
|
+
return parseInt(s.slice(2), 16);
|
|
72
|
+
}
|
|
73
|
+
// Plain "0" is valid decimal zero; leading-zero forms (`0177`) are octal
|
|
74
|
+
if (s.length > 1 && s.startsWith("0") && /^0[0-7]+$/.test(s)) {
|
|
75
|
+
return parseInt(s.slice(1), 8);
|
|
76
|
+
}
|
|
77
|
+
if (/^\d+$/.test(s)) {
|
|
78
|
+
return parseInt(s, 10);
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Normalize any IPv4-like host string to canonical dotted-decimal form, or
|
|
84
|
+
* return `null` if it's not parseable as IPv4.
|
|
85
|
+
*
|
|
86
|
+
* Handles:
|
|
87
|
+
* - 127.0.0.1 (canonical)
|
|
88
|
+
* - 0177.0.0.1 (octal octets)
|
|
89
|
+
* - 0x7f.0.0.1 (hex octets)
|
|
90
|
+
* - 0x7f000001 (hex integer)
|
|
91
|
+
* - 2130706433 (decimal integer)
|
|
92
|
+
* - 0177.0.0.1 (mixed encodings)
|
|
93
|
+
*/
|
|
94
|
+
function normalizeIPv4(host) {
|
|
95
|
+
if (host.length === 0) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const parts = host.split(".");
|
|
99
|
+
if (parts.length === 4) {
|
|
100
|
+
const octets = parts.map(parseOctet);
|
|
101
|
+
if (octets.some((o) => o === null || o < 0 || o > 255)) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return octets.join(".");
|
|
105
|
+
}
|
|
106
|
+
// Single integer form: 2130706433 or 0x7f000001
|
|
107
|
+
if (parts.length === 1) {
|
|
108
|
+
let n;
|
|
109
|
+
if (/^0x[0-9a-f]+$/i.test(host)) {
|
|
110
|
+
n = parseInt(host.slice(2), 16);
|
|
111
|
+
}
|
|
112
|
+
else if (/^\d+$/.test(host)) {
|
|
113
|
+
n = parseInt(host, 10);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
if (Number.isNaN(n) || n < 0 || n > 0xffffffff) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return [
|
|
122
|
+
(n >>> 24) & 0xff,
|
|
123
|
+
(n >>> 16) & 0xff,
|
|
124
|
+
(n >>> 8) & 0xff,
|
|
125
|
+
n & 0xff,
|
|
126
|
+
].join(".");
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Expand a compressed IPv6 address (`::1`) to full 8-group form
|
|
132
|
+
* (`0000:0000:0000:0000:0000:0000:0000:0001`) for unambiguous prefix matching.
|
|
133
|
+
*
|
|
134
|
+
* Returns the expanded lowercased string, or `null` if `host` isn't IPv6.
|
|
135
|
+
*/
|
|
136
|
+
function expandIPv6(host) {
|
|
137
|
+
if (isIP(host) !== 6) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
// Handle IPv4-mapped IPv6: ::ffff:127.0.0.1 → expand the IPv4 part to two groups
|
|
141
|
+
const v4MappedMatch = host.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
|
|
142
|
+
let groups;
|
|
143
|
+
if (v4MappedMatch) {
|
|
144
|
+
const v4 = normalizeIPv4(v4MappedMatch[1]);
|
|
145
|
+
if (!v4) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const v4Octets = v4.split(".").map((n) => parseInt(n, 10));
|
|
149
|
+
const high = ((v4Octets[0] << 8) | v4Octets[1]).toString(16);
|
|
150
|
+
const low = ((v4Octets[2] << 8) | v4Octets[3]).toString(16);
|
|
151
|
+
groups = ["0", "0", "0", "0", "0", "ffff", high, low];
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const [head, tail = ""] = host.split("::");
|
|
155
|
+
const headParts = head ? head.split(":") : [];
|
|
156
|
+
const tailParts = tail ? tail.split(":") : [];
|
|
157
|
+
const missing = 8 - headParts.length - tailParts.length;
|
|
158
|
+
if (missing < 0) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
groups = [...headParts, ...Array(missing).fill("0"), ...tailParts];
|
|
162
|
+
}
|
|
163
|
+
if (groups.length !== 8) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
return groups.map((g) => g.toLowerCase().padStart(4, "0")).join(":");
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* If `host` is an IPv4-mapped IPv6 address, return the embedded IPv4 in
|
|
170
|
+
* canonical dotted-decimal form, or `null` otherwise.
|
|
171
|
+
*
|
|
172
|
+
* Handles both forms:
|
|
173
|
+
* - dotted-decimal IPv4 part: `::ffff:127.0.0.1`
|
|
174
|
+
* - hex-encoded IPv4 part: `::ffff:7f00:1` / `::ffff:7f00:0001`
|
|
175
|
+
*
|
|
176
|
+
* Node's `URL` parser normalises bracketed `::ffff:127.0.0.1` to
|
|
177
|
+
* `[::ffff:7f00:1]`, so the hex form is the one we actually receive after
|
|
178
|
+
* `URL.hostname` + bracket stripping. Both paths must be covered.
|
|
179
|
+
*/
|
|
180
|
+
function extractIPv4FromMapped(host) {
|
|
181
|
+
// Form 1: `::ffff:127.0.0.1`
|
|
182
|
+
const dottedMatch = host.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
|
|
183
|
+
if (dottedMatch) {
|
|
184
|
+
return normalizeIPv4(dottedMatch[1]);
|
|
185
|
+
}
|
|
186
|
+
// Form 2: `::ffff:7f00:1` (two hex groups, optionally zero-padded)
|
|
187
|
+
const hexMatch = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
|
|
188
|
+
if (hexMatch) {
|
|
189
|
+
const high = parseInt(hexMatch[1], 16);
|
|
190
|
+
const low = parseInt(hexMatch[2], 16);
|
|
191
|
+
if (Number.isNaN(high) ||
|
|
192
|
+
Number.isNaN(low) ||
|
|
193
|
+
high > 0xffff ||
|
|
194
|
+
low > 0xffff) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
return [
|
|
198
|
+
(high >> 8) & 0xff,
|
|
199
|
+
high & 0xff,
|
|
200
|
+
(low >> 8) & 0xff,
|
|
201
|
+
low & 0xff,
|
|
202
|
+
].join(".");
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
function ipv4ToInt(ip) {
|
|
207
|
+
const [a, b, c, d] = ip.split(".").map((n) => parseInt(n, 10));
|
|
208
|
+
return ((a << 24) | (b << 16) | (c << 8) | d) >>> 0;
|
|
209
|
+
}
|
|
210
|
+
function isBlockedIPv4(ip) {
|
|
211
|
+
const ipInt = ipv4ToInt(ip);
|
|
212
|
+
for (const [network, prefix] of BLOCKED_V4_CIDRS) {
|
|
213
|
+
const netInt = ipv4ToInt(network);
|
|
214
|
+
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
|
|
215
|
+
if ((ipInt & mask) === (netInt & mask)) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
function isBlockedIPv6(expanded) {
|
|
222
|
+
return BLOCKED_V6_PREFIXES.some((prefix) => {
|
|
223
|
+
if (prefix.length === 39) {
|
|
224
|
+
// full-form exact match
|
|
225
|
+
return expanded === prefix;
|
|
226
|
+
}
|
|
227
|
+
return expanded.startsWith(prefix);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Strip the IPv6 brackets that `URL.hostname` returns for IPv6 hosts
|
|
232
|
+
* (Node behaviour varies — sometimes `[::1]`, sometimes `::1`).
|
|
233
|
+
*/
|
|
234
|
+
function stripBrackets(host) {
|
|
235
|
+
if (host.startsWith("[") && host.endsWith("]")) {
|
|
236
|
+
return host.slice(1, -1);
|
|
237
|
+
}
|
|
238
|
+
return host;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Internal check: given a host string (already bracket-stripped, lowercased),
|
|
242
|
+
* return a reject reason or null if safe.
|
|
243
|
+
*
|
|
244
|
+
* Detects IP literals via every encoded form. Does NOT do DNS — that's the
|
|
245
|
+
* caller's job.
|
|
246
|
+
*/
|
|
247
|
+
function checkHostLiteral(host) {
|
|
248
|
+
// IPv4 (including encoded forms)
|
|
249
|
+
const v4 = normalizeIPv4(host);
|
|
250
|
+
if (v4) {
|
|
251
|
+
if (isBlockedIPv4(v4)) {
|
|
252
|
+
return `IPv4 ${host} → ${v4} is in a blocked range`;
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
// IPv6 (including IPv4-mapped)
|
|
257
|
+
if (host.includes(":")) {
|
|
258
|
+
// First, check IPv4-mapped: convert and re-check via v4 path
|
|
259
|
+
const v4FromMapped = extractIPv4FromMapped(host);
|
|
260
|
+
if (v4FromMapped) {
|
|
261
|
+
if (isBlockedIPv4(v4FromMapped)) {
|
|
262
|
+
return `IPv4-mapped IPv6 ${host} → ${v4FromMapped} is in a blocked range`;
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
const expanded = expandIPv6(host);
|
|
267
|
+
if (!expanded) {
|
|
268
|
+
return `IPv6 ${host} could not be parsed`;
|
|
269
|
+
}
|
|
270
|
+
if (isBlockedIPv6(expanded)) {
|
|
271
|
+
return `IPv6 ${host} is in a blocked range`;
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
// Not an IP literal — caller should fall through to DNS resolution
|
|
276
|
+
return "not-an-ip";
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Assert that `url` is safe to fetch server-side.
|
|
280
|
+
*
|
|
281
|
+
* @throws {Error} when the URL is non-HTTPS, parses as a blocked IP literal,
|
|
282
|
+
* or resolves (A or AAAA) to a blocked IP. **Also throws on DNS lookup
|
|
283
|
+
* failure** (the previous behaviour of silently allowing was a bypass —
|
|
284
|
+
* an attacker-controlled resolver could force NXDOMAIN here and a private
|
|
285
|
+
* IP at the actual fetch).
|
|
286
|
+
*/
|
|
287
|
+
export async function assertSafeUrl(url) {
|
|
288
|
+
let parsed;
|
|
289
|
+
try {
|
|
290
|
+
parsed = new URL(url);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
throw new Error(`Invalid URL: "${url}"`);
|
|
294
|
+
}
|
|
295
|
+
if (parsed.protocol !== "https:") {
|
|
296
|
+
throw new Error(`Only HTTPS URLs are permitted; got "${parsed.protocol}//" in "${url}"`);
|
|
297
|
+
}
|
|
298
|
+
const host = stripBrackets(parsed.hostname).toLowerCase();
|
|
299
|
+
// First, try as an IP literal (covers encoded forms + IPv4-mapped IPv6).
|
|
300
|
+
const literalCheck = checkHostLiteral(host);
|
|
301
|
+
if (literalCheck === null) {
|
|
302
|
+
return; // routable IP literal — safe
|
|
303
|
+
}
|
|
304
|
+
if (literalCheck !== "not-an-ip") {
|
|
305
|
+
throw new Error(`URL "${url}" rejected: ${literalCheck}`);
|
|
306
|
+
}
|
|
307
|
+
// Hostname — resolve BOTH A and AAAA. Reject if either family yields a
|
|
308
|
+
// blocked address (closes off the "publish AAAA public, A private" attack).
|
|
309
|
+
const [a, aaaa] = await Promise.allSettled([
|
|
310
|
+
lookup(host, { family: 4, all: true }),
|
|
311
|
+
lookup(host, { family: 6, all: true }),
|
|
312
|
+
]);
|
|
313
|
+
const v4Addresses = [];
|
|
314
|
+
const v6Addresses = [];
|
|
315
|
+
let hadAnySuccess = false;
|
|
316
|
+
if (a.status === "fulfilled") {
|
|
317
|
+
hadAnySuccess = true;
|
|
318
|
+
for (const entry of a.value) {
|
|
319
|
+
v4Addresses.push(entry.address);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (aaaa.status === "fulfilled") {
|
|
323
|
+
hadAnySuccess = true;
|
|
324
|
+
for (const entry of aaaa.value) {
|
|
325
|
+
v6Addresses.push(entry.address);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (!hadAnySuccess) {
|
|
329
|
+
// BOTH lookups failed — the host doesn't resolve at all. Re-throw with
|
|
330
|
+
// a clear message rather than silently allowing the fetch (the prior
|
|
331
|
+
// behaviour, which is the DNS-rebinding bypass).
|
|
332
|
+
const aErr = a.status === "rejected"
|
|
333
|
+
? a.reason instanceof Error
|
|
334
|
+
? a.reason.message
|
|
335
|
+
: String(a.reason)
|
|
336
|
+
: "ok";
|
|
337
|
+
const aaaaErr = aaaa.status === "rejected"
|
|
338
|
+
? aaaa.reason instanceof Error
|
|
339
|
+
? aaaa.reason.message
|
|
340
|
+
: String(aaaa.reason)
|
|
341
|
+
: "ok";
|
|
342
|
+
throw new Error(`URL "${url}" rejected: hostname ${host} could not be resolved (A: ${aErr}; AAAA: ${aaaaErr})`);
|
|
343
|
+
}
|
|
344
|
+
for (const addr of v4Addresses) {
|
|
345
|
+
if (isBlockedIPv4(addr)) {
|
|
346
|
+
throw new Error(`URL "${url}" rejected: hostname ${host} resolves to ${addr} (IPv4 in blocked range)`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
for (const addr of v6Addresses) {
|
|
350
|
+
// Re-use the literal check pipeline for IPv6 so IPv4-mapped resolved
|
|
351
|
+
// addresses are caught.
|
|
352
|
+
const reason = checkHostLiteral(addr.toLowerCase());
|
|
353
|
+
if (reason && reason !== "not-an-ip") {
|
|
354
|
+
throw new Error(`URL "${url}" rejected: hostname ${host} resolves to ${addr} (IPv6 ${reason})`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Validate `url` and return the resolved IP that should be used for the
|
|
360
|
+
* actual fetch (companion to `safeFetch.ts:safeDownload`).
|
|
361
|
+
*
|
|
362
|
+
* For IP-literal hosts, returns the normalised IP and family. For hostnames,
|
|
363
|
+
* returns the first acceptable IP from the resolver. Same throw semantics as
|
|
364
|
+
* {@link assertSafeUrl}.
|
|
365
|
+
*
|
|
366
|
+
* This is the canonical entry point for binary downloads where DNS-rebinding
|
|
367
|
+
* pinning matters — see `safeFetch.ts`.
|
|
368
|
+
*/
|
|
369
|
+
export async function validateAndResolveUrl(url) {
|
|
370
|
+
let parsed;
|
|
371
|
+
try {
|
|
372
|
+
parsed = new URL(url);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
throw new Error(`Invalid URL: "${url}"`);
|
|
376
|
+
}
|
|
377
|
+
if (parsed.protocol !== "https:") {
|
|
378
|
+
throw new Error(`Only HTTPS URLs are permitted; got "${parsed.protocol}//" in "${url}"`);
|
|
379
|
+
}
|
|
380
|
+
const host = stripBrackets(parsed.hostname).toLowerCase();
|
|
381
|
+
// IP literal — normalise + check, return canonical form
|
|
382
|
+
const v4 = normalizeIPv4(host);
|
|
383
|
+
if (v4) {
|
|
384
|
+
if (isBlockedIPv4(v4)) {
|
|
385
|
+
throw new Error(`URL "${url}" rejected: IPv4 ${host} → ${v4} is in a blocked range`);
|
|
386
|
+
}
|
|
387
|
+
return { url, ip: v4, family: 4 };
|
|
388
|
+
}
|
|
389
|
+
if (host.includes(":")) {
|
|
390
|
+
const v4FromMapped = extractIPv4FromMapped(host);
|
|
391
|
+
if (v4FromMapped) {
|
|
392
|
+
if (isBlockedIPv4(v4FromMapped)) {
|
|
393
|
+
throw new Error(`URL "${url}" rejected: IPv4-mapped IPv6 ${host} → ${v4FromMapped} is in a blocked range`);
|
|
394
|
+
}
|
|
395
|
+
return { url, ip: v4FromMapped, family: 4 };
|
|
396
|
+
}
|
|
397
|
+
const expanded = expandIPv6(host);
|
|
398
|
+
if (!expanded) {
|
|
399
|
+
throw new Error(`URL "${url}" rejected: IPv6 ${host} could not be parsed`);
|
|
400
|
+
}
|
|
401
|
+
if (isBlockedIPv6(expanded)) {
|
|
402
|
+
throw new Error(`URL "${url}" rejected: IPv6 ${host} is in a blocked range`);
|
|
403
|
+
}
|
|
404
|
+
return { url, ip: host, family: 6 };
|
|
405
|
+
}
|
|
406
|
+
// Hostname — resolve and pick a safe address
|
|
407
|
+
await assertSafeUrl(url);
|
|
408
|
+
const result = await lookup(host);
|
|
409
|
+
return { url, ip: result.address, family: result.family };
|
|
410
|
+
}
|