@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.
Files changed (358) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +18 -17
  3. package/dist/adapters/providerImageAdapter.js +29 -1
  4. package/dist/adapters/replicate/auth.d.ts +19 -0
  5. package/dist/adapters/replicate/auth.js +32 -0
  6. package/dist/adapters/replicate/predictionLifecycle.d.ts +46 -0
  7. package/dist/adapters/replicate/predictionLifecycle.js +283 -0
  8. package/dist/adapters/video/klingVideoHandler.d.ts +37 -0
  9. package/dist/adapters/video/klingVideoHandler.js +305 -0
  10. package/dist/adapters/video/replicateVideoHandler.d.ts +29 -0
  11. package/dist/adapters/video/replicateVideoHandler.js +157 -0
  12. package/dist/adapters/video/runwayVideoHandler.d.ts +32 -0
  13. package/dist/adapters/video/runwayVideoHandler.js +316 -0
  14. package/dist/adapters/video/vertexVideoHandler.d.ts +19 -1
  15. package/dist/adapters/video/vertexVideoHandler.js +42 -11
  16. package/dist/autoresearch/runner.js +8 -2
  17. package/dist/avatar/index.d.ts +13 -0
  18. package/dist/avatar/index.js +13 -0
  19. package/dist/avatar/providers/DIDAvatar.d.ts +49 -0
  20. package/dist/avatar/providers/DIDAvatar.js +501 -0
  21. package/dist/avatar/providers/HeyGenAvatar.d.ts +30 -0
  22. package/dist/avatar/providers/HeyGenAvatar.js +337 -0
  23. package/dist/avatar/providers/ReplicateAvatar.d.ts +36 -0
  24. package/dist/avatar/providers/ReplicateAvatar.js +267 -0
  25. package/dist/browser/neurolink.min.js +573 -554
  26. package/dist/cli/commands/mcp.js +29 -0
  27. package/dist/cli/commands/proxy.js +24 -5
  28. package/dist/cli/factories/commandFactory.d.ts +25 -1
  29. package/dist/cli/factories/commandFactory.js +341 -63
  30. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  31. package/dist/cli/loop/optionsSchema.js +12 -0
  32. package/dist/constants/contextWindows.js +101 -0
  33. package/dist/constants/enums.d.ts +273 -2
  34. package/dist/constants/enums.js +290 -1
  35. package/dist/constants/videoErrors.d.ts +4 -0
  36. package/dist/constants/videoErrors.js +4 -0
  37. package/dist/core/baseProvider.d.ts +23 -3
  38. package/dist/core/baseProvider.js +217 -11
  39. package/dist/core/constants.d.ts +11 -0
  40. package/dist/core/constants.js +69 -1
  41. package/dist/core/modules/MessageBuilder.js +20 -0
  42. package/dist/core/redisConversationMemoryManager.js +6 -0
  43. package/dist/evaluation/index.d.ts +2 -0
  44. package/dist/evaluation/index.js +4 -0
  45. package/dist/factories/providerFactory.js +7 -1
  46. package/dist/factories/providerRegistry.js +203 -2
  47. package/dist/features/ppt/contentPlanner.js +42 -14
  48. package/dist/index.d.ts +9 -1
  49. package/dist/index.js +16 -1
  50. package/dist/lib/adapters/providerImageAdapter.js +29 -1
  51. package/dist/lib/adapters/replicate/auth.d.ts +19 -0
  52. package/dist/lib/adapters/replicate/auth.js +33 -0
  53. package/dist/lib/adapters/replicate/predictionLifecycle.d.ts +46 -0
  54. package/dist/lib/adapters/replicate/predictionLifecycle.js +284 -0
  55. package/dist/lib/adapters/video/klingVideoHandler.d.ts +37 -0
  56. package/dist/lib/adapters/video/klingVideoHandler.js +306 -0
  57. package/dist/lib/adapters/video/replicateVideoHandler.d.ts +29 -0
  58. package/dist/lib/adapters/video/replicateVideoHandler.js +158 -0
  59. package/dist/lib/adapters/video/runwayVideoHandler.d.ts +32 -0
  60. package/dist/lib/adapters/video/runwayVideoHandler.js +317 -0
  61. package/dist/lib/adapters/video/vertexVideoHandler.d.ts +19 -1
  62. package/dist/lib/adapters/video/vertexVideoHandler.js +42 -11
  63. package/dist/lib/autoresearch/runner.js +8 -2
  64. package/dist/lib/avatar/index.d.ts +13 -0
  65. package/dist/lib/avatar/index.js +14 -0
  66. package/dist/lib/avatar/providers/DIDAvatar.d.ts +49 -0
  67. package/dist/lib/avatar/providers/DIDAvatar.js +502 -0
  68. package/dist/lib/avatar/providers/HeyGenAvatar.d.ts +30 -0
  69. package/dist/lib/avatar/providers/HeyGenAvatar.js +338 -0
  70. package/dist/lib/avatar/providers/ReplicateAvatar.d.ts +36 -0
  71. package/dist/lib/avatar/providers/ReplicateAvatar.js +268 -0
  72. package/dist/lib/constants/contextWindows.js +101 -0
  73. package/dist/lib/constants/enums.d.ts +273 -2
  74. package/dist/lib/constants/enums.js +290 -1
  75. package/dist/lib/constants/videoErrors.d.ts +4 -0
  76. package/dist/lib/constants/videoErrors.js +4 -0
  77. package/dist/lib/core/baseProvider.d.ts +23 -3
  78. package/dist/lib/core/baseProvider.js +217 -11
  79. package/dist/lib/core/constants.d.ts +11 -0
  80. package/dist/lib/core/constants.js +69 -1
  81. package/dist/lib/core/modules/MessageBuilder.js +20 -0
  82. package/dist/lib/core/redisConversationMemoryManager.js +6 -0
  83. package/dist/lib/evaluation/index.d.ts +2 -0
  84. package/dist/lib/evaluation/index.js +4 -0
  85. package/dist/lib/factories/providerFactory.js +7 -1
  86. package/dist/lib/factories/providerRegistry.js +203 -2
  87. package/dist/lib/features/ppt/contentPlanner.js +42 -14
  88. package/dist/lib/index.d.ts +9 -1
  89. package/dist/lib/index.js +16 -1
  90. package/dist/lib/memory/hippocampusInitializer.d.ts +2 -2
  91. package/dist/lib/memory/hippocampusInitializer.js +32 -2
  92. package/dist/lib/middleware/builtin/lifecycle.js +52 -51
  93. package/dist/lib/music/index.d.ts +13 -0
  94. package/dist/lib/music/index.js +14 -0
  95. package/dist/lib/music/providers/BeatovenMusic.d.ts +31 -0
  96. package/dist/lib/music/providers/BeatovenMusic.js +334 -0
  97. package/dist/lib/music/providers/ElevenLabsMusic.d.ts +30 -0
  98. package/dist/lib/music/providers/ElevenLabsMusic.js +169 -0
  99. package/dist/lib/music/providers/LyriaMusic.d.ts +29 -0
  100. package/dist/lib/music/providers/LyriaMusic.js +173 -0
  101. package/dist/lib/music/providers/ReplicateMusic.d.ts +31 -0
  102. package/dist/lib/music/providers/ReplicateMusic.js +262 -0
  103. package/dist/lib/neurolink.d.ts +30 -0
  104. package/dist/lib/neurolink.js +342 -49
  105. package/dist/lib/providers/amazonBedrock.d.ts +10 -0
  106. package/dist/lib/providers/amazonBedrock.js +94 -39
  107. package/dist/lib/providers/anthropic.js +55 -7
  108. package/dist/lib/providers/anthropicBaseProvider.js +1 -1
  109. package/dist/lib/providers/azureOpenai.js +66 -17
  110. package/dist/lib/providers/cloudflare.d.ts +35 -0
  111. package/dist/lib/providers/cloudflare.js +174 -0
  112. package/dist/lib/providers/cohere.d.ts +52 -0
  113. package/dist/lib/providers/cohere.js +253 -0
  114. package/dist/lib/providers/deepseek.js +72 -17
  115. package/dist/lib/providers/fireworks.d.ts +33 -0
  116. package/dist/lib/providers/fireworks.js +164 -0
  117. package/dist/lib/providers/googleAiStudio.d.ts +11 -3
  118. package/dist/lib/providers/googleAiStudio.js +336 -344
  119. package/dist/lib/providers/googleNativeGemini3.d.ts +107 -2
  120. package/dist/lib/providers/googleNativeGemini3.js +381 -25
  121. package/dist/lib/providers/googleVertex.d.ts +116 -129
  122. package/dist/lib/providers/googleVertex.js +3002 -1988
  123. package/dist/lib/providers/groq.d.ts +33 -0
  124. package/dist/lib/providers/groq.js +181 -0
  125. package/dist/lib/providers/huggingFace.js +9 -8
  126. package/dist/lib/providers/ideogram.d.ts +34 -0
  127. package/dist/lib/providers/ideogram.js +184 -0
  128. package/dist/lib/providers/index.d.ts +13 -0
  129. package/dist/lib/providers/index.js +13 -0
  130. package/dist/lib/providers/jina.d.ts +59 -0
  131. package/dist/lib/providers/jina.js +218 -0
  132. package/dist/lib/providers/llamaCpp.js +14 -46
  133. package/dist/lib/providers/lmStudio.js +14 -47
  134. package/dist/lib/providers/mistral.js +7 -7
  135. package/dist/lib/providers/nvidiaNim.js +160 -19
  136. package/dist/lib/providers/ollama.js +7 -7
  137. package/dist/lib/providers/openAI.d.ts +22 -1
  138. package/dist/lib/providers/openAI.js +181 -0
  139. package/dist/lib/providers/openRouter.js +38 -22
  140. package/dist/lib/providers/openaiCompatible.js +9 -8
  141. package/dist/lib/providers/perplexity.d.ts +33 -0
  142. package/dist/lib/providers/perplexity.js +179 -0
  143. package/dist/lib/providers/recraft.d.ts +34 -0
  144. package/dist/lib/providers/recraft.js +197 -0
  145. package/dist/lib/providers/replicate.d.ts +75 -0
  146. package/dist/lib/providers/replicate.js +403 -0
  147. package/dist/lib/providers/stability.d.ts +37 -0
  148. package/dist/lib/providers/stability.js +191 -0
  149. package/dist/lib/providers/togetherAi.d.ts +33 -0
  150. package/dist/lib/providers/togetherAi.js +176 -0
  151. package/dist/lib/providers/voyage.d.ts +47 -0
  152. package/dist/lib/providers/voyage.js +177 -0
  153. package/dist/lib/providers/xai.d.ts +33 -0
  154. package/dist/lib/providers/xai.js +172 -0
  155. package/dist/lib/telemetry/index.d.ts +1 -1
  156. package/dist/lib/telemetry/index.js +1 -1
  157. package/dist/lib/telemetry/tracers.d.ts +19 -0
  158. package/dist/lib/telemetry/tracers.js +19 -0
  159. package/dist/lib/telemetry/withSpan.d.ts +35 -0
  160. package/dist/lib/telemetry/withSpan.js +103 -0
  161. package/dist/lib/types/aliases.d.ts +14 -0
  162. package/dist/lib/types/avatar.d.ts +143 -0
  163. package/dist/lib/types/avatar.js +20 -0
  164. package/dist/lib/types/cli.d.ts +6 -0
  165. package/dist/lib/types/common.d.ts +0 -3
  166. package/dist/lib/types/conversation.d.ts +10 -3
  167. package/dist/lib/types/generate.d.ts +76 -5
  168. package/dist/lib/types/index.d.ts +6 -0
  169. package/dist/lib/types/index.js +8 -0
  170. package/dist/lib/types/memory.d.ts +96 -0
  171. package/dist/lib/types/memory.js +23 -0
  172. package/dist/lib/types/middleware.d.ts +27 -0
  173. package/dist/lib/types/multimodal.d.ts +35 -2
  174. package/dist/lib/types/music.d.ts +165 -0
  175. package/dist/lib/types/music.js +21 -0
  176. package/dist/lib/types/providers.d.ts +284 -3
  177. package/dist/lib/types/replicate.d.ts +67 -0
  178. package/dist/lib/types/replicate.js +10 -0
  179. package/dist/lib/types/safeFetch.d.ts +15 -0
  180. package/dist/lib/types/safeFetch.js +7 -0
  181. package/dist/lib/types/stream.d.ts +8 -1
  182. package/dist/lib/types/tools.d.ts +13 -0
  183. package/dist/lib/types/video.d.ts +89 -0
  184. package/dist/lib/types/video.js +15 -0
  185. package/dist/lib/utils/avatarProcessor.d.ts +68 -0
  186. package/dist/lib/utils/avatarProcessor.js +172 -0
  187. package/dist/lib/utils/cloneOptions.d.ts +36 -0
  188. package/dist/lib/utils/cloneOptions.js +62 -0
  189. package/dist/lib/utils/lifecycleCallbacks.d.ts +56 -0
  190. package/dist/lib/utils/lifecycleCallbacks.js +100 -0
  191. package/dist/lib/utils/lifecycleTimeout.d.ts +25 -0
  192. package/dist/lib/utils/lifecycleTimeout.js +39 -0
  193. package/dist/lib/utils/logSanitize.d.ts +49 -0
  194. package/dist/lib/utils/logSanitize.js +170 -0
  195. package/dist/lib/utils/loggingFetch.d.ts +29 -0
  196. package/dist/lib/utils/loggingFetch.js +60 -0
  197. package/dist/lib/utils/messageBuilder.d.ts +10 -0
  198. package/dist/lib/utils/messageBuilder.js +83 -30
  199. package/dist/lib/utils/modelChoices.js +236 -3
  200. package/dist/lib/utils/modelDetection.d.ts +11 -0
  201. package/dist/lib/utils/modelDetection.js +27 -0
  202. package/dist/lib/utils/musicProcessor.d.ts +67 -0
  203. package/dist/lib/utils/musicProcessor.js +189 -0
  204. package/dist/lib/utils/optionsConversion.js +3 -2
  205. package/dist/lib/utils/parameterValidation.js +14 -4
  206. package/dist/lib/utils/pricing.js +193 -0
  207. package/dist/lib/utils/providerConfig.d.ts +55 -0
  208. package/dist/lib/utils/providerConfig.js +224 -0
  209. package/dist/lib/utils/providerHealth.js +7 -7
  210. package/dist/lib/utils/safeFetch.d.ts +26 -0
  211. package/dist/lib/utils/safeFetch.js +83 -0
  212. package/dist/lib/utils/schemaConversion.d.ts +1 -1
  213. package/dist/lib/utils/schemaConversion.js +59 -4
  214. package/dist/lib/utils/sizeGuard.d.ts +34 -0
  215. package/dist/lib/utils/sizeGuard.js +45 -0
  216. package/dist/lib/utils/ssrfGuard.d.ts +52 -0
  217. package/dist/lib/utils/ssrfGuard.js +411 -0
  218. package/dist/lib/utils/tokenLimits.js +23 -32
  219. package/dist/lib/utils/videoProcessor.d.ts +60 -0
  220. package/dist/lib/utils/videoProcessor.js +201 -0
  221. package/dist/lib/voice/providers/FishAudioTTS.d.ts +27 -0
  222. package/dist/lib/voice/providers/FishAudioTTS.js +183 -0
  223. package/dist/lib/workflow/core/ensembleExecutor.js +26 -9
  224. package/dist/memory/hippocampusInitializer.d.ts +2 -2
  225. package/dist/memory/hippocampusInitializer.js +32 -2
  226. package/dist/middleware/builtin/lifecycle.js +52 -51
  227. package/dist/music/index.d.ts +13 -0
  228. package/dist/music/index.js +13 -0
  229. package/dist/music/providers/BeatovenMusic.d.ts +31 -0
  230. package/dist/music/providers/BeatovenMusic.js +333 -0
  231. package/dist/music/providers/ElevenLabsMusic.d.ts +30 -0
  232. package/dist/music/providers/ElevenLabsMusic.js +168 -0
  233. package/dist/music/providers/LyriaMusic.d.ts +29 -0
  234. package/dist/music/providers/LyriaMusic.js +172 -0
  235. package/dist/music/providers/ReplicateMusic.d.ts +31 -0
  236. package/dist/music/providers/ReplicateMusic.js +261 -0
  237. package/dist/neurolink.d.ts +30 -0
  238. package/dist/neurolink.js +342 -49
  239. package/dist/providers/amazonBedrock.d.ts +10 -0
  240. package/dist/providers/amazonBedrock.js +94 -39
  241. package/dist/providers/anthropic.js +55 -7
  242. package/dist/providers/anthropicBaseProvider.js +1 -1
  243. package/dist/providers/azureOpenai.js +66 -17
  244. package/dist/providers/cloudflare.d.ts +35 -0
  245. package/dist/providers/cloudflare.js +173 -0
  246. package/dist/providers/cohere.d.ts +52 -0
  247. package/dist/providers/cohere.js +252 -0
  248. package/dist/providers/deepseek.js +72 -17
  249. package/dist/providers/fireworks.d.ts +33 -0
  250. package/dist/providers/fireworks.js +163 -0
  251. package/dist/providers/googleAiStudio.d.ts +11 -3
  252. package/dist/providers/googleAiStudio.js +335 -344
  253. package/dist/providers/googleNativeGemini3.d.ts +107 -2
  254. package/dist/providers/googleNativeGemini3.js +381 -25
  255. package/dist/providers/googleVertex.d.ts +116 -129
  256. package/dist/providers/googleVertex.js +3000 -1987
  257. package/dist/providers/groq.d.ts +33 -0
  258. package/dist/providers/groq.js +180 -0
  259. package/dist/providers/huggingFace.js +9 -8
  260. package/dist/providers/ideogram.d.ts +34 -0
  261. package/dist/providers/ideogram.js +183 -0
  262. package/dist/providers/index.d.ts +13 -0
  263. package/dist/providers/index.js +13 -0
  264. package/dist/providers/jina.d.ts +59 -0
  265. package/dist/providers/jina.js +217 -0
  266. package/dist/providers/llamaCpp.js +14 -46
  267. package/dist/providers/lmStudio.js +14 -47
  268. package/dist/providers/mistral.js +7 -7
  269. package/dist/providers/nvidiaNim.js +160 -19
  270. package/dist/providers/ollama.js +7 -7
  271. package/dist/providers/openAI.d.ts +22 -1
  272. package/dist/providers/openAI.js +181 -0
  273. package/dist/providers/openRouter.js +38 -22
  274. package/dist/providers/openaiCompatible.js +9 -8
  275. package/dist/providers/perplexity.d.ts +33 -0
  276. package/dist/providers/perplexity.js +178 -0
  277. package/dist/providers/recraft.d.ts +34 -0
  278. package/dist/providers/recraft.js +196 -0
  279. package/dist/providers/replicate.d.ts +75 -0
  280. package/dist/providers/replicate.js +402 -0
  281. package/dist/providers/stability.d.ts +37 -0
  282. package/dist/providers/stability.js +190 -0
  283. package/dist/providers/togetherAi.d.ts +33 -0
  284. package/dist/providers/togetherAi.js +175 -0
  285. package/dist/providers/voyage.d.ts +47 -0
  286. package/dist/providers/voyage.js +176 -0
  287. package/dist/providers/xai.d.ts +33 -0
  288. package/dist/providers/xai.js +171 -0
  289. package/dist/telemetry/index.d.ts +1 -1
  290. package/dist/telemetry/index.js +1 -1
  291. package/dist/telemetry/tracers.d.ts +19 -0
  292. package/dist/telemetry/tracers.js +19 -0
  293. package/dist/telemetry/withSpan.d.ts +35 -0
  294. package/dist/telemetry/withSpan.js +103 -0
  295. package/dist/types/aliases.d.ts +14 -0
  296. package/dist/types/avatar.d.ts +143 -0
  297. package/dist/types/avatar.js +19 -0
  298. package/dist/types/cli.d.ts +6 -0
  299. package/dist/types/common.d.ts +0 -3
  300. package/dist/types/conversation.d.ts +10 -3
  301. package/dist/types/generate.d.ts +76 -5
  302. package/dist/types/index.d.ts +6 -0
  303. package/dist/types/index.js +8 -0
  304. package/dist/types/memory.d.ts +96 -0
  305. package/dist/types/memory.js +22 -0
  306. package/dist/types/middleware.d.ts +27 -0
  307. package/dist/types/multimodal.d.ts +35 -2
  308. package/dist/types/music.d.ts +165 -0
  309. package/dist/types/music.js +20 -0
  310. package/dist/types/providers.d.ts +284 -3
  311. package/dist/types/replicate.d.ts +67 -0
  312. package/dist/types/replicate.js +9 -0
  313. package/dist/types/safeFetch.d.ts +15 -0
  314. package/dist/types/safeFetch.js +6 -0
  315. package/dist/types/stream.d.ts +8 -1
  316. package/dist/types/tools.d.ts +13 -0
  317. package/dist/types/video.d.ts +89 -0
  318. package/dist/types/video.js +14 -0
  319. package/dist/utils/avatarProcessor.d.ts +68 -0
  320. package/dist/utils/avatarProcessor.js +171 -0
  321. package/dist/utils/cloneOptions.d.ts +36 -0
  322. package/dist/utils/cloneOptions.js +61 -0
  323. package/dist/utils/lifecycleCallbacks.d.ts +56 -0
  324. package/dist/utils/lifecycleCallbacks.js +99 -0
  325. package/dist/utils/lifecycleTimeout.d.ts +25 -0
  326. package/dist/utils/lifecycleTimeout.js +38 -0
  327. package/dist/utils/logSanitize.d.ts +49 -0
  328. package/dist/utils/logSanitize.js +169 -0
  329. package/dist/utils/loggingFetch.d.ts +29 -0
  330. package/dist/utils/loggingFetch.js +59 -0
  331. package/dist/utils/messageBuilder.d.ts +10 -0
  332. package/dist/utils/messageBuilder.js +83 -30
  333. package/dist/utils/modelChoices.js +236 -3
  334. package/dist/utils/modelDetection.d.ts +11 -0
  335. package/dist/utils/modelDetection.js +27 -0
  336. package/dist/utils/musicProcessor.d.ts +67 -0
  337. package/dist/utils/musicProcessor.js +188 -0
  338. package/dist/utils/optionsConversion.js +3 -2
  339. package/dist/utils/parameterValidation.js +14 -4
  340. package/dist/utils/pricing.js +193 -0
  341. package/dist/utils/providerConfig.d.ts +55 -0
  342. package/dist/utils/providerConfig.js +224 -0
  343. package/dist/utils/providerHealth.js +7 -7
  344. package/dist/utils/safeFetch.d.ts +26 -0
  345. package/dist/utils/safeFetch.js +82 -0
  346. package/dist/utils/schemaConversion.d.ts +1 -1
  347. package/dist/utils/schemaConversion.js +59 -4
  348. package/dist/utils/sizeGuard.d.ts +34 -0
  349. package/dist/utils/sizeGuard.js +44 -0
  350. package/dist/utils/ssrfGuard.d.ts +52 -0
  351. package/dist/utils/ssrfGuard.js +410 -0
  352. package/dist/utils/tokenLimits.js +23 -32
  353. package/dist/utils/videoProcessor.d.ts +60 -0
  354. package/dist/utils/videoProcessor.js +200 -0
  355. package/dist/voice/providers/FishAudioTTS.d.ts +27 -0
  356. package/dist/voice/providers/FishAudioTTS.js +182 -0
  357. package/dist/workflow/core/ensembleExecutor.js +26 -9
  358. 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
- logger.debug(`[SCHEMA-INLINE] Could not resolve $ref: ${refPath}`);
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 — convert via zod-to-json-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: "openApi3", // Use OpenAPI 3.0 for nullable: true instead of anyOf with null (required for Vertex AI)
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
+ }