File size: 4,773 Bytes
fc05a6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f98007
fc05a6d
 
 
 
 
 
 
 
 
 
 
9f98007
fc05a6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f98007
fc05a6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f98007
 
 
 
 
 
 
 
fc05a6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f98007
 
 
 
 
 
 
 
 
 
 
 
fc05a6d
 
9f98007
fc05a6d
 
 
 
 
 
 
 
 
 
 
 
 
 
2aadb1f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
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'));