export const ALLOWED_MIME_TYPES = new Set([ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/plain', 'text/csv', ]); export const MIME_TO_EXT: Record = { 'image/jpeg': 'jpg', 'image/png': 'png', 'image/gif': 'gif', 'image/webp': 'webp', 'application/pdf': 'pdf', 'application/msword': 'doc', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx', 'application/vnd.ms-excel': 'xls', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx', 'text/plain': 'txt', 'text/csv': 'csv', }; export const MAX_FILE_SIZE = 52_428_800; // 50MB export const PREVIEWABLE_MIMES = new Set([ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf', ]); /** * Magic-byte signatures keyed by claimed MIME type. Used by the file * upload handler to reject files whose first few bytes don't match the * MIME the browser declared. Without this, a `
` could lie about * Content-Type and pass arbitrary bytes through ALLOWED_MIME_TYPES. * * Each signature is the leading prefix of the file. When multiple variants * exist (e.g. JPEG SOI + APPn marker), we accept any of them. */ export const MAGIC_BYTE_SIGNATURES: Record = { 'image/jpeg': [new Uint8Array([0xff, 0xd8, 0xff])], 'image/png': [new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])], 'image/gif': [ new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x37, 0x61]), // GIF87a new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]), // GIF89a ], 'image/webp': [new Uint8Array([0x52, 0x49, 0x46, 0x46])], // RIFF; WEBP signature follows at offset 8 'application/pdf': [new Uint8Array([0x25, 0x50, 0x44, 0x46])], // %PDF // Office formats are zip-based (modern: docx/xlsx) or OLE (legacy: doc/xls). // Both share well-known magic bytes — match either family for a given MIME. 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': [ new Uint8Array([0x50, 0x4b, 0x03, 0x04]), // PK\3\4 (zip) new Uint8Array([0x50, 0x4b, 0x05, 0x06]), // empty archive ], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': [ new Uint8Array([0x50, 0x4b, 0x03, 0x04]), new Uint8Array([0x50, 0x4b, 0x05, 0x06]), ], 'application/msword': [ new Uint8Array([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]), // OLE compound ], 'application/vnd.ms-excel': [new Uint8Array([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1])], // text/plain and text/csv have no magic bytes — leave unconstrained; // size cap + ALLOWED_MIME_TYPES allow-list is the only gate. }; /** Returns true when the buffer starts with one of the registered prefixes * for the given MIME, or when the MIME has no signature requirement. */ export function bufferMatchesMime(buffer: Buffer, mime: string): boolean { const sigs = MAGIC_BYTE_SIGNATURES[mime]; if (!sigs) return true; // text/plain, text/csv, or unrecognised allow-list entry return sigs.some((sig) => { if (buffer.length < sig.length) return false; for (let i = 0; i < sig.length; i++) { if (buffer[i] !== sig[i]) return false; } return true; }); }