@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
297 lines (296 loc) • 12.9 kB
TypeScript
/**
* Image processing utilities for multimodal support
* Handles format conversion for different AI providers
*/
import { redactPathFromMessage } from "./logSanitize.js";
import type { ProcessedImage, FileProcessingResult } from "../types/index.js";
/**
* Minimum bytes required just to detect an image format from magic bytes (#293).
*/
export declare const MIN_IMAGE_SIZE = 12;
/**
* Minimum plausible byte size for a non-empty image of each format (#293). A
* buffer that carries a format's magic bytes but is smaller than this is
* truncated/corrupt and would fail (or render blank) downstream — reject early
* with a clear message instead.
*
* Each threshold approximates the smallest byte count that format's
* container can structurally be while still holding the mandatory
* header/chunk skeleton for a minimal (e.g. 1×1 pixel) image — not an exact
* spec minimum, but close enough that anything smaller is unambiguously
* truncated:
* - **PNG (67)**: 8-byte signature + IHDR chunk (4 len + 4 type + 13 data +
* 4 CRC = 25) + a minimal IDAT chunk carrying one zlib-compressed
* scanline (~22 bytes) + IEND chunk (4 len + 4 type + 0 data + 4 CRC =
* 12) — matches the commonly-cited size of the smallest possible valid
* PNG (a 1×1 transparent pixel).
* - **JPEG (125)**: SOI + APP0/JFIF marker (18) + at least one DQT
* quantization table (~69 for one 8-bit luma table) + SOF0 + DHT + SOS
* headers + a minimal entropy-coded scan + EOI — the smallest legal
* baseline-JPEG skeleton for a 1×1 image.
* - **GIF (43)**: 6-byte GIF87a/89a header + 7-byte Logical Screen
* Descriptor + a 2-color (6-byte) color table + 10-byte Image
* Descriptor + minimal LZW-coded image sub-blocks + 1-byte trailer.
* - **WEBP (20)**: RIFF container header ("RIFF" + size + "WEBP" = 12
* bytes) + the first chunk's fourCC + size (8 bytes) — enough to confirm
* a well-formed RIFF/WEBP container and its first chunk, before any
* actual VP8/VP8L bitstream payload.
* - **AVIF (100)**: ISOBMFF `ftyp` box plus the mandatory `meta` box
* skeleton (`hdlr`/`pitm`/`iloc`/`iinf` sub-boxes) — roughly the minimum
* to hold AVIF's required box structure before any AV1 image item data.
*/
export declare const MIN_VALID_IMAGE_SIZE: Record<string, number>;
/**
* Image processor class for handling provider-specific image formatting
*/
export declare class ImageProcessor {
/**
* Process image Buffer (unified interface)
* Matches CSVProcessor.process() signature for consistency
*
* @param content - Image file as Buffer
* @param options - Processing options (unused for now)
* @returns Processed image as data URI
*/
static process(content: Buffer, _options?: unknown): Promise<FileProcessingResult>;
/**
* Strict magic-byte match: does `content` actually begin with `mediaType`'s
* signature? `detectImageType` returns `image/jpeg` as a *fallback* for
* unrecognized data, so a per-format minimum must only be enforced when the
* bytes genuinely match — otherwise valid BMP/TIFF/unknown buffers would be
* wrongly rejected as "truncated jpeg" (#293 review).
*/
private static hasStrictImageMagic;
/**
* Validate that a buffer is a plausibly-complete image (#293): non-empty and,
* for a strictly-identified raster format, at least the minimum plausible byte
* size. Returns the detected media type.
*
* @throws Error when the buffer is empty or a recognized format is truncated.
*/
private static validateBufferNotEmpty;
/**
* Validate processed output meets required format
* Checks:
* - Base64 content is non-empty
* - Data URI format is valid (data:{mimeType};base64,{content})
* - MIME type is in the allowed list
* @param dataUri - The complete data URI string
* @param base64 - The base64-encoded content
* @param mediaType - The MIME type of the image
* @throws Error if any validation fails
*/
private static validateProcessOutput;
/**
* Process image for OpenAI (requires data URI format)
*/
static processImageForOpenAI(image: Buffer | string): string;
/**
* Process image for Google AI (requires base64 without data URI prefix)
*/
static processImageForGoogle(image: Buffer | string): {
mimeType: string;
data: string;
};
/**
* Process image for Anthropic (requires base64 without data URI prefix)
*/
static processImageForAnthropic(image: Buffer | string): {
mediaType: string;
data: string;
};
/**
* Process image for Vertex AI (model-specific routing)
*/
static processImageForVertex(image: Buffer | string, model: string): {
mimeType?: string;
mediaType?: string;
data: string;
};
/**
* Detect image type from filename or data
*/
static detectImageType(input: string | Buffer): string;
/**
* Throwing size guard for a raw byte length. Prevents memory exhaustion by
* rejecting an oversized input BEFORE an unbounded read/allocation happens
* (#257) — callers that can get a size cheaply (e.g. `fs.stat`) should
* validate it before ever reading the bytes into memory. Default limit is
* the canonical `SIZE_LIMITS_BYTES.IMAGE_MAX` (10 MB); there is no public
* way to raise it — `maxSize` is an internal-only override for advanced
* callers (e.g. video image inputs use a higher limit).
*
* @param size - Byte length to validate
* @param context - Short label identifying the source (unused in the
* thrown message today, kept for call-site readability and future use)
* @param maxSize - Max allowed size in bytes
* @throws NeuroLinkError (`INVALID_IMAGE_SIZE`) if `size` is not a finite,
* non-negative number
* @throws NeuroLinkError (`IMAGE_TOO_LARGE`) if `size` exceeds `maxSize`
*/
static validateSize(size: number, context: string, maxSize?: number): void;
/**
* Throwing size guard for image buffers. Prevents memory exhaustion by
* rejecting oversized buffers with a descriptive error BEFORE an unbounded
* `Buffer.toString("base64")` allocation (#257). Delegates to
* `validateSize` so buffer-based and pre-read (stat-based) callers share
* one implementation.
*
* @param buffer - Image buffer to validate
* @param context - Short label identifying the source (see `validateSize`)
* @param maxSize - Max allowed size in bytes
* @throws NeuroLinkError (`IMAGE_TOO_LARGE`) if the buffer exceeds `maxSize`
*/
static validateBufferSize(buffer: Buffer, context: string, maxSize?: number): void;
/**
* Size-guarded Buffer → base64. Use in place of `buffer.toString("base64")`
* on any path that converts a caller-supplied image buffer (#257).
*/
static safeBase64Convert(buffer: Buffer, context: string, maxSize?: number): string;
/**
* Validate image format
*/
static validateImageFormat(mediaType: string): boolean;
/**
* Get image dimensions from Buffer (basic implementation)
*/
static getImageDimensions(buffer: Buffer): {
width: number;
height: number;
} | null;
/**
* Convert image to ProcessedImage format
*/
static processImage(image: Buffer | string, provider: string, model?: string): ProcessedImage;
}
/**
* Whitelist of valid image file extensions (lowercase, no dots).
* Used to validate file extensions against a known set of image formats.
*/
export declare const VALID_IMAGE_EXTENSIONS: string[];
/**
* Utility functions for image handling
*/
export declare const imageUtils: {
/**
* Check if a string is a valid data URI
*/
isDataUri: (str: string) => boolean;
/**
* Check if a string is a valid URL
*/
isUrl: (str: string) => boolean;
/**
* Check if a string is base64 encoded
*/
isBase64: (str: string) => boolean;
/**
* Extract file extension from filename or URL.
* Strips query strings and fragments before matching so that
* "image.jpg?v=1" correctly returns "jpg".
* Returns null if no extension is found or if the extension
* contains non-alphanumeric characters.
*/
getFileExtension: (filename: string) => string | null;
/**
* Validate that an extension is a recognised image format.
* Case-insensitive; rejects extensions with special characters.
*/
isValidImageExtension: (extension: string) => boolean;
/**
* Extract and validate image file extension from a filename or URL.
* Returns null if the extension is missing or not a recognised image format.
*
* Security note: the last extension is used, so "malware.exe.jpg" returns
* "jpg". Callers should apply additional checks (e.g. content inspection)
* where double-extension attacks are a concern.
*/
getValidatedImageExtension: (filename: string) => string | null;
/**
* Convert file size to human readable format
*/
formatFileSize: (bytes: number) => string;
/**
* Convert Buffer to base64 string. Guarded by the same #257 size limit as
* every other conversion site (default `SIZE_LIMITS_BYTES.IMAGE_MAX`);
* pass `maxBytes` to override for a caller with a legitimate need for a
* different ceiling rather than silently accepting an unbounded buffer.
* `imageUtils`/`ImageProcessor` are internal-only — not re-exported from
* `src/lib/index.ts` or the package's public `exports` map — so this
* default does not change behavior for any external SDK consumer.
*/
bufferToBase64: (buffer: Buffer, maxBytes?: number) => string;
/**
* Convert base64 string to Buffer
*/
base64ToBuffer: (base64: string) => Buffer;
/**
* Redact `filePath` (both as given and its `path.resolve()`'d form) from
* an error message. Exposed directly (not just used internally by
* {@link fileToBase64DataUri}) so the resolved-path branch can be verified
* by a deterministic unit test — real `fs` errors only ever embed the
* literal path as passed, never a resolved variant, so that branch isn't
* otherwise observable through the async file-reading API.
*/
redactPathFromMessage: typeof redactPathFromMessage;
/**
* Convert file path to base64 data URI
*/
fileToBase64DataUri: (filePath: string, maxBytes?: number) => Promise<string>;
/**
* Convert URL to base64 data URI by downloading the image.
* Implements retry logic with exponential backoff for network errors.
*
* Retries are performed for:
* - Network errors (ECONNRESET, ENOTFOUND, ECONNREFUSED, ETIMEDOUT, ERR_NETWORK, AbortError)
* - Server errors (5xx status codes)
* - Rate limiting (429 Too Many Requests)
* - Request timeouts (408 Request Timeout)
*
* Retries are NOT performed for:
* - Client errors (4xx status codes except 408, 429)
* - Invalid content type
* - Content size limit exceeded
* - Unsupported protocol
*
* @param url - The URL of the image to download
* @param options - Configuration options
* @param options.timeoutMs - Timeout for each download attempt (default: 15000ms)
* @param options.maxBytes - Maximum allowed file size (default: 10MB)
* @param options.maxAttempts - Maximum number of total attempts including initial attempt (default: 3)
* @returns Promise<string> - Base64 data URI of the downloaded image
* Rate-limited to 10 downloads per second to prevent DoS
* Uses LRU cache to avoid redundant downloads of the same URL
*/
urlToBase64DataUri: (url: string, { timeoutMs, maxBytes, maxAttempts, }?: {
timeoutMs?: number;
maxBytes?: number;
maxAttempts?: number;
}) => Promise<string>;
/**
* Extract base64 data from data URI
*/
extractBase64FromDataUri: (dataUri: string) => string;
/**
* Extract MIME type from data URI
*/
extractMimeTypeFromDataUri: (dataUri: string) => string;
/**
* Create data URI from base64 and MIME type
*/
createDataUri: (base64: string, mimeType?: string) => string;
/**
* Validate base64 string format
* Validates format BEFORE buffer allocation to prevent memory exhaustion
*/
isValidBase64: (str: string) => boolean;
/**
* Get base64 string size in bytes
*/
getBase64Size: (base64: string) => number;
/**
* Compress base64 image by reducing quality (basic implementation)
* Note: This is a placeholder - for production use, consider using sharp or similar
*/
compressBase64: (base64: string, _quality?: number) => string;
};