const express = require('express'); const admin = require('firebase-admin'); const jwt = require('jsonwebtoken'); const { v4: uuidv4 } = require('uuid'); const axios = require('axios'); const bodyParser = require('body-parser'); const app = express(); app.use(bodyParser.json()); // --------------------------------------------------------- // 1. STATE MANAGEMENT (In-Memory DB) // --------------------------------------------------------- // Store temporary "proj_" redemption keys. // Structure: { "proj_xyz": { uid: "...", projectId: "...", expiresAt: timestamp } } const tempKeys = new Map(); // Store JWT Secrets (Hydrated Cache). // Structure: { "uid:projectId": { secret: "...", lastAccessed: timestamp } } const activeSessions = new Map(); let db = null; // Firebase DB Reference // --------------------------------------------------------- // 2. FIREBASE INITIALIZATION // --------------------------------------------------------- try { if (process.env.FIREBASE_SERVICE_ACCOUNT_JSON) { const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON); if (admin.apps.length === 0) { admin.initializeApp({ credential: admin.credential.cert(serviceAccount), databaseURL: process.env.FIREBASE_DB_URL || "https://your-firebase-project.firebaseio.com" }); } db = admin.database(); console.log("๐ฅ Firebase Connected & StateManager Linked"); } else { console.warn("โ ๏ธ Memory-Only mode (No Firebase JSON provided)."); } } catch (e) { console.error("Firebase Init Error:", e); } // --------------------------------------------------------- // 3. HELPER FUNCTIONS & MIDDLEWARE // --------------------------------------------------------- // Middleware to verify Firebase ID Token (for /key and /nullify) // Includes a Debug Bypass variable const verifyFirebaseUser = async (req, res, next) => { const debugMode = process.env.DEBUG_NO_AUTH === 'true'; // Set to true in .env for testing without valid tokens if (debugMode) { req.user = { uid: "debug_user_001" }; 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 (admin.apps.length > 0) { const decodedToken = await admin.auth().verifyIdToken(idToken); req.user = decodedToken; next(); } else { // Fallback for memory-only mode without validation req.user = { uid: "memory_user" }; next(); } } catch (error) { return res.status(403).json({ error: 'Unauthorized', details: error.message }); } }; // Helper to fetch Secret (Memory -> DB -> 404) async function getSessionSecret(uid, projectId) { const cacheKey = `${uid}:${projectId}`; // 1. Check Memory if (activeSessions.has(cacheKey)) { const session = activeSessions.get(cacheKey); session.lastAccessed = Date.now(); // Update access time for cleanup return session.secret; } // 2. Hydrate from DB if (db) { try { const snapshot = await db.ref(`plugin_oauth/${uid}/${projectId}`).once('value'); if (snapshot.exists()) { const secret = snapshot.val(); // Store in memory for next time activeSessions.set(cacheKey, { secret, lastAccessed: Date.now() }); console.log(`๐ง Hydrated secret for ${cacheKey} from DB`); return secret; } } catch (err) { console.error("DB Read Error:", err); } } // 3. Not found return null; } // --------------------------------------------------------- // 4. ENDPOINTS // --------------------------------------------------------- // Endpoint: /key // Generates a temporary 'proj_' token. Expects Firebase Auth. app.post('/key', verifyFirebaseUser, (req, res) => { const { projectId } = req.body; if (!projectId) return res.status(400).json({ error: 'projectId required' }); const key = `proj_${uuidv4().replace(/-/g, '')}`; // Store in memory (valid for 5 minutes) tempKeys.set(key, { uid: req.user.uid, projectId: projectId, createdAt: Date.now() }); console.log(`๐ Generated Key for user ${req.user.uid}: ${key}`); res.json({ key, expiresIn: 300 }); }); // Endpoint: /redeem // Exchanges 'proj_' key for a JWT. 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); // Generate a unique secret for this session/project const sessionSecret = uuidv4(); // Create JWT const token = jwt.sign( { uid: data.uid, projectId: data.projectId }, sessionSecret, { expiresIn: '7d' } // Plugin token validity ); const cacheKey = `${data.uid}:${data.projectId}`; // 1. Store in Memory activeSessions.set(cacheKey, { secret: sessionSecret, lastAccessed: Date.now() }); // 2. Store in Firebase (Persist) if (db) { await db.ref(`plugin_oauth/${data.uid}/${data.projectId}`).set(sessionSecret); } // Burn the redemption key (One-time use) tempKeys.delete(key); console.log(`๐ Redeemed JWT for ${cacheKey}`); res.json({ token }); }); // Endpoint: /poll // Verifies JWT using the stored secret and forwards to external server. app.post('/poll', async (req, res) => { const { token, payload: clientPayload } = req.body; if (!token) return res.status(400).json({ error: 'Token required' }); // Decode without verification first to find WHO it is const decoded = jwt.decode(token); if (!decoded || !decoded.uid || !decoded.projectId) { return res.status(401).json({ error: 'Malformed token' }); } // Fetch secret (Memory -> DB -> 404) const secret = await getSessionSecret(decoded.uid, decoded.projectId); if (!secret) { return res.status(404).json({ error: 'Session revoked or not found' }); } // Verify signature try { jwt.verify(token, secret); // If valid, post to external server const externalUrl = process.env.EXTERNAL_SERVER_URL || 'https://httpbin.org/post'; // Default for testing try { const response = await axios.post(externalUrl, { user: decoded.uid, project: decoded.projectId, data: clientPayload }); return res.json({ status: 'success', externalResponse: response.data }); } catch (extError) { return res.status(502).json({ error: 'External server error' }); } } catch (err) { return res.status(403).json({ error: 'Invalid Token Signature' }); } }); // Endpoint: /cleanup // Memory management: Clears old JWT secrets from RAM that haven't been used recently. // Does NOT delete from Firebase. app.post('/cleanup', (req, res) => { const THRESHOLD = 1000 * 60 * 60; // 1 Hour const now = Date.now(); let cleanedCount = 0; for (const [key, value] of activeSessions.entries()) { if (now - value.lastAccessed > THRESHOLD) { activeSessions.delete(key); cleanedCount++; } } // Also clean old temp keys (older than 10 mins) for (const [key, value] of tempKeys.entries()) { if (now - value.createdAt > (1000 * 60 * 10)) { tempKeys.delete(key); } } console.log(`๐งน Garbage Collection: Removed ${cleanedCount} cached sessions.`); res.json({ message: `Cleaned ${cleanedCount} cached sessions from memory.` }); }); // Endpoint: /nullify // Security Nuke: Removes session from Memory AND Firebase. app.post('/nullify', verifyFirebaseUser, async (req, res) => { const { projectId } = req.body; if (!projectId) return res.status(400).json({ error: 'projectId required' }); const cacheKey = `${req.user.uid}:${projectId}`; // 1. Remove from Memory const existedInMemory = activeSessions.delete(cacheKey); // 2. Remove from Firebase if (db) { try { await db.ref(`plugin_oauth/${req.user.uid}/${projectId}`).remove(); } catch (e) { return res.status(500).json({ error: 'Database error during nullify' }); } } console.log(`โข๏ธ NULLIFIED session for ${cacheKey}`); res.json({ success: true, message: 'Session secrets purged from memory and database.', wasCached: existedInMemory }); }); // --------------------------------------------------------- // 5. EMBEDDED HTML DEBUGGER // --------------------------------------------------------- app.get('/', (req, res) => { res.send(`
Waiting...
Waiting...
Waiting...
Waiting...