const express = require('express'); const fetch = require('node-fetch'); const cors = require('cors'); const rateLimit = require('express-rate-limit'); require('dotenv').config(); const app = express(); // --- 1. SECURITY: TRUST PROXY --- // Required for Hugging Face to see the Real IP for rate limiting app.set('trust proxy', 1); // --- 2. SECURITY: RATE LIMITING --- // Limit each IP to 100 requests per 15 minutes const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100, standardHeaders: true, legacyHeaders: false, message: { error: "Too many requests, please try again later." } }); app.use(limiter); // --- 3. SECURITY: STRICT CORS --- const allowedOrigins = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(',') : []; app.use(cors({ origin: function (origin, callback) { // Open Mode: If list is empty or origin is null (tools/mobile), allow it. if (!origin || allowedOrigins.length === 0) return callback(null, true); if (allowedOrigins.indexOf(origin) !== -1) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } } })); app.use(express.json()); // --- 4. MULTI-INSTANCE CONFIGURATION --- let INSTANCES = []; try { // Example Secret: [{"url":"https://my-flowise.com","key":"tr-123"}, {"url":"https://open-flowise.com","key":""}] INSTANCES = JSON.parse(process.env.FLOWISE_INSTANCES || '[]'); } catch (e) { console.error("CRITICAL ERROR: Could not parse FLOWISE_INSTANCES JSON", e); } // Master Cache: Maps "bot-name" -> { flowId, instanceUrl, apiKey } let flowDirectory = {}; let lastCacheUpdate = 0; // --- 5. DYNAMIC DISCOVERY --- async function refreshFlowDirectory() { // Cache valid for 60 seconds if (Date.now() - lastCacheUpdate < 60000 && Object.keys(flowDirectory).length > 0) return; console.log(`[System] Scanning ${INSTANCES.length} Flowise Instances...`); const newDirectory = {}; // Run all fetches in parallel const promises = INSTANCES.map(async (inst) => { try { // Smart Auth: Only add header if key exists const headers = {}; if (inst.key && inst.key.length > 0) { headers['Authorization'] = `Bearer ${inst.key}`; } const res = await fetch(`${inst.url}/api/v1/chatflows`, { headers }); if(!res.ok) throw new Error(`Status ${res.status}`); const flows = await res.json(); flows.forEach(flow => { const alias = flow.name.toLowerCase().replace(/\s+/g, '-'); newDirectory[alias] = { id: flow.id, host: inst.url, key: inst.key }; }); } catch (err) { console.error(`[Error] Failed to fetch from ${inst.url}:`, err.message); } }); await Promise.allSettled(promises); flowDirectory = newDirectory; lastCacheUpdate = Date.now(); console.log(`[System] Directory Updated. Serving ${Object.keys(flowDirectory).length} bots.`); } // --- 6. THE ROUTE --- app.post('/api/v1/prediction/:botName', async (req, res) => { const botName = req.params.botName.toLowerCase(); await refreshFlowDirectory(); const target = flowDirectory[botName]; if (!target) { lastCacheUpdate = 0; await refreshFlowDirectory(); if (!flowDirectory[botName]) { return res.status(404).json({ error: `Bot '${botName}' not found.` }); } } const finalTarget = flowDirectory[botName]; try { // Construct Headers for forwarding const forwardHeaders = { 'Content-Type': 'application/json', 'HTTP-Referer': req.headers.origin || 'https://huggingface.co', 'X-Title': 'FederatedProxy' }; // Smart Auth: Only add Bearer if key exists if (finalTarget.key && finalTarget.key.length > 0) { forwardHeaders['Authorization'] = `Bearer ${finalTarget.key}`; } const flowiseResponse = await fetch(`${finalTarget.host}/api/v1/prediction/${finalTarget.id}`, { method: 'POST', headers: forwardHeaders, body: JSON.stringify(req.body) }); const data = await flowiseResponse.json(); res.status(flowiseResponse.status).json(data); } catch (error) { console.error("Proxy Forwarding Error:", error); res.status(500).json({ error: 'Proxy forwarding failed.' }); } }); app.get('/', (req, res) => res.send('Federated Proxy Active')); app.listen(7860, '0.0.0.0', () => console.log('Federated Proxy running on port 7860'));