@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.
- package/CHANGELOG.md +6 -0
- package/dist/browser/neurolink.min.js +343 -343
- package/dist/lib/utils/errorHandling.d.ts +9 -0
- package/dist/lib/utils/errorHandling.js +21 -0
- package/dist/lib/utils/imageProcessor.d.ts +42 -4
- package/dist/lib/utils/imageProcessor.js +96 -19
- package/dist/lib/utils/messageBuilder.js +49 -11
- package/dist/utils/errorHandling.d.ts +9 -0
- package/dist/utils/errorHandling.js +21 -0
- package/dist/utils/imageProcessor.d.ts +42 -4
- package/dist/utils/imageProcessor.js +96 -19
- package/dist/utils/messageBuilder.js +49 -11
- package/package.json +1 -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";
|
|
@@ -199,6 +200,14 @@ export declare class ErrorFactory {
|
|
|
199
200
|
* Create an image too large error
|
|
200
201
|
*/
|
|
201
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;
|
|
202
211
|
/**
|
|
203
212
|
* Create an image too small error
|
|
204
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
|
|
@@ -570,6 +571,26 @@ export class ErrorFactory {
|
|
|
570
571
|
},
|
|
571
572
|
});
|
|
572
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
|
+
}
|
|
573
594
|
/**
|
|
574
595
|
* Create an image too small error
|
|
575
596
|
*/
|
|
@@ -59,9 +59,41 @@ export declare class ImageProcessor {
|
|
|
59
59
|
*/
|
|
60
60
|
static detectImageType(input: string | Buffer): string;
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
*
|
|
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
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
1402
|
-
|
|
1405
|
+
async function convertFilePathToBase64(filePath) {
|
|
1406
|
+
let stats;
|
|
1407
|
+
try {
|
|
1408
|
+
stats = await statAsync(filePath);
|
|
1403
1409
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
-
|
|
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
|
/**
|
|
@@ -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";
|
|
@@ -199,6 +200,14 @@ export declare class ErrorFactory {
|
|
|
199
200
|
* Create an image too large error
|
|
200
201
|
*/
|
|
201
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;
|
|
202
211
|
/**
|
|
203
212
|
* Create an image too small error
|
|
204
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
|
|
@@ -570,6 +571,26 @@ export class ErrorFactory {
|
|
|
570
571
|
},
|
|
571
572
|
});
|
|
572
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
|
+
}
|
|
573
594
|
/**
|
|
574
595
|
* Create an image too small error
|
|
575
596
|
*/
|
|
@@ -59,9 +59,41 @@ export declare class ImageProcessor {
|
|
|
59
59
|
*/
|
|
60
60
|
static detectImageType(input: string | Buffer): string;
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
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
|
|
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
|
*/
|