@juspay/neurolink 9.88.9 → 9.88.10

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 CHANGED
@@ -1,3 +1,9 @@
1
+ ## [9.88.10](https://github.com/juspay/neurolink/compare/v9.88.9...v9.88.10) (2026-07-16)
2
+
3
+ ### Bug Fixes
4
+
5
+ - **(message-builder):** image input download + data-URI safety ([746eab5](https://github.com/juspay/neurolink/commit/746eab54cce86a47ceec3049e46e2f51c4ef8547)), closes [#334](https://github.com/juspay/neurolink/issues/334) [#270](https://github.com/juspay/neurolink/issues/270) [#348](https://github.com/juspay/neurolink/issues/348)
6
+
1
7
  ## [9.88.9](https://github.com/juspay/neurolink/compare/v9.88.8...v9.88.9) (2026-07-15)
2
8
 
3
9
  ### Bug Fixes
@@ -1101,7 +1101,7 @@ IMPORTANT FILE HANDLING INSTRUCTIONS:
1101
1101
  - DO NOT use GitHub tools (get_file_contents, search_code, etc.) for local files - they only work for remote repository files
1102
1102
  - Analyze the provided file content directly without attempting to fetch or read files using tools
1103
1103
  - GitHub MCP tools are ONLY for remote repository operations, not local filesystem access
1104
- - Use the file content shown in this message for your analysis`}return t}async function J_(r,e,t){r.input||(r.input={});let n=r.input,o=Ty.getProviderConfig(e),s=o?o.maxSizeMB*1024*1024:10*1024*1024;eQt(r,e,t),await iO(r,s,e),await rQt(r),nQt(r,e,t);let i=await oQt(r,s,e),a=n.images&&n.images.length>0||n.content&&n.content.some(p=>p.type==="image"),c=i.length>0;if(!a&&!c)return n.csvFiles&&(n.csvFiles=[]),n.pdfFiles&&(n.pdfFiles=[]),n.files&&(n.files=[]),(await t4(r)).map(f=>{let h=f.providerOptions;return{role:f.role,content:f.content,...h&&{providerOptions:h}}});if(!VE.supportsVision(e,t))throw new Error(`Provider ${e} with model ${t} does not support vision processing. Supported providers: ${VE.getVisionProviders().join(", ")}`);let l=[],u=sQt(r,i.length>0);if(u.trim()&&l.push({role:"system",content:u.trim(),providerOptions:{anthropic:{cacheControl:{type:"ephemeral"}}}}),r.conversationHistory&&r.conversationHistory.length>0&&r.conversationHistory){for(let p of r.conversationHistory)if(p.role==="user"||p.role==="assistant"||p.role==="system"){let f=p.providerOptions,h=p.content;if(p.role==="assistant"&&Array.isArray(p.content)){let y=p.content.filter(b=>!!b&&typeof b=="object"&&b.type==="text"&&typeof b.text=="string");if(y.length===0)continue;y.some(b=>!!b.providerOptions)?h=y:h=y.length===1?y[0].text:y.map(b=>b.text).join(" ")}if(h==="")continue;l.push({role:p.role,content:h,...f&&{providerOptions:f}})}}try{let p;n.content&&n.content.length>0?p=await iQt(n.content,e,t):n.images&&n.images.length>0||i.length>0?p=await fUe(n.text??"",n.images||[],i,e,t):p=n.text,typeof p=="string"?l.push({role:"user",content:p}):l.push({role:"user",content:p});let f=r.context?.requestId;return pUe(l,f),l}catch(p){throw yy.logError("MULTIMODAL_BUILD",p,{provider:e,model:t,hasImages:a,imageCount:n.images?.length||0}),p}}async function iQt(r,e,t){let n=r.find(u=>u.type==="text"),o=r.filter(u=>u.type==="image"),s=r.filter(u=>u.type==="pdf"),i=n?.text||"";if(!(o.length>0||s.length>0)&&!i)throw new Error("Content must include either text or multimodal content");if(o.length===0&&s.length===0)return i;let c=o.map(u=>u.data),l=s.map(u=>({buffer:typeof u.data=="string"?Buffer.from(u.data,"base64"):u.data,filename:u.metadata?.filename||"document.pdf",pageCount:u.metadata?.pages??null}));return await fUe(i,c,l,e,t)}function mUe(r){return r.startsWith("http://")||r.startsWith("https://")}async function aQt(r){let e=oNe(),t=e.get(r);if(t)return g.debug("Using cached image for URL",{url:r.substring(0,50)}),t.dataUri;await KOe.acquire();try{let n=await zE(r,{dispatcher:$E().compose(BE.redirect({maxRedirections:5})),method:"GET",headersTimeout:1e4,bodyTimeout:3e4});if(n.statusCode!==200)throw new Error(`HTTP ${n.statusCode}: Failed to download image from ${r}`);let o=n.headers["content-type"]||"image/jpeg";if(!o.startsWith("image/"))throw new Error(`URL does not point to an image. Content-Type: ${o}`);let s=[];for await(let u of n.body)s.push(u);let i=Buffer.concat(s),a=10*1024*1024;if(i.length>a)throw new Error(`Image too large: ${i.length} bytes (max: ${a} bytes)`);let c=i.toString("base64"),l=`data:${o};base64,${c}`;return e.set(r,l,o,i),l}catch(n){throw yy.logError("URL_DOWNLOAD_FAILED",n,{url:r}),new Error(`Failed to download image from ${r}: ${n instanceof Error?n.message:String(n)}`,{cause:n})}}function cQt(r){switch(r.toLowerCase().split(".").pop()){case"png":return"image/png";case"gif":return"image/gif";case"webp":return"image/webp";case"bmp":return"image/bmp";case"tiff":case"tif":return"image/tiff";default:return"image/jpeg"}}function lQt(r){if(r.length>=3&&r[0]===255&&r[1]===216&&r[2]===255)return"image/jpeg";if(r.length>=8&&r[0]===137&&r[1]===80&&r[2]===78&&r[3]===71&&r[4]===13&&r[5]===10&&r[6]===26&&r[7]===10)return"image/png";if(r.length>=6&&r[0]===71&&r[1]===73&&r[2]===70&&r[3]===56&&(r[4]===55||r[4]===57)&&r[5]===97)return"image/gif";if(r.length>=12&&r[0]===82&&r[1]===73&&r[2]===70&&r[3]===70&&r[8]===87&&r[9]===69&&r[10]===66&&r[11]===80)return"image/webp";if(r.length>=2&&r[0]===66&&r[1]===77)return"image/bmp";if(r.length>=4&&(r[0]===73&&r[1]===73&&r[2]===42&&r[3]===0||r[0]===77&&r[1]===77&&r[2]===0&&r[3]===42))return"image/tiff"}function uQt(r){if(!Jn(r))throw new Error(`Image file not found: ${r}`);return cl(r).toString("base64")}function dQt(r,e){let t,n="image/jpeg";if(typeof r=="string")if(r.startsWith("data:")){let o=r.match(/^data:([^;]+);base64,(.+)$/);o?(n=o[1],t=o[2]):t=r}else{if(mUe(r))throw new Error(`Unprocessed URL found in actualImages: ${r}`);try{t=uQt(r),n=cQt(r)}catch(o){throw yy.logError("FILE_PATH_CONVERSION",o,{index:e,filePath:r}),new Error(`Failed to convert file path to base64: ${r}. ${o}`,{cause:o})}}else{let o=lQt(r);o&&(n=o),t=r.toString("base64")}return{imageData:t,mimeType:n}}async function pQt(r,e,t,n){VE.validateImageCount(e.length,t,n);let o=e.map((l,u)=>{let d=lUe(l);return d?`[Image ${u+1}: ${d}]`:null}).filter(Boolean),s=o.length>0?`${r}
1104
+ - Use the file content shown in this message for your analysis`}return t}async function J_(r,e,t){r.input||(r.input={});let n=r.input,o=Ty.getProviderConfig(e),s=o?o.maxSizeMB*1024*1024:10*1024*1024;eQt(r,e,t),await iO(r,s,e),await rQt(r),nQt(r,e,t);let i=await oQt(r,s,e),a=n.images&&n.images.length>0||n.content&&n.content.some(p=>p.type==="image"),c=i.length>0;if(!a&&!c)return n.csvFiles&&(n.csvFiles=[]),n.pdfFiles&&(n.pdfFiles=[]),n.files&&(n.files=[]),(await t4(r)).map(f=>{let h=f.providerOptions;return{role:f.role,content:f.content,...h&&{providerOptions:h}}});if(!VE.supportsVision(e,t))throw new Error(`Provider ${e} with model ${t} does not support vision processing. Supported providers: ${VE.getVisionProviders().join(", ")}`);let l=[],u=sQt(r,i.length>0);if(u.trim()&&l.push({role:"system",content:u.trim(),providerOptions:{anthropic:{cacheControl:{type:"ephemeral"}}}}),r.conversationHistory&&r.conversationHistory.length>0&&r.conversationHistory){for(let p of r.conversationHistory)if(p.role==="user"||p.role==="assistant"||p.role==="system"){let f=p.providerOptions,h=p.content;if(p.role==="assistant"&&Array.isArray(p.content)){let y=p.content.filter(b=>!!b&&typeof b=="object"&&b.type==="text"&&typeof b.text=="string");if(y.length===0)continue;y.some(b=>!!b.providerOptions)?h=y:h=y.length===1?y[0].text:y.map(b=>b.text).join(" ")}if(h==="")continue;l.push({role:p.role,content:h,...f&&{providerOptions:f}})}}try{let p;n.content&&n.content.length>0?p=await iQt(n.content,e,t):n.images&&n.images.length>0||i.length>0?p=await fUe(n.text??"",n.images||[],i,e,t):p=n.text,typeof p=="string"?l.push({role:"user",content:p}):l.push({role:"user",content:p});let f=r.context?.requestId;return pUe(l,f),l}catch(p){throw yy.logError("MULTIMODAL_BUILD",p,{provider:e,model:t,hasImages:a,imageCount:n.images?.length||0}),p}}async function iQt(r,e,t){let n=r.find(u=>u.type==="text"),o=r.filter(u=>u.type==="image"),s=r.filter(u=>u.type==="pdf"),i=n?.text||"";if(!(o.length>0||s.length>0)&&!i)throw new Error("Content must include either text or multimodal content");if(o.length===0&&s.length===0)return i;let c=o.map(u=>u.data),l=s.map(u=>({buffer:typeof u.data=="string"?Buffer.from(u.data,"base64"):u.data,filename:u.metadata?.filename||"document.pdf",pageCount:u.metadata?.pages??null}));return await fUe(i,c,l,e,t)}function mUe(r){let e=r.toLowerCase();return e.startsWith("http://")||e.startsWith("https://")}async function aQt(r){let e=oNe(),t=e.get(r);if(t)return g.debug("Using cached image for URL",{url:r.substring(0,50)}),t.dataUri;await KOe.acquire();try{let n=await zE(r,{dispatcher:$E().compose(BE.redirect({maxRedirections:5})),method:"GET",headersTimeout:1e4,bodyTimeout:3e4});if(n.statusCode!==200)throw new Error(`HTTP ${n.statusCode}: Failed to download image from ${r}`);let o=n.headers["content-type"]||"image/jpeg";if(!o.startsWith("image/"))throw new Error(`URL does not point to an image. Content-Type: ${o}`);let s=10*1024*1024,i=[],a=0;for await(let d of n.body){if(a+=d.length,a>s)throw new Error(`Image too large: exceeds ${s} bytes while downloading from ${r}`);i.push(d)}let c=Buffer.concat(i),l=c.toString("base64"),u=`data:${o};base64,${l}`;return e.set(r,u,o,c),u}catch(n){throw yy.logError("URL_DOWNLOAD_FAILED",n,{url:r}),new Error(`Failed to download image from ${r}: ${n instanceof Error?n.message:String(n)}`,{cause:n})}}function cQt(r){switch(r.toLowerCase().split(".").pop()){case"png":return"image/png";case"gif":return"image/gif";case"webp":return"image/webp";case"bmp":return"image/bmp";case"tiff":case"tif":return"image/tiff";default:return"image/jpeg"}}function lQt(r){if(r.length>=3&&r[0]===255&&r[1]===216&&r[2]===255)return"image/jpeg";if(r.length>=8&&r[0]===137&&r[1]===80&&r[2]===78&&r[3]===71&&r[4]===13&&r[5]===10&&r[6]===26&&r[7]===10)return"image/png";if(r.length>=6&&r[0]===71&&r[1]===73&&r[2]===70&&r[3]===56&&(r[4]===55||r[4]===57)&&r[5]===97)return"image/gif";if(r.length>=12&&r[0]===82&&r[1]===73&&r[2]===70&&r[3]===70&&r[8]===87&&r[9]===69&&r[10]===66&&r[11]===80)return"image/webp";if(r.length>=2&&r[0]===66&&r[1]===77)return"image/bmp";if(r.length>=4&&(r[0]===73&&r[1]===73&&r[2]===42&&r[3]===0||r[0]===77&&r[1]===77&&r[2]===0&&r[3]===42))return"image/tiff"}function uQt(r){if(!Jn(r))throw new Error(`Image file not found: ${r}`);return cl(r).toString("base64")}function dQt(r,e){let t,n="image/jpeg";if(typeof r=="string")if(r.startsWith("data:")){let o=r.match(/^data:([^;]+);base64,(.+)$/);if(o){let s=o[1];if(!s.startsWith("image/"))throw new Error(`Unsupported data URI MIME type for image input at index ${e}: "${s}" (expected image/*)`);n=s,t=o[2]}else throw new Error(`Malformed image data URI at index ${e} (expected "data:<image/...>;base64,<data>")`)}else{if(mUe(r))throw new Error(`Unprocessed URL found in actualImages: ${r}`);try{t=uQt(r),n=cQt(r)}catch(o){throw yy.logError("FILE_PATH_CONVERSION",o,{index:e,filePath:r}),new Error(`Failed to convert file path to base64: ${r}. ${o}`,{cause:o})}}else{let o=lQt(r);o&&(n=o),t=r.toString("base64")}return{imageData:t,mimeType:n}}async function pQt(r,e,t,n){VE.validateImageCount(e.length,t,n);let o=e.map((l,u)=>{let d=lUe(l);return d?`[Image ${u+1}: ${d}]`:null}).filter(Boolean),s=o.length>0?`${r}
1105
1105
 
1106
1106
  Image descriptions for context: ${o.join(" ")}`:r,i=[],a=[];e.forEach((l,u)=>{let d=Z7t(l),p=lUe(l);typeof d=="string"&&mUe(d)?i.push({url:d,altText:p}):a.push({data:d,altText:p})});for(let{url:l,altText:u}of i)try{let d=await aQt(l);a.push({data:d,altText:u})}catch(d){yy.logError("URL_DOWNLOAD_FAILED_SKIPPING",d,{url:l}),g.warn(`Failed to download image from ${l}, skipping: ${d instanceof Error?d.message:String(d)}`)}let c=[{type:"text",text:s}];return a.forEach(({data:l},u)=>{try{let{imageData:d,mimeType:p}=dQt(l,u);c.push({type:"image",image:d,mimeType:p})}catch(d){throw yy.logError("ADD_IMAGE_TO_CONTENT",d,{index:u,provider:t}),d}}),c}async function fUe(r,e,t,n,o){let s=[{type:"text",text:r}];if(e.length>0){let a=await pQt("",e,n,o);Array.isArray(a)&&a.forEach(c=>{c.type!=="text"&&s.push(c)})}if(Ty.supportsNativePDF(n))s.push(...t.map(a=>(g.info(`[PDF] \u2705 Added to content (native PDF format): ${a.filename}`),{type:"file",data:a.buffer,mediaType:"application/pdf"})));else{g.info(`[PDF\u2192Image] Provider ${n} doesn't support native PDF. Converting ${t.length} PDF(s) to images...`);for(let a of t)try{let c=await yNe.convertToImages(a.buffer,{scale:2,maxPages:20});g.info(`[PDF\u2192Image] \u2705 Converted ${a.filename}: ${c.pageCount} page(s) \u2192 images`),c.images.forEach((l,u)=>{s.push({type:"image",image:l,mimeType:"image/png"}),g.debug(`[PDF\u2192Image] Added page ${u+1}/${c.pageCount} of ${a.filename}`)}),c.warnings&&c.warnings.forEach(l=>{g.warn(`[PDF\u2192Image] ${l}`)})}catch(c){let l=c instanceof Error?c.message:String(c);throw g.error(`[PDF\u2192Image] \u274C Failed to convert ${a.filename}: ${l}`),new Error(`PDF to image conversion failed for ${a.filename}: ${l}. Provider ${n} doesn't support native PDFs and image conversion failed.`,{cause:c})}}return s}function Hb(r){return typeof r=="object"&&!Buffer.isBuffer(r)&&"buffer"in r&&"filename"in r}function Wy(r,e=0){if(Hb(r))return r.filename;if(typeof r=="string"){if(r.startsWith("http"))try{return new URL(r).pathname.split("/").pop()||`file-${e+1}`}catch{return`file-${e+1}`}return r.split("/").pop()||r.split("\\").pop()||`file-${e+1}`}return`file-${e+1}`}function mQt(r){if(Hb(r))return r.buffer.length;if(Buffer.isBuffer(r))return r.length;if(typeof r=="string"&&Jn(r))try{return tm(r).size}catch{return 0}return typeof r=="string"?r.length:0}async function fQt(r){if(Hb(r))return r.buffer;if(Buffer.isBuffer(r))return r;if(typeof r=="string"&&Jn(r))try{return cl(r)}catch{return null}return null}function gQt(r){if(Hb(r)||Buffer.isBuffer(r))return"buffer";if(typeof r=="string"){if(r.startsWith("data:"))return"datauri";if(r.startsWith("http://")||r.startsWith("https://"))return"url";if(Jn(r))return"path"}return"buffer"}async function hQt(r,e,t,n=0){try{let o=await fQt(r);if(!o)return!1;let s=Wy(r,n),i=typeof r=="object"&&!Buffer.isBuffer(r)?r.mimetype:void 0;return await t.register(o,gQt(r),{filename:s,mimetype:i}),g.info(`[FileDetector] Registered "${s}" (${(e/1024).toFixed(0)} KB) as lazy reference \u2014 skipping upfront processing`),!0}catch(o){return g.warn(`[FileDetector] Failed to register file as reference, falling back to full processing: ${o instanceof Error?o.message:String(o)}`),!1}}function yQt(r,e){let t={"text/javascript":"javascript","text/typescript":"typescript","text/x-python":"python","text/x-java-source":"java","text/x-go":"go","text/x-rustsrc":"rust","text/x-ruby":"ruby","text/x-php":"php","text/x-c":"c","text/x-c++":"cpp","text/x-csharp":"csharp","text/x-swift":"swift","text/x-kotlin":"kotlin","text/x-scala":"scala","text/x-shellscript":"bash","text/x-powershell":"powershell","text/x-sql":"sql","text/x-r":"r","text/x-lua":"lua","text/x-perl":"perl","text/x-dart":"dart","text/x-elixir":"elixir","text/x-erlang":"erlang","text/x-haskell":"haskell","text/x-clojure":"clojure","text/x-lisp":"lisp","text/html":"html","text/css":"css","text/markdown":"markdown","application/json":"json","application/xml":"xml","text/xml":"xml","application/yaml":"yaml","application/x-yaml":"yaml"},n=r.toLowerCase().split(";")[0].trim();if(t[n])return t[n];let o=e.split(".").pop()?.toLowerCase();return o&&{js:"javascript",mjs:"javascript",cjs:"javascript",jsx:"javascript",ts:"typescript",tsx:"typescript",py:"python",java:"java",go:"go",rs:"rust",rb:"ruby",php:"php",c:"c",cpp:"cpp",cc:"cpp",h:"c",hpp:"cpp",cs:"csharp",swift:"swift",kt:"kotlin",kts:"kotlin",scala:"scala",sh:"bash",bash:"bash",zsh:"bash",ps1:"powershell",sql:"sql",r:"r",lua:"lua",pl:"perl",perl:"perl",dart:"dart",ex:"elixir",exs:"elixir",erl:"erlang",hs:"haskell",clj:"clojure",lisp:"lisp",vim:"vim",html:"html",htm:"html",css:"css",md:"markdown",markdown:"markdown",json:"json",xml:"xml",yaml:"yaml",yml:"yaml",toml:"toml",ini:"ini",cfg:"ini"}[o]||null}function $oe(r){return`
1107
1107
  **NOTE**: You can perform calculations directly on the CSV data shown above. For advanced operations on the full file (counting by column, grouping, etc.), you may optionally use the analyzeCSV tool with filePath="${r}".
@@ -1245,7 +1245,10 @@ async function convertContentToProviderFormat(content, provider, _model) {
1245
1245
  * Check if a string is an internet URL
1246
1246
  */
1247
1247
  function isInternetUrl(input) {
1248
- return input.startsWith("http://") || input.startsWith("https://");
1248
+ // Scheme is case-insensitive (RFC 3986) "HTTPS://..." must still be a URL,
1249
+ // not fall through to the file-path branch and produce a confusing error.
1250
+ const lower = input.toLowerCase();
1251
+ return lower.startsWith("http://") || lower.startsWith("https://");
1249
1252
  }
1250
1253
  /**
1251
1254
  * Download image from URL and convert to base64 data URI
@@ -1278,17 +1281,21 @@ async function downloadImageFromUrl(url) {
1278
1281
  if (!contentType.startsWith("image/")) {
1279
1282
  throw new Error(`URL does not point to an image. Content-Type: ${contentType}`);
1280
1283
  }
1281
- // Read the response body
1284
+ // Read the response body, enforcing the size cap INCREMENTALLY: a
1285
+ // misbehaving/malicious server on a user-supplied URL must not be able to
1286
+ // force unbounded memory growth by streaming gigabytes before we ever
1287
+ // check the total (the previous code concat'd everything first).
1288
+ const maxSize = 10 * 1024 * 1024; // 10MB
1282
1289
  const chunks = [];
1290
+ let totalSize = 0;
1283
1291
  for await (const chunk of response.body) {
1292
+ totalSize += chunk.length;
1293
+ if (totalSize > maxSize) {
1294
+ throw new Error(`Image too large: exceeds ${maxSize} bytes while downloading from ${url}`);
1295
+ }
1284
1296
  chunks.push(chunk);
1285
1297
  }
1286
1298
  const buffer = Buffer.concat(chunks);
1287
- // Check file size (limit to 10MB)
1288
- const maxSize = 10 * 1024 * 1024; // 10MB
1289
- if (buffer.length > maxSize) {
1290
- throw new Error(`Image too large: ${buffer.length} bytes (max: ${maxSize} bytes)`);
1291
- }
1292
1299
  // Convert to base64 data URI
1293
1300
  const base64 = buffer.toString("base64");
1294
1301
  const dataUri = `data:${contentType};base64,${base64}`;
@@ -1414,11 +1421,20 @@ function processImageToBase64(image, index) {
1414
1421
  // Data URI (including downloaded URLs) - extract mime type and raw base64
1415
1422
  const match = image.match(/^data:([^;]+);base64,(.+)$/);
1416
1423
  if (match) {
1417
- mimeType = match[1];
1424
+ const declaredMime = match[1];
1425
+ // #348: only accept image/* data URIs; reject a non-image MIME before
1426
+ // it reaches a provider API rather than passing it through unchecked.
1427
+ if (!declaredMime.startsWith("image/")) {
1428
+ throw new Error(`Unsupported data URI MIME type for image input at index ${index}: "${declaredMime}" (expected image/*)`);
1429
+ }
1430
+ mimeType = declaredMime;
1418
1431
  imageData = match[2]; // Raw base64 only — NOT the full data: URI
1419
1432
  }
1420
1433
  else {
1421
- imageData = image;
1434
+ // #270: a malformed data: URI must fail loudly, not silently pass the
1435
+ // raw string through as if it were valid base64 (which corrupts the
1436
+ // request and surfaces as an opaque provider error later).
1437
+ throw new Error(`Malformed image data URI at index ${index} (expected "data:<image/...>;base64,<data>")`);
1422
1438
  }
1423
1439
  }
1424
1440
  else if (isInternetUrl(image)) {
@@ -1245,7 +1245,10 @@ async function convertContentToProviderFormat(content, provider, _model) {
1245
1245
  * Check if a string is an internet URL
1246
1246
  */
1247
1247
  function isInternetUrl(input) {
1248
- return input.startsWith("http://") || input.startsWith("https://");
1248
+ // Scheme is case-insensitive (RFC 3986) "HTTPS://..." must still be a URL,
1249
+ // not fall through to the file-path branch and produce a confusing error.
1250
+ const lower = input.toLowerCase();
1251
+ return lower.startsWith("http://") || lower.startsWith("https://");
1249
1252
  }
1250
1253
  /**
1251
1254
  * Download image from URL and convert to base64 data URI
@@ -1278,17 +1281,21 @@ async function downloadImageFromUrl(url) {
1278
1281
  if (!contentType.startsWith("image/")) {
1279
1282
  throw new Error(`URL does not point to an image. Content-Type: ${contentType}`);
1280
1283
  }
1281
- // Read the response body
1284
+ // Read the response body, enforcing the size cap INCREMENTALLY: a
1285
+ // misbehaving/malicious server on a user-supplied URL must not be able to
1286
+ // force unbounded memory growth by streaming gigabytes before we ever
1287
+ // check the total (the previous code concat'd everything first).
1288
+ const maxSize = 10 * 1024 * 1024; // 10MB
1282
1289
  const chunks = [];
1290
+ let totalSize = 0;
1283
1291
  for await (const chunk of response.body) {
1292
+ totalSize += chunk.length;
1293
+ if (totalSize > maxSize) {
1294
+ throw new Error(`Image too large: exceeds ${maxSize} bytes while downloading from ${url}`);
1295
+ }
1284
1296
  chunks.push(chunk);
1285
1297
  }
1286
1298
  const buffer = Buffer.concat(chunks);
1287
- // Check file size (limit to 10MB)
1288
- const maxSize = 10 * 1024 * 1024; // 10MB
1289
- if (buffer.length > maxSize) {
1290
- throw new Error(`Image too large: ${buffer.length} bytes (max: ${maxSize} bytes)`);
1291
- }
1292
1299
  // Convert to base64 data URI
1293
1300
  const base64 = buffer.toString("base64");
1294
1301
  const dataUri = `data:${contentType};base64,${base64}`;
@@ -1414,11 +1421,20 @@ function processImageToBase64(image, index) {
1414
1421
  // Data URI (including downloaded URLs) - extract mime type and raw base64
1415
1422
  const match = image.match(/^data:([^;]+);base64,(.+)$/);
1416
1423
  if (match) {
1417
- mimeType = match[1];
1424
+ const declaredMime = match[1];
1425
+ // #348: only accept image/* data URIs; reject a non-image MIME before
1426
+ // it reaches a provider API rather than passing it through unchecked.
1427
+ if (!declaredMime.startsWith("image/")) {
1428
+ throw new Error(`Unsupported data URI MIME type for image input at index ${index}: "${declaredMime}" (expected image/*)`);
1429
+ }
1430
+ mimeType = declaredMime;
1418
1431
  imageData = match[2]; // Raw base64 only — NOT the full data: URI
1419
1432
  }
1420
1433
  else {
1421
- imageData = image;
1434
+ // #270: a malformed data: URI must fail loudly, not silently pass the
1435
+ // raw string through as if it were valid base64 (which corrupts the
1436
+ // request and surfaces as an opaque provider error later).
1437
+ throw new Error(`Malformed image data URI at index ${index} (expected "data:<image/...>;base64,<data>")`);
1422
1438
  }
1423
1439
  }
1424
1440
  else if (isInternetUrl(image)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.88.9",
3
+ "version": "9.88.10",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (58+ servers), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {