@juspay/neurolink 9.94.3 → 9.94.4

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.
@@ -6,7 +6,9 @@ import { logger } from "./logger.js";
6
6
  import { urlDownloadRateLimiter } from "./rateLimiter.js";
7
7
  import { withRetry } from "./retryHandler.js";
8
8
  import { SYSTEM_LIMITS } from "../core/constants.js";
9
+ import { SIZE_LIMITS_BYTES } from "../processors/config/sizeLimits.js";
9
10
  import { getImageCache } from "./imageCache.js";
11
+ import { ErrorFactory, NeuroLinkError } from "./errorHandling.js";
10
12
  /**
11
13
  * Network error codes that should trigger a retry
12
14
  */
@@ -75,7 +77,7 @@ export class ImageProcessor {
75
77
  throw new Error("Invalid image processing: buffer is empty");
76
78
  }
77
79
  const mediaType = this.detectImageType(content);
78
- const base64 = content.toString("base64");
80
+ const base64 = ImageProcessor.safeBase64Convert(content, "image processing");
79
81
  const dataUri = `data:${mediaType};base64,${base64}`;
80
82
  // Validate output before returning
81
83
  this.validateProcessOutput(dataUri, base64, mediaType);
@@ -138,10 +140,15 @@ export class ImageProcessor {
138
140
  return `data:image/jpeg;base64,${image}`;
139
141
  }
140
142
  // Handle Buffer - convert to data URI
141
- const base64 = image.toString("base64");
143
+ const base64 = ImageProcessor.safeBase64Convert(image, "OpenAI image input");
142
144
  return `data:image/jpeg;base64,${base64}`;
143
145
  }
144
146
  catch (error) {
147
+ // Preserve typed errors (e.g. IMAGE_TOO_LARGE) as-is — don't flatten
148
+ // them into a generic Error and lose the error `code` for callers.
149
+ if (error instanceof NeuroLinkError) {
150
+ throw error;
151
+ }
145
152
  logger.error("Failed to process image for OpenAI:", error);
146
153
  throw new Error(`Image processing failed for OpenAI: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error });
147
154
  }
@@ -170,7 +177,7 @@ export class ImageProcessor {
170
177
  }
171
178
  }
172
179
  else {
173
- base64Data = image.toString("base64");
180
+ base64Data = ImageProcessor.safeBase64Convert(image, "provider image input");
174
181
  }
175
182
  return {
176
183
  mimeType,
@@ -178,6 +185,11 @@ export class ImageProcessor {
178
185
  };
179
186
  }
180
187
  catch (error) {
188
+ // Preserve typed errors (e.g. IMAGE_TOO_LARGE) as-is — don't flatten
189
+ // them into a generic Error and lose the error `code` for callers.
190
+ if (error instanceof NeuroLinkError) {
191
+ throw error;
192
+ }
181
193
  logger.error("Failed to process image for Google AI:", error);
182
194
  throw new Error(`Image processing failed for Google AI: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error });
183
195
  }
@@ -206,7 +218,7 @@ export class ImageProcessor {
206
218
  }
207
219
  }
208
220
  else {
209
- base64Data = image.toString("base64");
221
+ base64Data = ImageProcessor.safeBase64Convert(image, "provider image input");
210
222
  }
211
223
  return {
212
224
  mediaType,
@@ -214,6 +226,11 @@ export class ImageProcessor {
214
226
  };
215
227
  }
216
228
  catch (error) {
229
+ // Preserve typed errors (e.g. IMAGE_TOO_LARGE) as-is — don't flatten
230
+ // them into a generic Error and lose the error `code` for callers.
231
+ if (error instanceof NeuroLinkError) {
232
+ throw error;
233
+ }
217
234
  logger.error("Failed to process image for Anthropic:", error);
218
235
  throw new Error(`Image processing failed for Anthropic: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error });
219
236
  }
@@ -238,6 +255,11 @@ export class ImageProcessor {
238
255
  }
239
256
  }
240
257
  catch (error) {
258
+ // Preserve typed errors bubbling up from processImageForGoogle/
259
+ // processImageForAnthropic (e.g. IMAGE_TOO_LARGE) as-is.
260
+ if (error instanceof NeuroLinkError) {
261
+ throw error;
262
+ }
241
263
  logger.error("Failed to process image for Vertex AI:", error);
242
264
  throw new Error(`Image processing failed for Vertex AI: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error });
243
265
  }
@@ -322,20 +344,64 @@ export class ImageProcessor {
322
344
  }
323
345
  }
324
346
  /**
325
- * Validate image size (default 10MB limit)
347
+ * Throwing size guard for a raw byte length. Prevents memory exhaustion by
348
+ * rejecting an oversized input BEFORE an unbounded read/allocation happens
349
+ * (#257) — callers that can get a size cheaply (e.g. `fs.stat`) should
350
+ * validate it before ever reading the bytes into memory. Default limit is
351
+ * the canonical `SIZE_LIMITS_BYTES.IMAGE_MAX` (10 MB); there is no public
352
+ * way to raise it — `maxSize` is an internal-only override for advanced
353
+ * callers (e.g. video image inputs use a higher limit).
354
+ *
355
+ * @param size - Byte length to validate
356
+ * @param context - Short label identifying the source (unused in the
357
+ * thrown message today, kept for call-site readability and future use)
358
+ * @param maxSize - Max allowed size in bytes
359
+ * @throws NeuroLinkError (`INVALID_IMAGE_SIZE`) if `size` is not a finite,
360
+ * non-negative number
361
+ * @throws NeuroLinkError (`IMAGE_TOO_LARGE`) if `size` exceeds `maxSize`
326
362
  */
327
- static validateImageSize(data, maxSize = 10 * 1024 * 1024) {
328
- try {
329
- const size = typeof data === "string"
330
- ? Buffer.byteLength(data, "base64")
331
- : data.length;
332
- return size <= maxSize;
333
- }
334
- catch (error) {
335
- logger.warn("Failed to validate image size:", error);
336
- return false;
363
+ static validateSize(size, context, maxSize = SIZE_LIMITS_BYTES.IMAGE_MAX) {
364
+ // Reject a malformed maxSize before it's ever compared: `size > NaN` and
365
+ // `size > Infinity` (for a negative maxSize) can both evaluate `false`,
366
+ // which would let a corrupted/negative limit bypass the guard entirely.
367
+ if (!Number.isFinite(maxSize) || maxSize < 0) {
368
+ throw ErrorFactory.invalidConfiguration("maxSize", "must be a finite, non-negative byte count", { maxSize, context });
369
+ }
370
+ // Fail closed on malformed sizes first: `NaN > maxSize` and
371
+ // `-1 > maxSize` both evaluate to `false`, which would otherwise let a
372
+ // corrupted stat/header value silently skip the guard below.
373
+ if (!Number.isFinite(size) || size < 0) {
374
+ throw ErrorFactory.invalidImageSize(size);
375
+ }
376
+ if (size > maxSize) {
377
+ const mb = (size / (1024 * 1024)).toFixed(1);
378
+ const maxMb = (maxSize / (1024 * 1024)).toFixed(0);
379
+ throw ErrorFactory.imageTooLarge(mb, maxMb);
337
380
  }
338
381
  }
382
+ /**
383
+ * Throwing size guard for image buffers. Prevents memory exhaustion by
384
+ * rejecting oversized buffers with a descriptive error BEFORE an unbounded
385
+ * `Buffer.toString("base64")` allocation (#257). Delegates to
386
+ * `validateSize` so buffer-based and pre-read (stat-based) callers share
387
+ * one implementation.
388
+ *
389
+ * @param buffer - Image buffer to validate
390
+ * @param context - Short label identifying the source (see `validateSize`)
391
+ * @param maxSize - Max allowed size in bytes
392
+ * @throws NeuroLinkError (`IMAGE_TOO_LARGE`) if the buffer exceeds `maxSize`
393
+ */
394
+ static validateBufferSize(buffer, context, maxSize = SIZE_LIMITS_BYTES.IMAGE_MAX) {
395
+ ImageProcessor.validateSize(buffer.length, context, maxSize);
396
+ }
397
+ /**
398
+ * Size-guarded Buffer → base64. Use in place of `buffer.toString("base64")`
399
+ * on any path that converts a caller-supplied image buffer (#257).
400
+ */
401
+ static safeBase64Convert(buffer, context, maxSize = SIZE_LIMITS_BYTES.IMAGE_MAX) {
402
+ ImageProcessor.validateBufferSize(buffer, context, maxSize);
403
+ return buffer.toString("base64");
404
+ }
339
405
  /**
340
406
  * Validate image format
341
407
  */
@@ -467,7 +533,7 @@ export class ImageProcessor {
467
533
  : image;
468
534
  }
469
535
  else {
470
- data = image.toString("base64");
536
+ data = ImageProcessor.safeBase64Convert(image, "image input");
471
537
  }
472
538
  format = "base64";
473
539
  }
@@ -479,6 +545,11 @@ export class ImageProcessor {
479
545
  };
480
546
  }
481
547
  catch (error) {
548
+ // Preserve typed errors (e.g. IMAGE_TOO_LARGE) bubbling up from the
549
+ // provider-specific branches above as-is.
550
+ if (error instanceof NeuroLinkError) {
551
+ throw error;
552
+ }
482
553
  logger.error(`Failed to process image for ${provider}:`, error);
483
554
  throw new Error(`Image processing failed: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error });
484
555
  }
@@ -597,10 +668,16 @@ export const imageUtils = {
597
668
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
598
669
  },
599
670
  /**
600
- * Convert Buffer to base64 string
671
+ * Convert Buffer to base64 string. Guarded by the same #257 size limit as
672
+ * every other conversion site (default `SIZE_LIMITS_BYTES.IMAGE_MAX`);
673
+ * pass `maxBytes` to override for a caller with a legitimate need for a
674
+ * different ceiling rather than silently accepting an unbounded buffer.
675
+ * `imageUtils`/`ImageProcessor` are internal-only — not re-exported from
676
+ * `src/lib/index.ts` or the package's public `exports` map — so this
677
+ * default does not change behavior for any external SDK consumer.
601
678
  */
602
- bufferToBase64: (buffer) => {
603
- return buffer.toString("base64");
679
+ bufferToBase64: (buffer, maxBytes = SIZE_LIMITS_BYTES.IMAGE_MAX) => {
680
+ return ImageProcessor.safeBase64Convert(buffer, "buffer-to-base64", maxBytes);
604
681
  },
605
682
  /**
606
683
  * Convert base64 string to Buffer
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readFileSync, statSync } from "fs";
2
+ import { readFile as readFileAsync, stat as statAsync } from "fs/promises";
2
3
  import { getGlobalDispatcher, interceptors, request } from "undici";
3
4
  import { MultimodalLogger, ProviderImageAdapter, } from "../adapters/providerImageAdapter.js";
4
5
  import { CONVERSATION_INSTRUCTIONS, STRUCTURED_OUTPUT_INSTRUCTIONS, } from "../config/conversationMemory.js";
@@ -6,8 +7,10 @@ import { getAvailableInputTokens } from "../constants/contextWindows.js";
6
7
  import { enforceAggregateFileBudget, FILE_READ_BUDGET_PERCENT, } from "../context/fileTokenBudget.js";
7
8
  import { SIZE_TIER_THRESHOLDS } from "../types/index.js";
8
9
  import { tracers, ATTR, withSpan } from "../telemetry/index.js";
10
+ import { NeuroLinkError } from "./errorHandling.js";
9
11
  import { FileDetector } from "./fileDetector.js";
10
12
  import { getImageCache } from "./imageCache.js";
13
+ import { ImageProcessor } from "./imageProcessor.js";
11
14
  import { logger } from "./logger.js";
12
15
  import { PDFImageConverter, PDFProcessor } from "./pdfProcessor.js";
13
16
  import { urlDownloadRateLimiter } from "./rateLimiter.js";
@@ -1396,13 +1399,38 @@ function detectMimeTypeFromBuffer(buffer) {
1396
1399
  /**
1397
1400
  * Convert file path to raw base64 string.
1398
1401
  * Returns raw base64 (not a data: URI) to avoid SSRF validation in AI SDK v6.
1402
+ * Uses async fs so a large/slow-filesystem image never blocks the event loop
1403
+ * while the size guard (below) or the read itself is in flight.
1399
1404
  */
1400
- function convertFilePathToBase64(filePath) {
1401
- if (!existsSync(filePath)) {
1402
- throw new Error(`Image file not found: ${filePath}`);
1405
+ async function convertFilePathToBase64(filePath) {
1406
+ let stats;
1407
+ try {
1408
+ stats = await statAsync(filePath);
1403
1409
  }
1404
- const buffer = readFileSync(filePath);
1405
- return buffer.toString("base64");
1410
+ catch (error) {
1411
+ // Only ENOENT means "not found" — EACCES/ELOOP/etc. are real access or
1412
+ // filesystem problems that must not be misreported as a missing file.
1413
+ const code = error instanceof Error
1414
+ ? error.code
1415
+ : undefined;
1416
+ if (code === "ENOENT") {
1417
+ throw new Error(`Image file not found: ${filePath}`, { cause: error });
1418
+ }
1419
+ throw new Error(`Cannot access image file ${filePath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1420
+ }
1421
+ if (!stats.isFile()) {
1422
+ throw new Error(`Image path is not a file: ${filePath}`);
1423
+ }
1424
+ // #257: check the file size via stat BEFORE reading it into memory, so a
1425
+ // huge image path never triggers an unbounded read + base64 allocation.
1426
+ // Delegates to ImageProcessor's shared guard (no local reimplementation).
1427
+ const context = `image file "${filePath}"`;
1428
+ ImageProcessor.validateSize(stats.size, context);
1429
+ const buffer = await readFileAsync(filePath);
1430
+ // TOCTOU: the file can grow, or a symlink can be swapped, between the stat
1431
+ // above and this read completing. Re-validate the bytes actually read
1432
+ // (not just the pre-read stat) before the base64 allocation.
1433
+ return ImageProcessor.safeBase64Convert(buffer, context);
1406
1434
  }
1407
1435
  /**
1408
1436
  * Process a single image input and convert to raw base64 format.
@@ -1413,7 +1441,7 @@ function convertFilePathToBase64(filePath) {
1413
1441
  * Passing raw base64 avoids this because `new URL(base64string)` throws and
1414
1442
  * the SDK treats the string as inline base64 data instead.
1415
1443
  */
1416
- function processImageToBase64(image, index) {
1444
+ async function processImageToBase64(image, index) {
1417
1445
  let imageData;
1418
1446
  let mimeType = "image/jpeg"; // Default mime type
1419
1447
  if (typeof image === "string") {
@@ -1444,7 +1472,7 @@ function processImageToBase64(image, index) {
1444
1472
  else {
1445
1473
  // File path string - convert to raw base64
1446
1474
  try {
1447
- imageData = convertFilePathToBase64(image);
1475
+ imageData = await convertFilePathToBase64(image);
1448
1476
  mimeType = getMimeTypeFromExtension(image);
1449
1477
  }
1450
1478
  catch (error) {
@@ -1452,6 +1480,11 @@ function processImageToBase64(image, index) {
1452
1480
  index,
1453
1481
  filePath: image,
1454
1482
  });
1483
+ // Preserve typed errors (e.g. IMAGE_TOO_LARGE) as-is — don't flatten
1484
+ // them into a generic Error and lose the error `code` for callers.
1485
+ if (error instanceof NeuroLinkError) {
1486
+ throw error;
1487
+ }
1455
1488
  throw new Error(`Failed to convert file path to base64: ${image}. ${error}`, { cause: error });
1456
1489
  }
1457
1490
  }
@@ -1462,6 +1495,9 @@ function processImageToBase64(image, index) {
1462
1495
  if (detectedMimeType) {
1463
1496
  mimeType = detectedMimeType;
1464
1497
  }
1498
+ // #257: guard the buffer size before the unbounded base64 conversion.
1499
+ // Delegates to ImageProcessor's shared guard (no local reimplementation).
1500
+ ImageProcessor.validateBufferSize(image, `image input at index ${index}`);
1465
1501
  imageData = image.toString("base64");
1466
1502
  }
1467
1503
  return { imageData, mimeType };
@@ -1524,11 +1560,13 @@ async function convertSimpleImagesToProviderFormat(text, images, provider, _mode
1524
1560
  const content = [
1525
1561
  { type: "text", text: enhancedText },
1526
1562
  ];
1527
- // Process all images (including downloaded URLs) for Vercel AI SDK
1528
- actualImages.forEach(({ data: image }, index) => {
1563
+ // Process all images (including downloaded URLs) for Vercel AI SDK.
1564
+ // Sequential for...of (not Promise.all) to preserve image ordering and
1565
+ // keep the original forEach's one-at-a-time error semantics.
1566
+ for (const [index, { data: image }] of actualImages.entries()) {
1529
1567
  try {
1530
1568
  // Use helper function to process image and reduce nesting depth
1531
- const { imageData, mimeType } = processImageToBase64(image, index);
1569
+ const { imageData, mimeType } = await processImageToBase64(image, index);
1532
1570
  content.push({
1533
1571
  type: "image",
1534
1572
  image: imageData,
@@ -1542,7 +1580,7 @@ async function convertSimpleImagesToProviderFormat(text, images, provider, _mode
1542
1580
  });
1543
1581
  throw error;
1544
1582
  }
1545
- });
1583
+ }
1546
1584
  return content;
1547
1585
  }
1548
1586
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.94.3",
3
+ "version": "9.94.4",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {