UNPKG

@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
/** * 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; };