/** * Shared attachment content-type configuration. * * By default only image types are allowed. Set the * `PAPERCLIP_ALLOWED_ATTACHMENT_TYPES` environment variable to a * comma-separated list of MIME types or wildcard patterns to expand the * allowed set. * * Examples: * PAPERCLIP_ALLOWED_ATTACHMENT_TYPES=image/*,application/pdf * PAPERCLIP_ALLOWED_ATTACHMENT_TYPES=image/*,application/pdf,text/* * * Supported pattern syntax: * - Exact types: "application/pdf" * - Wildcards: "image/*" or "application/vnd.openxmlformats-officedocument.*" */ export const DEFAULT_ALLOWED_TYPES: readonly string[] = [ "image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif", "application/pdf", "text/markdown", "text/plain", "application/json", "text/csv", "text/html", ]; /** * Parse a comma-separated list of MIME type patterns into a normalised array. * Returns the default image-only list when the input is empty or undefined. */ export function parseAllowedTypes(raw: string | undefined): string[] { if (!raw) return [...DEFAULT_ALLOWED_TYPES]; const parsed = raw .split(",") .map((s) => s.trim().toLowerCase()) .filter((s) => s.length > 0); return parsed.length > 0 ? parsed : [...DEFAULT_ALLOWED_TYPES]; } /** * Check whether `contentType` matches any entry in `allowedPatterns`. * * Supports exact matches ("application/pdf") and wildcard / prefix * patterns ("image/*", "application/vnd.openxmlformats-officedocument.*"). */ export function matchesContentType(contentType: string, allowedPatterns: string[]): boolean { const ct = contentType.toLowerCase(); return allowedPatterns.some((pattern) => { if (pattern === "*") return true; if (pattern.endsWith("/*") || pattern.endsWith(".*")) { return ct.startsWith(pattern.slice(0, -1)); } return ct === pattern; }); } // ---------- Module-level singletons read once at startup ---------- const allowedPatterns: string[] = parseAllowedTypes( process.env.PAPERCLIP_ALLOWED_ATTACHMENT_TYPES, ); /** Convenience wrapper using the process-level allowed list. */ export function isAllowedContentType(contentType: string): boolean { return matchesContentType(contentType, allowedPatterns); } export const MAX_ATTACHMENT_BYTES = Number(process.env.PAPERCLIP_ATTACHMENT_MAX_BYTES) || 10 * 1024 * 1024;