// pages/api/mcp-handler.js - Updated for Passkey System import fs from 'fs/promises' import fssync from 'fs'; import path from 'path'; // Use /data for Hugging Face Spaces persistent storage const DATA_DIR = process.env.SPACE_ID ? '/data' : path.join(process.cwd(), 'public', 'data'); const PUBLIC_DIR = path.join(DATA_DIR, 'public'); // Ensure directories exist async function ensureDirectories() { if (!fssync.existsSync(DATA_DIR)) { await fs.mkdir(DATA_DIR, { recursive: true }); } if (!fssync.existsSync(PUBLIC_DIR)) { await fs.mkdir(PUBLIC_DIR, { recursive: true }); } } // Validate passkey format (alphanumeric and hyphens/underscores only) function isValidPasskey(passkey) { return passkey && /^[a-zA-Z0-9_-]+$/.test(passkey) && passkey.length >= 4; } // Sanitize filename to prevent path traversal function sanitizeFileName(fileName) { return fileName.replace(/[^a-zA-Z0-9._-]/g, '_'); } // Sanitize passkey (used for directory names) function sanitizePasskey(passkey) { return passkey.replace(/[^a-zA-Z0-9_-]/g, ''); } export default async function handler(req, res) { // Enable CORS res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { return res.status(200).end(); } try { await ensureDirectories(); if (req.method === 'POST') { const { passkey, action, fileName, content, isPublic = false } = req.body; // Public files don't need passkey if (!isPublic && !isValidPasskey(passkey)) { return res.status(400).json({ success: false, error: 'Invalid or missing passkey (minimum 4 characters required)' }); } // Validate required fields if (!action || !fileName || content === undefined) { return res.status(400).json({ success: false, error: 'Missing required fields: action, fileName, or content' }); } const sanitizedFileName = sanitizeFileName(fileName); try { let targetDir; let responseData = {}; if (isPublic) { targetDir = PUBLIC_DIR; responseData.isPublic = true; responseData.location = 'Public Files'; } else { const sanitizedKey = sanitizePasskey(passkey); targetDir = path.join(DATA_DIR, sanitizedKey); if (!fssync.existsSync(targetDir)) { await fs.mkdir(targetDir, { recursive: true }); } responseData.passkey = sanitizedKey; responseData.location = 'Secure Data'; } // Handle different actions switch (action) { case 'save_file': case 'deploy_quiz': case 'save': { const filePath = path.join(targetDir, sanitizedFileName); await fs.writeFile(filePath, content, 'utf8'); return res.status(200).json({ success: true, message: `File saved successfully to ${responseData.location}`, fileName: sanitizedFileName, action: action, ...responseData, url: isPublic ? `${process.env.SPACE_ID ? 'https://mcp-1st-birthday-reuben-os.hf.space' : 'http://localhost:3000'}/data/public/${sanitizedFileName}` : null }); } case 'delete_file': case 'delete': { const filePath = path.join(targetDir, sanitizedFileName); try { await fs.unlink(filePath); return res.status(200).json({ success: true, message: `File deleted successfully`, fileName: sanitizedFileName, ...responseData }); } catch (err) { return res.status(404).json({ success: false, error: 'File not found' }); } } case 'clear_session': case 'clear': { if (isPublic) { return res.status(400).json({ success: false, error: 'Cannot clear public folder via this endpoint' }); } const files = await fs.readdir(targetDir); let deletedCount = 0; for (const file of files) { await fs.unlink(path.join(targetDir, file)); deletedCount++; } return res.status(200).json({ success: true, message: `Cleared ${deletedCount} files`, deletedCount, ...responseData }); } default: return res.status(400).json({ success: false, error: `Unknown action: ${action}` }); } } catch (error) { console.error('File operation error:', error); return res.status(500).json({ success: false, error: `Failed to ${action}: ${error.message}` }); } } else if (req.method === 'GET') { const { passkey, isPublic } = req.query; let targetDir; let responseData = {}; if (isPublic === 'true') { targetDir = PUBLIC_DIR; responseData.isPublic = true; responseData.location = 'Public Files'; } else { if (!isValidPasskey(passkey)) { return res.status(400).json({ success: false, error: 'Invalid or missing passkey' }); } const sanitizedKey = sanitizePasskey(passkey); targetDir = path.join(DATA_DIR, sanitizedKey); if (!fssync.existsSync(targetDir)) { return res.status(200).json({ success: true, passkey: sanitizedKey, files: [], count: 0, message: 'No files found for this passkey' }); } responseData.passkey = sanitizedKey; responseData.location = 'Secure Data'; } try { const allFiles = await fs.readdir(targetDir); const fileList = await Promise.all( allFiles.map(async (file) => { const filePath = path.join(targetDir, file); const stats = await fs.stat(filePath); // Read file content (with size limit) let content = null; if (stats.size < 1024 * 1024) { // 1MB limit try { content = await fs.readFile(filePath, 'utf8'); } catch (err) { console.error(`Error reading file ${file}:`, err); } } return { name: file, size: stats.size, modified: stats.mtime, isQuiz: file === 'quiz.json', content: content, extension: path.extname(file).substring(1) }; }) ); // Sort by modification time (newest first) fileList.sort((a, b) => new Date(b.modified) - new Date(a.modified)); return res.status(200).json({ success: true, files: fileList, count: fileList.length, ...responseData }); } catch (error) { console.error('Error reading files:', error); return res.status(500).json({ success: false, error: `Failed to retrieve files: ${error.message}` }); } } else { return res.status(405).json({ success: false, error: 'Method not allowed' }); } } catch (error) { console.error('Unexpected error:', error); return res.status(500).json({ success: false, error: `Server error: ${error.message}` }); } }