knowledgegraph-mcp
Version:
MCP server for enabling persistent knowledge storage for Claude through a knowledge graph with multiple storage backends
244 lines • 8.97 kB
JavaScript
/**
* File attachment manager for the knowledge graph system
*/
import * as crypto from 'crypto';
import { DEFAULT_FILE_STORAGE_CONFIG } from '../types/file-attachments.js';
/**
* File validator for security and type checking
*/
export class FileValidator {
allowedMimeTypes;
maxFileSize;
constructor(config = DEFAULT_FILE_STORAGE_CONFIG) {
this.allowedMimeTypes = new Set(config.allowedMimeTypes);
this.maxFileSize = config.maxFileSize;
}
/**
* Validate a file buffer and metadata
*/
async validateFile(file, originalFilename, providedMimeType) {
try {
// 1. Size validation
if (file.length > this.maxFileSize) {
return {
isValid: false,
mimeType: providedMimeType || 'application/octet-stream',
hash: '',
error: `File too large: ${file.length} bytes (max: ${this.maxFileSize})`
};
}
// 2. MIME type detection and validation
const detectedMimeType = providedMimeType || this.detectMimeType(file, originalFilename);
if (!this.allowedMimeTypes.has(detectedMimeType)) {
return {
isValid: false,
mimeType: detectedMimeType,
hash: '',
error: `File type not allowed: ${detectedMimeType}`
};
}
// 3. Basic security checks
const securityCheck = this.performSecurityChecks(file, originalFilename);
if (!securityCheck.isValid) {
return {
isValid: false,
mimeType: detectedMimeType,
hash: '',
error: securityCheck.error
};
}
// 4. Generate integrity hash
const hash = crypto.createHash('sha256').update(file).digest('hex');
return {
isValid: true,
mimeType: detectedMimeType,
hash
};
}
catch (error) {
return {
isValid: false,
mimeType: providedMimeType || 'application/octet-stream',
hash: '',
error: `Validation failed: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* Simple MIME type detection based on file extension and content
*/
detectMimeType(file, filename) {
const ext = filename.toLowerCase().split('.').pop() || '';
// Check file signature (magic numbers) for common types
if (file.length >= 4) {
const signature = file.subarray(0, 4);
// PDF
if (signature.toString('ascii', 0, 4) === '%PDF') {
return 'application/pdf';
}
// PNG
if (signature[0] === 0x89 && signature[1] === 0x50 && signature[2] === 0x4E && signature[3] === 0x47) {
return 'image/png';
}
// JPEG
if (signature[0] === 0xFF && signature[1] === 0xD8) {
return 'image/jpeg';
}
// ZIP/Office documents
if (signature[0] === 0x50 && signature[1] === 0x4B) {
if (ext === 'docx')
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
if (ext === 'xlsx')
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
if (ext === 'pptx')
return 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
return 'application/zip';
}
}
// Fallback to extension-based detection
const mimeTypeMap = {
'txt': 'text/plain',
'md': 'text/markdown',
'json': 'application/json',
'xml': 'application/xml',
'html': 'text/html',
'css': 'text/css',
'js': 'text/javascript',
'ts': 'application/typescript',
'py': 'application/x-python',
'sh': 'application/x-sh',
'yaml': 'application/x-yaml',
'yml': 'application/x-yaml',
'csv': 'text/csv',
'pdf': 'application/pdf',
'doc': 'application/msword',
'xls': 'application/vnd.ms-excel',
'ppt': 'application/vnd.ms-powerpoint',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'png': 'image/png',
'gif': 'image/gif',
'webp': 'image/webp',
'svg': 'image/svg+xml',
'zip': 'application/zip',
'tar': 'application/x-tar',
'gz': 'application/gzip',
};
return mimeTypeMap[ext] || 'application/octet-stream';
}
/**
* Perform basic security checks on the file
*/
performSecurityChecks(file, filename) {
// Check for potentially dangerous file extensions
const dangerousExtensions = ['.exe', '.bat', '.cmd', '.com', '.scr', '.pif', '.vbs', '.js', '.jar'];
const ext = filename.toLowerCase();
for (const dangerous of dangerousExtensions) {
if (ext.endsWith(dangerous)) {
return {
isValid: false,
error: `Potentially dangerous file type: ${dangerous}`
};
}
}
// Check for embedded executables in common file types
if (file.length >= 2) {
// Check for MZ header (Windows executable)
if (file[0] === 0x4D && file[1] === 0x5A) {
return {
isValid: false,
error: 'File contains executable code'
};
}
}
// Check for script injection in text files
if (filename.toLowerCase().endsWith('.txt') || filename.toLowerCase().endsWith('.md')) {
const content = file.toString('utf8', 0, Math.min(file.length, 1024));
if (content.includes('<script') || content.includes('javascript:')) {
return {
isValid: false,
error: 'Text file contains potentially malicious script content'
};
}
}
return { isValid: true };
}
}
/**
* File attachment manager
*/
export class FileAttachmentManager {
validator;
config;
constructor(config = DEFAULT_FILE_STORAGE_CONFIG) {
this.config = config;
this.validator = new FileValidator(config);
}
/**
* Process and validate a file upload
*/
async processFileUpload(file, entityName, project) {
try {
// Validate the file
const validation = await this.validator.validateFile(file.buffer, file.originalname, file.mimetype);
if (!validation.isValid) {
return { error: validation.error || 'File validation failed' };
}
// Generate unique file ID
const fileId = crypto.randomUUID();
// Create file attachment metadata
const fileAttachment = {
fileId,
originalFilename: file.originalname,
mimeType: validation.mimeType,
fileSize: file.buffer.length,
fileHash: validation.hash,
uploadDate: new Date(),
metadata: {
entityName,
project,
uploadedAt: new Date().toISOString()
}
};
return { fileAttachment, fileBuffer: file.buffer };
}
catch (error) {
return {
error: `Failed to process file upload: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* Validate file count limits for an entity
*/
validateFileCount(currentAttachments) {
if (currentAttachments.length >= this.config.maxFilesPerEntity) {
return {
isValid: false,
error: `Maximum number of files per entity exceeded (${this.config.maxFilesPerEntity})`
};
}
return { isValid: true };
}
/**
* Calculate total storage used by an entity
*/
calculateEntityStorageUsage(attachments) {
return attachments.reduce((total, attachment) => total + attachment.fileSize, 0);
}
/**
* Generate file metadata for storage
*/
createFileMetadata(entityName, project, originalFilename, mimeType) {
return {
entityName,
project,
originalFilename,
mimeType,
metadata: {
uploadedAt: new Date().toISOString()
}
};
}
}
//# sourceMappingURL=file-manager.js.map