UNPKG

graphql-upload-ts

Version:

TypeScript-first middleware and Upload scalar for GraphQL multipart requests (file uploads) with support for Apollo Server, Express, Koa, and more.

136 lines (111 loc) 4.17 kB
import type { UploadOptions } from './process-request'; export interface ValidationResult { isValid: boolean; error?: string; } /** * Maximum size for non-file form fields (in bytes). * This applies to text fields like 'operations' and 'map' in GraphQL multipart requests. * These fields contain JSON data (query, variables, and file mappings). * Default: 1MB - sufficient for large GraphQL queries and variable sets. */ export const DEFAULT_MAX_FIELD_SIZE = 1_000_000; // 1MB /** * Maximum size for uploaded files (in bytes). * This applies to the actual binary file content being uploaded. * Default: 5MB - a reasonable limit for most web applications. * Can be overridden per request for larger files (videos, high-res images, etc.). */ export const DEFAULT_MAX_FILE_SIZE = 5_000_000; // 5MB /** * Maximum number of files that can be uploaded in a single request. * Default: Infinity - no limit on the number of files. * Should be set to a reasonable number in production to prevent abuse. */ export const DEFAULT_MAX_FILES = Number.POSITIVE_INFINITY; export function validateOptions(options?: UploadOptions): UploadOptions { const validatedOptions: Required<UploadOptions> = { maxFieldSize: options?.maxFieldSize ?? DEFAULT_MAX_FIELD_SIZE, maxFileSize: options?.maxFileSize ?? DEFAULT_MAX_FILE_SIZE, maxFiles: options?.maxFiles ?? DEFAULT_MAX_FILES, }; if (validatedOptions.maxFieldSize <= 0) { throw new Error('maxFieldSize must be a positive number'); } if (validatedOptions.maxFileSize <= 0) { throw new Error('maxFileSize must be a positive number'); } if (validatedOptions.maxFiles <= 0) { throw new Error('maxFiles must be a positive number'); } if ( !Number.isInteger(validatedOptions.maxFiles) && validatedOptions.maxFiles !== Number.POSITIVE_INFINITY ) { throw new Error('maxFiles must be an integer'); } return validatedOptions; } export function validateMimeType(mimetype: string, allowedTypes?: string[]): ValidationResult { if (!allowedTypes || allowedTypes.length === 0) { return { isValid: true }; } const isValid = allowedTypes.some((type) => { if (type.endsWith('/*')) { const prefix = type.slice(0, -2); return mimetype.startsWith(`${prefix}/`); } return mimetype === type; }); return { isValid, error: isValid ? undefined : `File type '${mimetype}' is not allowed`, }; } export function validateFileExtension( filename: string, allowedExtensions?: string[] ): ValidationResult { if (!allowedExtensions || allowedExtensions.length === 0) { return { isValid: true }; } const extension = filename.split('.').pop()?.toLowerCase(); if (!extension) { return { isValid: false, error: 'File must have an extension', }; } const isValid = allowedExtensions.some((ext) => ext.toLowerCase() === extension); return { isValid, error: isValid ? undefined : `File extension '.${extension}' is not allowed`, }; } export function sanitizeFilename(filename: string): string { // Remove path traversal attempts let sanitized = filename.replace(/\.\./g, ''); // Remove directory separators sanitized = sanitized.replace(/[/\\]/g, ''); // Remove control characters and non-printable characters // biome-ignore lint/suspicious/noControlCharactersInRegex: Intentional to remove control characters sanitized = sanitized.replace(/[\x00-\x1f\x7f-\x9f]/g, ''); // Remove leading/trailing dots and spaces sanitized = sanitized.replace(/^[\s.]+|[\s.]+$/g, ''); // If filename is empty after sanitization, generate a default name if (!sanitized) { sanitized = `upload_${Date.now()}`; } // Limit filename length const maxLength = 255; if (sanitized.length > maxLength) { const extension = sanitized.split('.').pop(); const nameWithoutExt = sanitized.substring(0, sanitized.lastIndexOf('.')); if (extension && extension.length < 20) { sanitized = `${nameWithoutExt.substring(0, maxLength - extension.length - 1)}.${extension}`; } else { sanitized = sanitized.substring(0, maxLength); } } return sanitized; }