UNPKG

@flavoai/fastfold

Version:

Flavo frontend package

410 lines 15.4 kB
import express from 'express'; import cors from 'cors'; import jwt from 'jsonwebtoken'; import { createDatabaseAdapter } from '../database/adapters'; import { CrudGenerator } from '../crud/generator'; import { ApiDocumentationGenerator, ApiExplorer } from '../docs'; export class FastfoldServer { app; config; db; crudGenerator; docGenerator; apiExplorer; constructor(config) { this.config = config; this.app = express(); this.app.set('trust proxy', true); this.setupMiddleware(); this.db = createDatabaseAdapter(config.database.provider, config.database.connection); this.crudGenerator = new CrudGenerator(this.db, config.tables); this.docGenerator = new ApiDocumentationGenerator(config); this.apiExplorer = new ApiExplorer(this.docGenerator); } /** * Initialize the server - connect to database and create tables */ async initialize() { await this.db.connect(); // Create all tables based on schema for (const [tableName, tableDefinition] of Object.entries(this.config.tables)) { await this.db.createTable(tableName, tableDefinition.schema); } this.setupRoutes(); } /** * Start the server */ async start(port) { await this.initialize(); const serverPort = port || this.config.port || 3001; this.app.listen(serverPort, () => { console.log(`🚀 Fastfold server running on http://localhost:${serverPort}`); console.log(`📊 Database: ${this.config.database.provider}`); console.log(`🔧 Tables: ${Object.keys(this.config.tables).join(', ')}`); }); } /** * Get the Express app instance */ getApp() { return this.app; } /** * Setup middleware */ setupMiddleware() { // Security headers previously provided by helmet have been removed this.app.use(cors()); // Body parsing this.app.use(express.json({ limit: '10mb' })); this.app.use(express.urlencoded({ extended: true })); // Request logging this.app.use((req, res, next) => { console.log(`${req.method} ${req.path}`); next(); }); // User provided additional middleware (applied before auth and routes) if (Array.isArray(this.config.middleware)) { for (const mw of this.config.middleware) { if (typeof mw === 'function') this.app.use(mw); } } // Auth middleware if (this.config.auth?.providers?.flavo) { this.setupFlavoAuth(); } else if (this.config.auth?.providers?.custom) { this.app.use(this.authMiddleware.bind(this)); } } /** * Flavo delegated auth: verify JWTs locally using the app's own public key. */ setupFlavoAuth() { const flavoSettings = this.config.auth?.providers?.flavo; const flavoAppId = flavoSettings?.appId || process.env.VITE_FASTFOLD_APP_ID || process.env.FLAVO_CONVERSATION_ID; const flavoGatewayUrl = (process.env.FLAVO_GATEWAY_URL || 'https://flavo.ai').replace(/\/$/, ''); if (!flavoAppId) { console.warn('[Fastfold] providers.flavo.appId is required for Flavo auth, or set VITE_FASTFOLD_APP_ID / FLAVO_CONVERSATION_ID env var.'); } const jwtPublicKeyPem = process.env.FLAVO_PUBLIC_KEY?.replace(/\\n/g, '\n'); if (!jwtPublicKeyPem) { console.warn('[Fastfold] FLAVO_PUBLIC_KEY env var is required for Flavo auth. Auth middleware will not verify tokens.'); return; } this.app.use((req, _res, next) => { const authHeader = req.headers.authorization; if (authHeader && authHeader.startsWith('Bearer ')) { const token = authHeader.substring(7); try { const payload = jwt.verify(token, jwtPublicKeyPem, { algorithms: ['RS256'], }); req.user = { id: payload.email, email: payload.email, displayName: payload.displayName || '', avatar: payload.avatar || '', authProvider: payload.provider || '', role: 'user', meta: {}, }; } catch (_err) { // Invalid or expired token — continue without user } } next(); }); const flavoAppToken = process.env.FLAVO_APP_TOKEN; const flavoInitiateUrl = `${flavoGatewayUrl}/api/app/auth/initiate`; this.app.get('/api/auth/login', async (req, res) => { if (!flavoAppToken) { return res.status(500).json({ success: false, error: 'FLAVO_APP_TOKEN env var is not set. Cannot initiate login.', }); } const redirectOrigin = `${req.protocol}://${req.get('host')}`; try { const response = await fetch(flavoInitiateUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ appId: flavoAppId, appToken: flavoAppToken, redirectOrigin }), }); if (!response.ok) { const fallbackMessage = `Failed to initiate login with Flavo (status ${response.status})`; let errorMessage = fallbackMessage; let errorCode; try { const body = await response.json(); if (typeof body.error === 'string' && body.error.trim().length > 0) { errorMessage = body.error; } if (typeof body.code === 'string' && body.code.trim().length > 0) { errorCode = body.code; } console.error(`[Fastfold] Failed to initiate login (${response.status}):`, body); } catch { console.error(`[Fastfold] Failed to initiate login (${response.status}) with non-JSON response.`); } const statusCode = response.status >= 400 && response.status <= 599 ? response.status : 502; const payload = { success: false, error: errorMessage, upstreamStatus: response.status, }; if (errorCode) { payload.code = errorCode; } return res.status(statusCode).json(payload); } const { redirectUrl } = await response.json(); res.redirect(redirectUrl); } catch (err) { console.error('[Fastfold] Error contacting Flavo for login initiation:', err); return res.status(502).json({ success: false, error: 'Unable to reach Flavo for login initiation', }); } }); this.app.get('/api/auth/me', (req, res) => { if (req.user) { return res.json({ user: req.user }); } return res.status(401).json({ error: 'Not authenticated' }); }); } /** * HS256 authentication middleware (original behavior) */ authMiddleware(req, _res, next) { const authHeader = req.headers.authorization; const secret = this.config.auth?.providers?.custom?.secret; if (authHeader && authHeader.startsWith('Bearer ') && secret) { const token = authHeader.substring(7); try { const decoded = jwt.verify(token, secret); req.user = decoded; } catch (error) { console.warn('Invalid JWT token:', error); } } next(); } /** * Setup API routes for all tables */ setupRoutes() { // Health check this.app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString() }); }); // API Documentation endpoints this.app.get('/docs', (req, res) => { res.setHeader('Content-Type', 'text/html'); res.send(this.apiExplorer.generateHtml()); }); this.app.get('/docs/json', (req, res) => { res.json(this.docGenerator.generateDocumentation()); }); // Generate routes for each table for (const tableName of Object.keys(this.config.tables)) { this.setupTableRoutes(tableName); } // 404 handler this.app.use((req, res) => { res.status(404).json({ success: false, error: `Route not found: ${req.method} ${req.path}` }); }); // Error handler this.app.use((error, req, res, next) => { console.error('Server error:', error); res.status(500).json({ success: false, error: error.message || 'Internal server error' }); }); } /** * Setup CRUD routes for a specific table */ setupTableRoutes(tableName) { const operations = this.crudGenerator.generateTableOperations(tableName); const basePath = `/api/${tableName}`; // GET /api/:table - Find many records this.app.get(basePath, async (req, res) => { try { const params = req.query.params ? JSON.parse(req.query.params) : {}; const context = this.createRequestContext(req, res); const data = await operations.findMany(params, context); const count = await operations.count(params.where, context); res.json({ success: true, data, count }); } catch (error) { this.handleError(error, res); } }); // GET /api/:table/:id - Find one record this.app.get(`${basePath}/:id`, async (req, res) => { try { const id = this.parseId(req.params.id); const context = this.createRequestContext(req, res); const data = await operations.findOne(id, context); res.json({ success: true, data }); } catch (error) { this.handleError(error, res); } }); // POST /api/:table - Create record this.app.post(basePath, async (req, res) => { try { const context = this.createRequestContext(req, res); const data = await operations.create(req.body, context); res.status(201).json({ success: true, data }); } catch (error) { this.handleError(error, res); } }); // PUT /api/:table/:id - Update record this.app.put(`${basePath}/:id`, async (req, res) => { try { const id = this.parseId(req.params.id); const context = this.createRequestContext(req, res); const data = await operations.update(id, req.body, context); res.json({ success: true, data }); } catch (error) { this.handleError(error, res); } }); // DELETE /api/:table/:id - Delete record this.app.delete(`${basePath}/:id`, async (req, res) => { try { const id = this.parseId(req.params.id); const context = this.createRequestContext(req, res); const success = await operations.delete(id, context); res.json({ success, data: { deleted: success } }); } catch (error) { this.handleError(error, res); } }); // GET /api/:table/count - Count records this.app.get(`${basePath}/count`, async (req, res) => { try { const where = req.query.where ? JSON.parse(req.query.where) : undefined; const context = this.createRequestContext(req, res); const count = await operations.count(where, context); res.json({ success: true, data: { count } }); } catch (error) { this.handleError(error, res); } }); } /** * Create request context for security checks */ createRequestContext(req, res) { return { req, res, user: req.user, operation: 'read', // Will be overridden by specific operations tableName: '' // Will be set by specific operations }; } /** * Parse ID parameter (handles both string and number IDs) */ parseId(id) { const numId = parseInt(id, 10); return isNaN(numId) ? id : numId; } /** * Handle errors consistently */ handleError(error, res) { console.error('API Error:', error); // Check if it's an authorization error if (error.message.includes('Unauthorized')) { res.status(403).json({ success: false, error: error.message }); return; } // Check if it's a not found error if (error.message.includes('not found')) { res.status(404).json({ success: false, error: error.message }); return; } // Check if it's a validation error if (error.message.includes('Missing required field') || error.message.includes('Invalid type for field')) { res.status(400).json({ success: false, error: error.message }); return; } // Default to 500 for other errors res.status(500).json({ success: false, error: error.message }); } /** * Graceful shutdown */ async shutdown() { console.log('Shutting down Fastfold server...'); await this.db.disconnect(); console.log('Database disconnected.'); } } // ============================================================================ // CONVENIENCE FUNCTION // ============================================================================ /** * Quick start function for Fastfold */ export function createFastfoldServer(config) { return new FastfoldServer(config); } //# sourceMappingURL=index.js.map