@flavoai/fastfold
Version:
Flavo frontend package
410 lines • 15.4 kB
JavaScript
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