Spaces:
Paused
Paused
| 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')); |