@juspay/neurolink 9.94.2 → 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.
Files changed (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/agent/directTools.js +3 -1
  3. package/dist/browser/neurolink.min.js +400 -397
  4. package/dist/cli/factories/commandFactory.d.ts +6 -0
  5. package/dist/cli/factories/commandFactory.js +36 -8
  6. package/dist/lib/agent/directTools.js +3 -1
  7. package/dist/lib/neurolink.js +5 -0
  8. package/dist/lib/rag/document/loaders.js +20 -4
  9. package/dist/lib/types/file.d.ts +43 -0
  10. package/dist/lib/types/generate.d.ts +3 -11
  11. package/dist/lib/types/rag.d.ts +7 -0
  12. package/dist/lib/types/stream.d.ts +2 -6
  13. package/dist/lib/utils/csvProcessor.d.ts +68 -10
  14. package/dist/lib/utils/csvProcessor.js +499 -135
  15. package/dist/lib/utils/errorHandling.d.ts +32 -0
  16. package/dist/lib/utils/errorHandling.js +86 -0
  17. package/dist/lib/utils/imageProcessor.d.ts +42 -4
  18. package/dist/lib/utils/imageProcessor.js +96 -19
  19. package/dist/lib/utils/messageBuilder.js +49 -11
  20. package/dist/lib/utils/multimodalOptionsBuilder.d.ts +1 -5
  21. package/dist/lib/utils/textEncoding.d.ts +25 -0
  22. package/dist/lib/utils/textEncoding.js +141 -0
  23. package/dist/neurolink.js +5 -0
  24. package/dist/rag/document/loaders.js +20 -4
  25. package/dist/types/file.d.ts +43 -0
  26. package/dist/types/generate.d.ts +3 -11
  27. package/dist/types/rag.d.ts +7 -0
  28. package/dist/types/stream.d.ts +2 -6
  29. package/dist/utils/csvProcessor.d.ts +68 -10
  30. package/dist/utils/csvProcessor.js +498 -134
  31. package/dist/utils/errorHandling.d.ts +32 -0
  32. package/dist/utils/errorHandling.js +86 -0
  33. package/dist/utils/imageProcessor.d.ts +42 -4
  34. package/dist/utils/imageProcessor.js +96 -19
  35. package/dist/utils/messageBuilder.js +49 -11
  36. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -5
  37. package/dist/utils/textEncoding.d.ts +25 -0
  38. package/dist/utils/textEncoding.js +140 -0
  39. package/package.json +3 -1
@@ -34,6 +34,7 @@ export declare const ERROR_CODES: {
34
34
  readonly IMAGE_TOO_LARGE: "IMAGE_TOO_LARGE";
35
35
  readonly IMAGE_TOO_SMALL: "IMAGE_TOO_SMALL";
36
36
  readonly INVALID_IMAGE_FORMAT: "INVALID_IMAGE_FORMAT";
37
+ readonly INVALID_IMAGE_SIZE: "INVALID_IMAGE_SIZE";
37
38
  readonly PDF_PAGE_LIMIT_EXCEEDED: "PDF_PAGE_LIMIT_EXCEEDED";
38
39
  readonly RATE_LIMITER_QUEUE_FULL: "RATE_LIMITER_QUEUE_FULL";
39
40
  readonly RATE_LIMITER_QUEUE_TIMEOUT: "RATE_LIMITER_QUEUE_TIMEOUT";
@@ -51,6 +52,10 @@ export declare const ERROR_CODES: {
51
52
  readonly INVALID_PPT_LOGO_PATH: "INVALID_PPT_LOGO_PATH";
52
53
  readonly INVALID_PPT_MODE: "INVALID_PPT_MODE";
53
54
  readonly INVALID_PPT_PROMPT: "INVALID_PPT_PROMPT";
55
+ readonly CSV_INVALID_INPUT: "CSV_INVALID_INPUT";
56
+ readonly CSV_ROW_INVALID: "CSV_ROW_INVALID";
57
+ readonly CSV_FILE_ACCESS_FAILED: "CSV_FILE_ACCESS_FAILED";
58
+ readonly CSV_PARSE_FAILED: "CSV_PARSE_FAILED";
54
59
  };
55
60
  /**
56
61
  * Enhanced error class with structured information
@@ -128,6 +133,25 @@ export declare class ErrorFactory {
128
133
  * Create an invalid configuration error (e.g., NaN for numeric values)
129
134
  */
130
135
  static invalidConfiguration(configName: string, reason: string, context?: Record<string, unknown>): NeuroLinkError;
136
+ /**
137
+ * Create an invalid CSV input error (e.g. an empty filePath/csvString argument).
138
+ */
139
+ static csvInvalidInput(message: string): NeuroLinkError;
140
+ /**
141
+ * Create a CSV row-shape violation error (#384) — a parsed row that isn't a
142
+ * string-keyed object with string values.
143
+ */
144
+ static csvRowInvalid(message: string, rowNumber: number): NeuroLinkError;
145
+ /**
146
+ * Create a CSV file access error (bad path, permissions, ENOENT/EACCES) (#375).
147
+ */
148
+ static csvFileAccessFailed(message: string, filePath: string, originalError?: Error): NeuroLinkError;
149
+ /**
150
+ * Create a CSV read/parse failure error (#375) — source stream errors and
151
+ * csv-parser errors both route through this, carrying the enriched
152
+ * `buildCsvParseErrorMessage` context in `message`.
153
+ */
154
+ static csvParseFailed(message: string, context: Record<string, unknown>, originalError?: Error): NeuroLinkError;
131
155
  /**
132
156
  * Create an invalid video resolution error
133
157
  */
@@ -176,6 +200,14 @@ export declare class ErrorFactory {
176
200
  * Create an image too large error
177
201
  */
178
202
  static imageTooLarge(sizeMB: string, maxMB: string): NeuroLinkError;
203
+ /**
204
+ * Create an invalid image size error (NaN/Infinity/negative byte length).
205
+ * Distinct from `imageTooLarge` — this rejects a malformed size value
206
+ * before it reaches the max-size comparison, since `NaN > maxSize` and
207
+ * `-1 > maxSize` both evaluate to `false` and would otherwise let a
208
+ * corrupted stat/header value silently skip the guard.
209
+ */
210
+ static invalidImageSize(size: number): NeuroLinkError;
179
211
  /**
180
212
  * Create an image too small error
181
213
  */
@@ -45,6 +45,7 @@ export const ERROR_CODES = {
45
45
  IMAGE_TOO_LARGE: "IMAGE_TOO_LARGE",
46
46
  IMAGE_TOO_SMALL: "IMAGE_TOO_SMALL",
47
47
  INVALID_IMAGE_FORMAT: "INVALID_IMAGE_FORMAT",
48
+ INVALID_IMAGE_SIZE: "INVALID_IMAGE_SIZE",
48
49
  // PDF validation errors
49
50
  PDF_PAGE_LIMIT_EXCEEDED: "PDF_PAGE_LIMIT_EXCEEDED",
50
51
  // Rate limiter errors
@@ -66,6 +67,11 @@ export const ERROR_CODES = {
66
67
  INVALID_PPT_LOGO_PATH: "INVALID_PPT_LOGO_PATH",
67
68
  INVALID_PPT_MODE: "INVALID_PPT_MODE",
68
69
  INVALID_PPT_PROMPT: "INVALID_PPT_PROMPT",
70
+ // CSV validation/parsing errors (#1199)
71
+ CSV_INVALID_INPUT: "CSV_INVALID_INPUT",
72
+ CSV_ROW_INVALID: "CSV_ROW_INVALID",
73
+ CSV_FILE_ACCESS_FAILED: "CSV_FILE_ACCESS_FAILED",
74
+ CSV_PARSE_FAILED: "CSV_PARSE_FAILED",
69
75
  };
70
76
  /**
71
77
  * Enhanced error class with structured information
@@ -259,6 +265,66 @@ export class ErrorFactory {
259
265
  });
260
266
  }
261
267
  // ============================================================================
268
+ // CSV VALIDATION/PARSING ERRORS (#1199)
269
+ // ============================================================================
270
+ /**
271
+ * Create an invalid CSV input error (e.g. an empty filePath/csvString argument).
272
+ */
273
+ static csvInvalidInput(message) {
274
+ return new NeuroLinkError({
275
+ code: ERROR_CODES.CSV_INVALID_INPUT,
276
+ message,
277
+ category: ErrorCategory.VALIDATION,
278
+ severity: ErrorSeverity.MEDIUM,
279
+ retriable: false,
280
+ context: {},
281
+ });
282
+ }
283
+ /**
284
+ * Create a CSV row-shape violation error (#384) — a parsed row that isn't a
285
+ * string-keyed object with string values.
286
+ */
287
+ static csvRowInvalid(message, rowNumber) {
288
+ return new NeuroLinkError({
289
+ code: ERROR_CODES.CSV_ROW_INVALID,
290
+ message,
291
+ category: ErrorCategory.VALIDATION,
292
+ severity: ErrorSeverity.MEDIUM,
293
+ retriable: false,
294
+ context: { rowNumber },
295
+ });
296
+ }
297
+ /**
298
+ * Create a CSV file access error (bad path, permissions, ENOENT/EACCES) (#375).
299
+ */
300
+ static csvFileAccessFailed(message, filePath, originalError) {
301
+ return new NeuroLinkError({
302
+ code: ERROR_CODES.CSV_FILE_ACCESS_FAILED,
303
+ message,
304
+ category: ErrorCategory.RESOURCE,
305
+ severity: ErrorSeverity.HIGH,
306
+ retriable: false,
307
+ context: { filePath },
308
+ originalError,
309
+ });
310
+ }
311
+ /**
312
+ * Create a CSV read/parse failure error (#375) — source stream errors and
313
+ * csv-parser errors both route through this, carrying the enriched
314
+ * `buildCsvParseErrorMessage` context in `message`.
315
+ */
316
+ static csvParseFailed(message, context, originalError) {
317
+ return new NeuroLinkError({
318
+ code: ERROR_CODES.CSV_PARSE_FAILED,
319
+ message,
320
+ category: ErrorCategory.EXECUTION,
321
+ severity: ErrorSeverity.MEDIUM,
322
+ retriable: false,
323
+ context,
324
+ originalError,
325
+ });
326
+ }
327
+ // ============================================================================
262
328
  // VIDEO VALIDATION ERRORS
263
329
  // ============================================================================
264
330
  /**
@@ -505,6 +571,26 @@ export class ErrorFactory {
505
571
  },
506
572
  });
507
573
  }
574
+ /**
575
+ * Create an invalid image size error (NaN/Infinity/negative byte length).
576
+ * Distinct from `imageTooLarge` — this rejects a malformed size value
577
+ * before it reaches the max-size comparison, since `NaN > maxSize` and
578
+ * `-1 > maxSize` both evaluate to `false` and would otherwise let a
579
+ * corrupted stat/header value silently skip the guard.
580
+ */
581
+ static invalidImageSize(size) {
582
+ return new NeuroLinkError({
583
+ code: ERROR_CODES.INVALID_IMAGE_SIZE,
584
+ message: `Invalid image size: ${size} (must be a finite, non-negative number)`,
585
+ category: ErrorCategory.VALIDATION,
586
+ severity: ErrorSeverity.MEDIUM,
587
+ retriable: false,
588
+ context: {
589
+ field: "input.images",
590
+ size,
591
+ },
592
+ });
593
+ }
508
594
  /**
509
595
  * Create an image too small error
510
596
  */
@@ -59,9 +59,41 @@ export declare class ImageProcessor {
59
59
  */
60
60
  static detectImageType(input: string | Buffer): string;
61
61
  /**
62
- * Validate image size (default 10MB limit)
62
+ * Throwing size guard for a raw byte length. Prevents memory exhaustion by
63
+ * rejecting an oversized input BEFORE an unbounded read/allocation happens
64
+ * (#257) — callers that can get a size cheaply (e.g. `fs.stat`) should
65
+ * validate it before ever reading the bytes into memory. Default limit is
66
+ * the canonical `SIZE_LIMITS_BYTES.IMAGE_MAX` (10 MB); there is no public
67
+ * way to raise it — `maxSize` is an internal-only override for advanced
68
+ * callers (e.g. video image inputs use a higher limit).
69
+ *
70
+ * @param size - Byte length to validate
71
+ * @param context - Short label identifying the source (unused in the
72
+ * thrown message today, kept for call-site readability and future use)
73
+ * @param maxSize - Max allowed size in bytes
74
+ * @throws NeuroLinkError (`INVALID_IMAGE_SIZE`) if `size` is not a finite,
75
+ * non-negative number
76
+ * @throws NeuroLinkError (`IMAGE_TOO_LARGE`) if `size` exceeds `maxSize`
77
+ */
78
+ static validateSize(size: number, context: string, maxSize?: number): void;
79
+ /**
80
+ * Throwing size guard for image buffers. Prevents memory exhaustion by
81
+ * rejecting oversized buffers with a descriptive error BEFORE an unbounded
82
+ * `Buffer.toString("base64")` allocation (#257). Delegates to
83
+ * `validateSize` so buffer-based and pre-read (stat-based) callers share
84
+ * one implementation.
85
+ *
86
+ * @param buffer - Image buffer to validate
87
+ * @param context - Short label identifying the source (see `validateSize`)
88
+ * @param maxSize - Max allowed size in bytes
89
+ * @throws NeuroLinkError (`IMAGE_TOO_LARGE`) if the buffer exceeds `maxSize`
90
+ */
91
+ static validateBufferSize(buffer: Buffer, context: string, maxSize?: number): void;
92
+ /**
93
+ * Size-guarded Buffer → base64. Use in place of `buffer.toString("base64")`
94
+ * on any path that converts a caller-supplied image buffer (#257).
63
95
  */
64
- static validateImageSize(data: Buffer | string, maxSize?: number): boolean;
96
+ static safeBase64Convert(buffer: Buffer, context: string, maxSize?: number): string;
65
97
  /**
66
98
  * Validate image format
67
99
  */
@@ -126,9 +158,15 @@ export declare const imageUtils: {
126
158
  */
127
159
  formatFileSize: (bytes: number) => string;
128
160
  /**
129
- * Convert Buffer to base64 string
161
+ * Convert Buffer to base64 string. Guarded by the same #257 size limit as
162
+ * every other conversion site (default `SIZE_LIMITS_BYTES.IMAGE_MAX`);
163
+ * pass `maxBytes` to override for a caller with a legitimate need for a
164
+ * different ceiling rather than silently accepting an unbounded buffer.
165
+ * `imageUtils`/`ImageProcessor` are internal-only — not re-exported from
166
+ * `src/lib/index.ts` or the package's public `exports` map — so this
167
+ * default does not change behavior for any external SDK consumer.
130
168
  */
131
- bufferToBase64: (buffer: Buffer) => string;
169
+ bufferToBase64: (buffer: Buffer, maxBytes?: number) => string;
132
170
  /**
133
171
  * Convert base64 string to Buffer
134
172
  */
@@ -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
  /**
@@ -50,11 +50,7 @@ export declare function buildMultimodalOptions(options: StreamOptions, providerN
50
50
  csvFiles: (string | Buffer<ArrayBufferLike>)[] | undefined;
51
51
  pdfFiles: (string | Buffer<ArrayBufferLike>)[] | undefined;
52
52
  };
53
- csvOptions: {
54
- maxRows?: number;
55
- formatStyle?: "raw" | "markdown" | "json";
56
- includeHeaders?: boolean;
57
- } | undefined;
53
+ csvOptions: import("../types/file.js").CSVProcessorOptions | undefined;
58
54
  systemPrompt: string | undefined;
59
55
  conversationHistory: import("../types/conversation.js").ChatMessage[] | undefined;
60
56
  provider: string;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Buffer → string decoding with encoding detection (#362).
3
+ *
4
+ * Order of resolution:
5
+ * 1. Explicit override (any iconv-lite label).
6
+ * 2. Deterministic BOM sniff (UTF-8 / UTF-16 LE / UTF-16 BE).
7
+ * 3. `chardet` statistical detection, mapped to an iconv-lite label.
8
+ * 4. Fallback to UTF-8 (preserves the historical hard-coded default).
9
+ *
10
+ * Decoding always goes through `iconv-lite`, and any residual BOM is stripped
11
+ * from the decoded string so it can't glue onto the first token.
12
+ */
13
+ import type { DecodedBuffer } from "../types/index.js";
14
+ /** Strip a leading BOM (U+FEFF) from an already-decoded string. */
15
+ export declare function stripBomString(text: string): string;
16
+ /**
17
+ * Decode a buffer to text, detecting the encoding unless overridden.
18
+ *
19
+ * @param isCompleteBuffer - Whether `buffer` is the entire source (default
20
+ * `true`). Pass `false` when `buffer` is only a peeked prefix (e.g. a
21
+ * streaming head-sniff) — the ASCII fast path still commits to "utf-8" for
22
+ * the peeked bytes (unchanged behavior), but reports a lower confidence
23
+ * since later bytes not yet seen could still be non-ASCII (#1199).
24
+ */
25
+ export declare function decodeBuffer(buffer: Buffer, override?: string, isCompleteBuffer?: boolean): DecodedBuffer;