const express = require('express'); const { createClient } = require('@supabase/supabase-js'); const jwt = require('jsonwebtoken'); const { v4: uuidv4 } = require('uuid'); const axios = require('axios'); const bodyParser = require('body-parser'); const cors = require('cors'); const app = express(); app.use(cors()); app.use(bodyParser.json({ limit: '50mb' })); const tempKeys = new Map(); const activeSessions = new Map(); const { SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, EXTERNAL_SERVER_URL = 'http://localhost:7860', STORAGE_BUCKET = 'project-assets', PORT = 7860 } = process.env; let supabase = null; try { if (SUPABASE_URL && SUPABASE_SERVICE_ROLE_KEY) { supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { auth: { autoRefreshToken: false, persistSession: false } }); console.log("⚡ Supabase Connected (Admin Context)"); } else { console.warn("⚠️ Memory-Only mode (Supabase credentials missing)."); } } catch (e) { console.error("Supabase Init Error:", e); } const verifySupabaseUser = async (req, res, next) => { const debugMode = process.env.DEBUG_NO_AUTH === 'true'; if (debugMode) { req.user = { id: "user_dev_01" }; return next(); } const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) return res.status(401).json({ error: 'Missing Bearer token' }); const idToken = authHeader.split('Bearer ')[1]; try { if (supabase) { const { data: { user }, error } = await supabase.auth.getUser(idToken); if (error || !user) throw new Error("Invalid Token"); req.user = user; next(); } else { req.user = { id: "memory_user" }; next(); } } catch (error) { return res.status(403).json({ error: 'Unauthorized', details: error.message }); } }; async function getSessionSecret(uid, projectId) { const cacheKey = `${uid}:${projectId}`; if (activeSessions.has(cacheKey)) { const session = activeSessions.get(cacheKey); session.lastAccessed = Date.now(); return session.secret; } if (supabase) { try { const { data } = await supabase.from('projects').select('plugin_secret').eq('id', projectId).eq('user_id', uid).single(); if (data && data.plugin_secret) { activeSessions.set(cacheKey, { secret: data.plugin_secret, lastAccessed: Date.now() }); return data.plugin_secret; } } catch (err) { console.error("DB Read Error:", err); } } return null; } app.post('/key', verifySupabaseUser, (req, res) => { const { projectId } = req.body; if (!projectId) return res.status(400).json({ error: 'projectId required' }); const key = `key_${uuidv4().replace(/-/g, '')}`; tempKeys.set(key, { uid: req.user.id, projectId: projectId, createdAt: Date.now() }); console.log(`🔑 Generated Key for user ${req.user.id}: ${key}`); res.json({ key, expiresIn: 300 }); }); app.post('/redeem', async (req, res) => { const { key } = req.body; if (!key || !tempKeys.has(key)) return res.status(404).json({ error: 'Invalid or expired key' }); const data = tempKeys.get(key); const sessionSecret = uuidv4(); const token = jwt.sign({ uid: data.uid, projectId: data.projectId }, sessionSecret, { expiresIn: '3d' }); const cacheKey = `${data.uid}:${data.projectId}`; activeSessions.set(cacheKey, { secret: sessionSecret, lastAccessed: Date.now() }); if (supabase) await supabase.from('projects').update({ plugin_secret: sessionSecret }).eq('id', data.projectId).eq('user_id', data.uid); tempKeys.delete(key); console.log(`🚀 Redeemed JWT for ${cacheKey}`); res.json({ token }); }); app.post('/verify', async (req, res) => { const { token } = req.body; if (!token) return res.status(400).json({ valid: false }); const decoded = jwt.decode(token); if (!decoded || !decoded.uid || !decoded.projectId) return res.status(401).json({ valid: false }); const secret = await getSessionSecret(decoded.uid, decoded.projectId); if (!secret) return res.status(401).json({ valid: false }); try { jwt.verify(token, secret); return res.json({ valid: true }); } catch (err) { return res.status(403).json({ valid: false }); } }); app.post('/feedback', async (req, res) => { const { token, ...pluginPayload } = req.body; if (!token) return res.status(400).json({ error: 'Token required' }); const decoded = jwt.decode(token); if (!decoded) return res.status(401).json({ error: 'Malformed token' }); const secret = await getSessionSecret(decoded.uid, decoded.projectId); if (!secret) return res.status(404).json({ error: 'Session revoked' }); try { jwt.verify(token, secret); const targetUrl = EXTERNAL_SERVER_URL.replace(/\/$/, '') + '/project/feedback'; const response = await axios.post(targetUrl, { userId: decoded.uid, projectId: decoded.projectId, ...pluginPayload }); return res.json({ success: true, externalResponse: response.data }); } catch (err) { return res.status(502).json({ error: 'Failed to forward' }); } }); app.post('/feedback2', verifySupabaseUser, async (req, res) => { const { projectId, prompt, images, ...otherPayload } = req.body; const userId = req.user.id; if (!projectId || !prompt) return res.status(400).json({ error: 'Missing projectId or prompt' }); const targetUrl = EXTERNAL_SERVER_URL.replace(/\/$/, '') + '/project/feedback'; try { const response = await axios.post(targetUrl, { userId: userId, projectId: projectId, prompt: prompt, images: images || [], ...otherPayload }); return res.json({ success: true, externalResponse: response.data }); } catch (err) { return res.status(502).json({ error: 'Failed to forward' }); } }); // --- STREAM FEED (Optimized headers) --- app.post('/stream-feed', verifySupabaseUser, async (req, res) => { const { projectId } = req.body; const userId = req.user.id; // Headers to disable caching for poller res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); if (!projectId) return res.status(400).json({ error: 'Missing projectId' }); if (supabase) { const { data, error } = await supabase.from('projects').select('id, user_id, info').eq('id', projectId).single(); if (error || !data || data.user_id !== userId) return res.status(403).json({ error: 'Unauthorized' }); const targetUrl = EXTERNAL_SERVER_URL.replace(/\/$/, '') + '/project/ping'; try { const response = await axios.post(targetUrl, { projectId, userId, isFrontend: true }); return res.json({ ...response.data, dbStatus: data.info?.status || 'idle' }); } catch (e) { return res.status(502).json({ error: "AI Server Unreachable" }); } } }); /* app.post('/poll', async (req, res) => { const { token } = req.body; if (!token) return res.status(400).json({ error: 'Token required' }); const decoded = jwt.decode(token); if (!decoded) return res.status(401).json({ error: 'Malformed token' }); const secret = await getSessionSecret(decoded.uid, decoded.projectId); if (!secret) return res.status(404).json({ error: 'Session revoked' }); try { jwt.verify(token, secret); const targetUrl = EXTERNAL_SERVER_URL.replace(/\/$/, '') + '/project/ping'; const response = await axios.post(targetUrl, { projectId: decoded.projectId, userId: decoded.uid }); return res.json(response.data); } catch (err) { return res.status(403).json({ error: 'Invalid Token' }); } }); */ app.post('/poll', async (req, res) => { const { token } = req.body; if (!token) return res.status(400).json({ error: 'Token required' }); const decoded = jwt.decode(token); if (!decoded) return res.status(401).json({ error: 'Malformed token' }); const secret = await getSessionSecret(decoded.uid, decoded.projectId); if (!secret) return res.status(404).json({ error: 'Session revoked' }); try { jwt.verify(token, secret); const targetUrl = EXTERNAL_SERVER_URL.replace(/\/$/, '') + '/project/ping'; // FIX: We do NOT send isFrontend: true. // We act as the Plugin (Executor), but the Main Server will still give us the snapshot now. const response = await axios.post(targetUrl, { projectId: decoded.projectId, userId: decoded.uid }); return res.json(response.data); } catch (err) { return res.status(403).json({ error: 'Invalid Token' }); } }); app.post('/project/delete', verifySupabaseUser, async (req, res) => { const { projectId } = req.body; const userId = req.user.id; if (!projectId) return res.status(400).json({ error: "Missing Project ID" }); try { const { data: project, error: fetchError } = await supabase.from('projects').select('user_id').eq('id', projectId).single(); if (fetchError || !project || project.user_id !== userId) return res.status(403).json({ error: "Unauthorized" }); await supabase.from('message_chunks').delete().eq('project_id', projectId); await supabase.from('projects').delete().eq('id', projectId); if (STORAGE_BUCKET) { const { data: files } = await supabase.storage.from(STORAGE_BUCKET).list(projectId); if (files && files.length > 0) { const filesToRemove = files.map(f => `${projectId}/${f.name}`); await supabase.storage.from(STORAGE_BUCKET).remove(filesToRemove); } } activeSessions.delete(`${userId}:${projectId}`); for (const [key, val] of tempKeys.entries()) { if (val.projectId === projectId) tempKeys.delete(key); } res.json({ success: true }); } catch (err) { res.status(500).json({ error: "Delete failed" }); } }); app.get('/cleanup', (req, res) => { // ... (Standard cleanup) ... const THRESHOLD = 1000 * 60 * 60; const now = Date.now(); let cleanedCount = 0; for (const [key, value] of activeSessions.entries()) { if (now - value.lastAccessed > THRESHOLD) { activeSessions.delete(key); cleanedCount++; } } for (const [key, value] of tempKeys.entries()) { if (now - value.createdAt > (1000 * 60 * 4)) { tempKeys.delete(key); } } res.json({ message: `Cleaned ${cleanedCount} cached sessions from memory.` }); }); app.post('/nullify', verifySupabaseUser, async (req, res) => { const { projectId } = req.body; if (!projectId) return res.status(400).json({ error: 'projectId required' }); const cacheKey = `${req.user.id}:${projectId}`; activeSessions.delete(cacheKey); if (supabase) await supabase.from('projects').update({ plugin_secret: null }).eq('id', projectId).eq('user_id', req.user.id); res.json({ success: true }); }); app.get('/', (req, res) => { res.send('Plugin Auth Proxy Running (Supabase Edition)'); }); app.listen(PORT, () => { console.log(`🚀 Auth Proxy running on port ${PORT}`); });