everydaycats commited on
Commit
c78bf1e
·
verified ·
1 Parent(s): 49740aa

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +351 -240
app.js CHANGED
@@ -1,294 +1,405 @@
1
- import express from 'express';
2
- import bodyParser from 'body-parser';
3
- import cors from 'cors';
4
- import { StateManager } from './stateManager.js';
5
- import { AIEngine } from './aiEngine.js';
6
- import fs from 'fs';
 
7
 
8
  const app = express();
9
- const PORT = process.env.PORT || 7860;
10
-
11
- // Load prompts
12
- const sysPrompts = JSON.parse(fs.readFileSync('./prompts.json', 'utf8'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
- app.use(cors());
15
- app.use(bodyParser.json({ limit: '50mb' }));
 
16
 
17
- const validateRequest = (req, res, next) => {
18
- const { userId, projectId } = req.body;
19
- if (!userId || !projectId) return res.status(400).json({ error: "Missing ID" });
20
- next();
21
- };
22
 
23
- // Helper to extract PM's isolated instruction
24
- function extractWorkerPrompt(text) {
25
- const match = text.match(/WORKER_PROMPT:\s*(.*)/s);
26
- return match ? match[1].trim() : null;
27
- }
28
 
29
- // Helper to format context for the AI
30
- function formatContext({ hierarchyContext, scriptContext, logContext }) {
31
- let out = "";
32
- if (scriptContext) out += `\n[TARGET SCRIPT]: ${scriptContext.targetName}\n[SOURCE PREVIEW]: ${scriptContext.scriptSource.substring(0, 500)}...`;
33
- if (logContext) out += `\n[LAST LOGS]: ${logContext.logs}`;
34
- return out;
35
- }
36
 
37
- /**
38
- * 1. NEW PROJECT (Restored Full Logic)
39
- * Flow: PM (GDD) -> PM (Tasks) -> Worker (Task 1 Isolated)
40
- */
41
- app.post('/new/project', validateRequest, async (req, res) => {
42
- const { userId, projectId, description } = req.body;
43
 
44
  try {
45
- // 1. Generate GDD
46
- const pmHistory = [];
47
- const gddPrompt = `Create a comprehensive GDD for: ${description}`;
48
- const gddResponse = await AIEngine.callPM(pmHistory, gddPrompt);
49
-
50
- pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] });
51
- pmHistory.push({ role: 'model', parts: [{ text: gddResponse }] });
 
 
 
 
 
 
52
 
53
- // 2. Generate Initial Task List & Extract First Prompt
54
- const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
55
- const taskResponse = await AIEngine.callPM(pmHistory, taskPrompt);
56
 
57
- pmHistory.push({ role: 'user', parts: [{ text: taskPrompt }] });
58
- pmHistory.push({ role: 'model', parts: [{ text: taskResponse }] });
 
 
 
 
59
 
60
- // 3. Extract the Isolated Prompt
61
- const initialWorkerInstruction = extractWorkerPrompt(taskResponse) || `Initialize structure for: ${description}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
- // 4. Initialize Worker (Fresh Scope)
64
- const workerHistory = [];
65
- const initialWorkerPrompt = `CONTEXT: None (New Project). \nINSTRUCTION: ${initialWorkerInstruction}`;
66
- const workerResponse = await AIEngine.callWorker(workerHistory, initialWorkerPrompt);
67
 
68
- workerHistory.push({ role: 'user', parts: [{ text: initialWorkerPrompt }] });
69
- workerHistory.push({ role: 'model', parts: [{ text: workerResponse }] });
 
70
 
71
- // 5. Save State
72
- await StateManager.updateProject(projectId, {
73
- userId,
74
- pmHistory,
75
- workerHistory,
76
- gdd: gddResponse,
77
- failureCount: 0
78
- });
79
 
80
- // Queue for Plugin
81
- StateManager.queueCommand(projectId, workerResponse);
 
 
 
 
 
 
82
 
83
- res.json({ success: true, message: "Project Initialized", gddPreview: gddResponse.substring(0, 200) });
 
 
84
 
85
- } catch (err) {
86
- console.error(err);
87
- res.status(500).json({ error: "Initialization Failed" });
 
 
 
 
88
  }
89
- });
90
 
91
- /**
92
- * 2. FEEDBACK LOOP (Restored Escalation & Termination)
93
- */
94
- app.post('/project/feedback', async (req, res) => {
95
- const { projectId, prompt, hierarchyContext, scriptContext, logContext, taskComplete } = req.body;
96
 
97
- const project = await StateManager.getProject(projectId);
98
- if (!project) return res.status(404).json({ error: "Project not found." });
99
-
100
- // ==========================================
101
- // A. TASK COMPLETE -> SCOPE SWITCH
102
- // ==========================================
103
- if (taskComplete) {
104
- console.log(`[${projectId}] TASK COMPLETE.`);
105
-
106
- // 1. Report to PM
107
- const summary = `Worker completed the previous task. Logs: ${logContext?.logs || "Clean"}. \nGenerate the NEXT task using 'WORKER_PROMPT:' format.`;
108
- const pmResponse = await AIEngine.callPM(project.pmHistory, summary);
109
-
110
- project.pmHistory.push({ role: 'user', parts: [{ text: summary }] });
111
- project.pmHistory.push({ role: 'model', parts: [{ text: pmResponse }] });
112
-
113
- // 2. Get Next Instruction
114
- const nextInstruction = extractWorkerPrompt(pmResponse);
115
- if (!nextInstruction) {
116
- await StateManager.updateProject(projectId, { pmHistory: project.pmHistory, status: "IDLE" });
117
- return res.json({ success: true, message: "No further tasks generated. Project Idle." });
118
- }
119
 
120
- // 3. NUKE WORKER (Scope Reset)
121
- console.log(`[${projectId}] 🧹 Nuking Worker for next task.`);
122
- const newWorkerHistory = [];
123
- const newPrompt = `New Objective: ${nextInstruction}`;
124
- const workerResponse = await AIEngine.callWorker(newWorkerHistory, newPrompt);
125
 
126
- newWorkerHistory.push({ role: 'user', parts: [{ text: newPrompt }] });
127
- newWorkerHistory.push({ role: 'model', parts: [{ text: workerResponse }] });
128
 
129
- await StateManager.updateProject(projectId, {
130
- pmHistory: project.pmHistory,
131
- workerHistory: newWorkerHistory,
132
- failureCount: 0
133
- });
134
 
135
- // Clear Console Command + New Code
136
- StateManager.queueCommand(projectId, "CLEAR_CONSOLE");
137
- StateManager.queueCommand(projectId, workerResponse);
138
 
139
- return res.json({ success: true, message: "Next Task Assigned" });
140
- }
 
141
 
142
- // ==========================================
143
- // B. ERROR HANDLING & ESCALATION
144
- // ==========================================
145
- let isFailure = false;
146
- if (logContext?.logs) {
147
- const errs = ["Error", "Exception", "failed", "Stack Begin"];
148
- if (errs.some(k => logContext.logs.includes(k))) {
149
- isFailure = true;
150
- project.failureCount = (project.failureCount || 0) + 1;
151
- }
152
- }
153
 
154
- // Prepare Prompt
155
- const fullInput = `USER: ${prompt || "Automatic Feedback"}` + formatContext({ hierarchyContext, scriptContext, logContext });
156
 
157
- // --- ESCALATION LOGIC (Restored) ---
158
- if (project.failureCount > 2) {
159
- console.log(`[${projectId}] 🚨 Escalating to PM (Failures: ${project.failureCount})...`);
160
-
161
- // 1. Ask PM for solution
162
- const pmPrompt = sysPrompts.pm_guidance_prompt.replace('{{LOGS}}', logContext?.logs);
163
- const pmVerdict = await AIEngine.callPM(project.pmHistory, pmPrompt);
164
 
165
- // 2. CHECK: Did PM order termination?
166
- if (pmVerdict.includes("[TERMINATE]")) {
167
- console.log(`[${projectId}] 💀 PM TERMINATED WORKER.`);
168
-
169
- const fixInstruction = pmVerdict.replace("[TERMINATE]", "").trim();
170
-
171
- // Hard Reset
172
- const resetHistory = [];
173
- const resetPrompt = `[SYSTEM]: Previous worker terminated. \nNew Objective: ${fixInstruction}`;
174
- const workerResp = await AIEngine.callWorker(resetHistory, resetPrompt);
175
-
176
- resetHistory.push({ role: 'user', parts: [{ text: resetPrompt }] });
177
- resetHistory.push({ role: 'model', parts: [{ text: workerResp }] });
178
 
179
- await StateManager.updateProject(projectId, { workerHistory: resetHistory, failureCount: 0 });
180
- StateManager.queueCommand(projectId, "CLEAR_CONSOLE");
181
- StateManager.queueCommand(projectId, workerResp);
182
-
183
- return res.json({ success: true, message: "Worker Terminated & Replaced." });
184
- }
185
- else {
186
- // 3. Just Guidance (Soft Fix)
187
- console.log(`[${projectId}] 💡 PM Provided Guidance.`);
188
- const injection = `[PM GUIDANCE]: ${pmVerdict} \n\nApply this fix now.`;
189
- const workerResp = await AIEngine.callWorker(project.workerHistory, injection);
190
-
191
- project.workerHistory.push({ role: 'user', parts: [{ text: injection }] });
192
- project.workerHistory.push({ role: 'model', parts: [{ text: workerResp }] });
193
-
194
- // Reset count slightly so we don't spam PM, but keep watch
195
- await StateManager.updateProject(projectId, { workerHistory: project.workerHistory, failureCount: 0 });
196
- await StateManager.queueCommand(projectId, workerResp);
197
-
198
- return res.json({ success: true, message: "PM Guidance Applied." });
199
- }
200
  }
201
 
202
- // ==========================================
203
- // C. STANDARD WORKER LOOP
204
- // ==========================================
205
  try {
206
- const response = await AIEngine.callWorker(project.workerHistory, fullInput);
207
 
208
- project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] });
209
- project.workerHistory.push({ role: 'model', parts: [{ text: response }] });
210
 
211
- await StateManager.updateProject(projectId, {
212
- workerHistory: project.workerHistory,
213
- failureCount: project.failureCount
214
- });
215
-
216
- await StateManager.queueCommand(projectId, response);
217
- res.json({ success: true });
 
 
 
218
 
219
  } catch (err) {
220
- console.error("AI Error:", err);
221
- res.status(500).json({ error: "AI Failed" });
222
  }
223
  });
224
 
225
- /**
226
- * 3. HUMAN OVERRIDE (God Mode)
227
- */
228
- app.post('/human/override', validateRequest, async (req, res) => {
229
- const { projectId, instruction, pruneHistory } = req.body;
230
- const project = await StateManager.getProject(projectId);
231
-
232
- const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`;
233
-
234
- // Optional: Prune the argument if they are stuck
235
- if (pruneHistory && project.workerHistory.length >= 2) {
236
- project.workerHistory.pop(); // last model
237
- project.workerHistory.pop(); // last user
238
  }
239
 
240
- const response = await AIEngine.callWorker(project.workerHistory, overrideMsg);
241
- project.workerHistory.push({ role: 'user', parts: [{ text: overrideMsg }] });
242
- project.workerHistory.push({ role: 'model', parts: [{ text: response }] });
 
 
 
243
 
244
- await StateManager.updateProject(projectId, { workerHistory: project.workerHistory });
245
- await StateManager.queueCommand(projectId, response);
246
- res.json({ success: true, message: "Override Executed" });
247
  });
248
 
249
- /**
250
- * 4. WORKER KILL (Manual Reassignment)
251
- */
252
- app.post('/worker/kill', validateRequest, async (req, res) => {
253
  const { projectId } = req.body;
254
- // Logic: Just reset the history with a "Try again" prompt
255
- const project = await StateManager.getProject(projectId);
256
-
257
- const rebootPrompt = "[SYSTEM]: Worker process restarted. Attempt the task again from scratch.";
258
- const response = await AIEngine.callWorker([{ role: 'user', parts: [{ text: rebootPrompt }] }], rebootPrompt);
259
-
260
- await StateManager.updateProject(projectId, {
261
- workerHistory: [{ role: 'user', parts: [{ text: rebootPrompt }] }], // Nuke history
262
- workerHistory: [{ role: 'model', parts: [{ text: response }] }],
263
- failureCount: 0
264
- });
265
-
266
- StateManager.queueCommand(projectId, response);
267
- res.json({ success: true, message: "Worker Killed & Restarted" });
268
- });
269
 
270
- /**
271
- * 5. PING (Plugin Polling)
272
- */
273
- app.post('/project/ping', async (req, res) => {
274
- const { projectId } = req.body;
275
- const command = await StateManager.popCommand(projectId);
276
-
277
- if (command) {
278
- if (command.payload === "CLEAR_CONSOLE") {
279
- res.json({ action: "CLEAR_LOGS" });
280
- } else {
281
- res.json({
282
- action: command.type,
283
- target: command.payload,
284
- code: command.type === 'EXECUTE' ? command.payload : null
285
- });
286
  }
287
- } else {
288
- res.json({ action: "IDLE" });
289
  }
 
 
 
 
 
 
 
290
  });
291
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  app.listen(PORT, () => {
293
- console.log(`AI Backend Running on ${PORT}`);
294
  });
 
1
+ require('dotenv').config();
2
+ const express = require('express');
3
+ const admin = require('firebase-admin');
4
+ const jwt = require('jsonwebtoken');
5
+ const { v4: uuidv4 } = require('uuid');
6
+ const axios = require('axios');
7
+ const bodyParser = require('body-parser');
8
 
9
  const app = express();
10
+ app.use(bodyParser.json());
11
+
12
+ // ---------------------------------------------------------
13
+ // 1. STATE MANAGEMENT (In-Memory DB)
14
+ // ---------------------------------------------------------
15
+
16
+ // Store temporary "proj_" redemption keys.
17
+ // Structure: { "proj_xyz": { uid: "...", projectId: "...", expiresAt: timestamp } }
18
+ const tempKeys = new Map();
19
+
20
+ // Store JWT Secrets (Hydrated Cache).
21
+ // Structure: { "uid:projectId": { secret: "...", lastAccessed: timestamp } }
22
+ const activeSessions = new Map();
23
+
24
+ let db = null; // Firebase DB Reference
25
+
26
+ // ---------------------------------------------------------
27
+ // 2. FIREBASE INITIALIZATION
28
+ // ---------------------------------------------------------
29
+ try {
30
+ if (process.env.FIREBASE_SERVICE_ACCOUNT_JSON) {
31
+ const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON);
32
+ if (admin.apps.length === 0) {
33
+ admin.initializeApp({
34
+ credential: admin.credential.cert(serviceAccount),
35
+ databaseURL: process.env.FIREBASE_DB_URL || "https://your-firebase-project.firebaseio.com"
36
+ });
37
+ }
38
+ db = admin.database();
39
+ console.log("🔥 Firebase Connected & StateManager Linked");
40
+ } else {
41
+ console.warn("⚠️ Memory-Only mode (No Firebase JSON provided).");
42
+ }
43
+ } catch (e) {
44
+ console.error("Firebase Init Error:", e);
45
+ }
46
 
47
+ // ---------------------------------------------------------
48
+ // 3. HELPER FUNCTIONS & MIDDLEWARE
49
+ // ---------------------------------------------------------
50
 
51
+ // Middleware to verify Firebase ID Token (for /key and /nullify)
52
+ // Includes a Debug Bypass variable
53
+ const verifyFirebaseUser = async (req, res, next) => {
54
+ const debugMode = process.env.DEBUG_NO_AUTH === 'true'; // Set to true in .env for testing without valid tokens
 
55
 
56
+ if (debugMode) {
57
+ req.user = { uid: "debug_user_001" };
58
+ return next();
59
+ }
 
60
 
61
+ const authHeader = req.headers.authorization;
62
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
63
+ return res.status(401).json({ error: 'Missing Bearer token' });
64
+ }
 
 
 
65
 
66
+ const idToken = authHeader.split('Bearer ')[1];
 
 
 
 
 
67
 
68
  try {
69
+ if (admin.apps.length > 0) {
70
+ const decodedToken = await admin.auth().verifyIdToken(idToken);
71
+ req.user = decodedToken;
72
+ next();
73
+ } else {
74
+ // Fallback for memory-only mode without validation
75
+ req.user = { uid: "memory_user" };
76
+ next();
77
+ }
78
+ } catch (error) {
79
+ return res.status(403).json({ error: 'Unauthorized', details: error.message });
80
+ }
81
+ };
82
 
83
+ // Helper to fetch Secret (Memory -> DB -> 404)
84
+ async function getSessionSecret(uid, projectId) {
85
+ const cacheKey = `${uid}:${projectId}`;
86
 
87
+ // 1. Check Memory
88
+ if (activeSessions.has(cacheKey)) {
89
+ const session = activeSessions.get(cacheKey);
90
+ session.lastAccessed = Date.now(); // Update access time for cleanup
91
+ return session.secret;
92
+ }
93
 
94
+ // 2. Hydrate from DB
95
+ if (db) {
96
+ try {
97
+ const snapshot = await db.ref(`plugin_oauth/${uid}/${projectId}`).once('value');
98
+ if (snapshot.exists()) {
99
+ const secret = snapshot.val();
100
+ // Store in memory for next time
101
+ activeSessions.set(cacheKey, { secret, lastAccessed: Date.now() });
102
+ console.log(`💧 Hydrated secret for ${cacheKey} from DB`);
103
+ return secret;
104
+ }
105
+ } catch (err) {
106
+ console.error("DB Read Error:", err);
107
+ }
108
+ }
109
 
110
+ // 3. Not found
111
+ return null;
112
+ }
 
113
 
114
+ // ---------------------------------------------------------
115
+ // 4. ENDPOINTS
116
+ // ---------------------------------------------------------
117
 
118
+ // Endpoint: /key
119
+ // Generates a temporary 'proj_' token. Expects Firebase Auth.
120
+ app.post('/key', verifyFirebaseUser, (req, res) => {
121
+ const { projectId } = req.body;
122
+ if (!projectId) return res.status(400).json({ error: 'projectId required' });
 
 
 
123
 
124
+ const key = `proj_${uuidv4().replace(/-/g, '')}`;
125
+
126
+ // Store in memory (valid for 5 minutes)
127
+ tempKeys.set(key, {
128
+ uid: req.user.uid,
129
+ projectId: projectId,
130
+ createdAt: Date.now()
131
+ });
132
 
133
+ console.log(`🔑 Generated Key for user ${req.user.uid}: ${key}`);
134
+ res.json({ key, expiresIn: 300 });
135
+ });
136
 
137
+ // Endpoint: /redeem
138
+ // Exchanges 'proj_' key for a JWT.
139
+ app.post('/redeem', async (req, res) => {
140
+ const { key } = req.body;
141
+
142
+ if (!key || !tempKeys.has(key)) {
143
+ return res.status(404).json({ error: 'Invalid or expired key' });
144
  }
 
145
 
146
+ const data = tempKeys.get(key);
 
 
 
 
147
 
148
+ // Generate a unique secret for this session/project
149
+ const sessionSecret = uuidv4();
150
+
151
+ // Create JWT
152
+ const token = jwt.sign(
153
+ { uid: data.uid, projectId: data.projectId },
154
+ sessionSecret,
155
+ { expiresIn: '7d' } // Plugin token validity
156
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
+ const cacheKey = `${data.uid}:${data.projectId}`;
 
 
 
 
159
 
160
+ // 1. Store in Memory
161
+ activeSessions.set(cacheKey, { secret: sessionSecret, lastAccessed: Date.now() });
162
 
163
+ // 2. Store in Firebase (Persist)
164
+ if (db) {
165
+ await db.ref(`plugin_oauth/${data.uid}/${data.projectId}`).set(sessionSecret);
166
+ }
 
167
 
168
+ // Burn the redemption key (One-time use)
169
+ tempKeys.delete(key);
 
170
 
171
+ console.log(`🚀 Redeemed JWT for ${cacheKey}`);
172
+ res.json({ token });
173
+ });
174
 
175
+ // Endpoint: /poll
176
+ // Verifies JWT using the stored secret and forwards to external server.
177
+ app.post('/poll', async (req, res) => {
178
+ const { token, payload: clientPayload } = req.body;
 
 
 
 
 
 
 
179
 
180
+ if (!token) return res.status(400).json({ error: 'Token required' });
 
181
 
182
+ // Decode without verification first to find WHO it is
183
+ const decoded = jwt.decode(token);
184
+ if (!decoded || !decoded.uid || !decoded.projectId) {
185
+ return res.status(401).json({ error: 'Malformed token' });
186
+ }
 
 
187
 
188
+ // Fetch secret (Memory -> DB -> 404)
189
+ const secret = await getSessionSecret(decoded.uid, decoded.projectId);
 
 
 
 
 
 
 
 
 
 
 
190
 
191
+ if (!secret) {
192
+ return res.status(404).json({ error: 'Session revoked or not found' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  }
194
 
195
+ // Verify signature
 
 
196
  try {
197
+ jwt.verify(token, secret);
198
 
199
+ // If valid, post to external server
200
+ const externalUrl = process.env.EXTERNAL_SERVER_URL || 'https://httpbin.org/post'; // Default for testing
201
 
202
+ try {
203
+ const response = await axios.post(externalUrl, {
204
+ user: decoded.uid,
205
+ project: decoded.projectId,
206
+ data: clientPayload
207
+ });
208
+ return res.json({ status: 'success', externalResponse: response.data });
209
+ } catch (extError) {
210
+ return res.status(502).json({ error: 'External server error' });
211
+ }
212
 
213
  } catch (err) {
214
+ return res.status(403).json({ error: 'Invalid Token Signature' });
 
215
  }
216
  });
217
 
218
+ // Endpoint: /cleanup
219
+ // Memory management: Clears old JWT secrets from RAM that haven't been used recently.
220
+ // Does NOT delete from Firebase.
221
+ app.post('/cleanup', (req, res) => {
222
+ const THRESHOLD = 1000 * 60 * 60; // 1 Hour
223
+ const now = Date.now();
224
+ let cleanedCount = 0;
225
+
226
+ for (const [key, value] of activeSessions.entries()) {
227
+ if (now - value.lastAccessed > THRESHOLD) {
228
+ activeSessions.delete(key);
229
+ cleanedCount++;
230
+ }
231
  }
232
 
233
+ // Also clean old temp keys (older than 10 mins)
234
+ for (const [key, value] of tempKeys.entries()) {
235
+ if (now - value.createdAt > (1000 * 60 * 10)) {
236
+ tempKeys.delete(key);
237
+ }
238
+ }
239
 
240
+ console.log(`🧹 Garbage Collection: Removed ${cleanedCount} cached sessions.`);
241
+ res.json({ message: `Cleaned ${cleanedCount} cached sessions from memory.` });
 
242
  });
243
 
244
+ // Endpoint: /nullify
245
+ // Security Nuke: Removes session from Memory AND Firebase.
246
+ app.post('/nullify', verifyFirebaseUser, async (req, res) => {
 
247
  const { projectId } = req.body;
248
+ if (!projectId) return res.status(400).json({ error: 'projectId required' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
 
250
+ const cacheKey = `${req.user.uid}:${projectId}`;
251
+
252
+ // 1. Remove from Memory
253
+ const existedInMemory = activeSessions.delete(cacheKey);
254
+
255
+ // 2. Remove from Firebase
256
+ if (db) {
257
+ try {
258
+ await db.ref(`plugin_oauth/${req.user.uid}/${projectId}`).remove();
259
+ } catch (e) {
260
+ return res.status(500).json({ error: 'Database error during nullify' });
 
 
 
 
 
261
  }
 
 
262
  }
263
+
264
+ console.log(`☢️ NULLIFIED session for ${cacheKey}`);
265
+ res.json({
266
+ success: true,
267
+ message: 'Session secrets purged from memory and database.',
268
+ wasCached: existedInMemory
269
+ });
270
  });
271
 
272
+ // ---------------------------------------------------------
273
+ // 5. EMBEDDED HTML DEBUGGER
274
+ // ---------------------------------------------------------
275
+ app.get('/', (req, res) => {
276
+ res.send(`
277
+ <!DOCTYPE html>
278
+ <html lang="en">
279
+ <head>
280
+ <meta charset="UTF-8">
281
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
282
+ <title>Roblox Plugin Auth Server</title>
283
+ <style>
284
+ body { font-family: monospace; background: #1e1e1e; color: #d4d4d4; padding: 20px; max-width: 800px; margin: 0 auto; }
285
+ .container { background: #252526; padding: 20px; border-radius: 8px; border: 1px solid #333; }
286
+ input, button { padding: 10px; margin: 5px 0; width: 100%; box-sizing: border-box; background: #3c3c3c; border: 1px solid #555; color: white; }
287
+ button { background: #0e639c; cursor: pointer; font-weight: bold; }
288
+ button:hover { background: #1177bb; }
289
+ button.danger { background: #a51d2d; }
290
+ label { display: block; margin-top: 10px; color: #9cdcfe; }
291
+ pre { background: #000; padding: 10px; border: 1px solid #444; overflow-x: auto; white-space: pre-wrap; }
292
+ .status { margin-top: 20px; padding: 10px; border-left: 4px solid #0e639c; background: #2d2d2d; }
293
+ </style>
294
+ </head>
295
+ <body>
296
+ <h1>🔌 Plugin Auth Debugger</h1>
297
+
298
+ <div class="container">
299
+ <h3>1. Configuration</h3>
300
+ <label>Firebase ID Token (leave empty if DEBUG_NO_AUTH=true)</label>
301
+ <input type="text" id="fbToken" placeholder="eyJhbG...">
302
+
303
+ <label>Project ID</label>
304
+ <input type="text" id="projId" value="roblox_world_1">
305
+
306
+ <hr style="border-color:#444">
307
+
308
+ <h3>2. Generate Key (/key)</h3>
309
+ <button onclick="generateKey()">Generate PROJ_ Key</button>
310
+ <pre id="keyResult">Waiting...</pre>
311
+
312
+ <h3>3. Redeem Token (/redeem)</h3>
313
+ <label>Key to Redeem</label>
314
+ <input type="text" id="redeemKeyInput">
315
+ <button onclick="redeemKey()">Redeem for JWT</button>
316
+ <pre id="jwtResult">Waiting...</pre>
317
+
318
+ <h3>4. Poll External (/poll)</h3>
319
+ <label>JWT Token</label>
320
+ <input type="text" id="jwtInput">
321
+ <button onclick="poll()">Poll External Server</button>
322
+ <pre id="pollResult">Waiting...</pre>
323
+
324
+ <h3>5. Management</h3>
325
+ <div style="display:flex; gap:10px;">
326
+ <button onclick="cleanup()">Memory Cleanup</button>
327
+ <button class="danger" onclick="nullify()">Nullify (Nuke)</button>
328
+ </div>
329
+ <pre id="mgmtResult">Waiting...</pre>
330
+ </div>
331
+
332
+ <script>
333
+ const baseUrl = '';
334
+
335
+ async function generateKey() {
336
+ const token = document.getElementById('fbToken').value;
337
+ const projectId = document.getElementById('projId').value;
338
+
339
+ const headers = {};
340
+ if(token) headers['Authorization'] = 'Bearer ' + token;
341
+
342
+ const res = await fetch(baseUrl + '/key', {
343
+ method: 'POST',
344
+ headers: { 'Content-Type': 'application/json', ...headers },
345
+ body: JSON.stringify({ projectId })
346
+ });
347
+ const data = await res.json();
348
+ document.getElementById('keyResult').innerText = JSON.stringify(data, null, 2);
349
+ if(data.key) document.getElementById('redeemKeyInput').value = data.key;
350
+ }
351
+
352
+ async function redeemKey() {
353
+ const key = document.getElementById('redeemKeyInput').value;
354
+ const res = await fetch(baseUrl + '/redeem', {
355
+ method: 'POST',
356
+ headers: { 'Content-Type': 'application/json' },
357
+ body: JSON.stringify({ key })
358
+ });
359
+ const data = await res.json();
360
+ document.getElementById('jwtResult').innerText = JSON.stringify(data, null, 2);
361
+ if(data.token) document.getElementById('jwtInput').value = data.token;
362
+ }
363
+
364
+ async function poll() {
365
+ const token = document.getElementById('jwtInput').value;
366
+ const res = await fetch(baseUrl + '/poll', {
367
+ method: 'POST',
368
+ headers: { 'Content-Type': 'application/json' },
369
+ body: JSON.stringify({ token, payload: { msg: "Hello from Roblox" } })
370
+ });
371
+ const data = await res.json();
372
+ document.getElementById('pollResult').innerText = JSON.stringify(data, null, 2);
373
+ }
374
+
375
+ async function cleanup() {
376
+ const res = await fetch(baseUrl + '/cleanup', { method: 'POST' });
377
+ const data = await res.json();
378
+ document.getElementById('mgmtResult').innerText = JSON.stringify(data, null, 2);
379
+ }
380
+
381
+ async function nullify() {
382
+ const token = document.getElementById('fbToken').value;
383
+ const projectId = document.getElementById('projId').value;
384
+
385
+ const headers = {};
386
+ if(token) headers['Authorization'] = 'Bearer ' + token;
387
+
388
+ const res = await fetch(baseUrl + '/nullify', {
389
+ method: 'POST',
390
+ headers: { 'Content-Type': 'application/json', ...headers },
391
+ body: JSON.stringify({ projectId })
392
+ });
393
+ const data = await res.json();
394
+ document.getElementById('mgmtResult').innerText = JSON.stringify(data, null, 2);
395
+ }
396
+ </script>
397
+ </body>
398
+ </html>
399
+ `);
400
+ });
401
+
402
+ const PORT = process.env.PORT || 7860;
403
  app.listen(PORT, () => {
404
+ console.log(`🚀 Server running on http://localhost:${PORT}`);
405
  });