File size: 9,353 Bytes
707375a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
import express from 'express';
import http from 'http';
import { WebSocketServer } from 'ws';
import puppeteer from 'puppeteer-core';
import path from 'path';

// ==========================================
// 1. CONFIGURATION LAYER
// ==========================================
const PORT = process.env.PORT || 3000;
const CHROME_PATH = process.env.CHROME_PATH || '/usr/bin/google-chrome';
const EXTENSION_PATH = process.env.EXTENSION_PATH || '/var/www/my-custom-extension';

const app = express();
const server = http.createServer(app);

// Simple health check endpoint
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'healthy', timestamp: new Date() });
});

// Serve static interface if running standard setup
app.use(express.static(path.join(process.cwd(), 'public')));

// Create the WebSocket server wrapper tied to the HTTP server
const wss = new WebSocketServer({ server });

console.log(`[SYSTEM]: Configuration active. PORT: ${PORT}`);
console.log(`[SYSTEM]: Target Chrome Binary: ${CHROME_PATH}`);
console.log(`[SYSTEM]: Custom Chrome Extension: ${EXTENSION_PATH}`);

// ==========================================
// 2. WEBSOCKET CONTROLLER & LIFECYCLE
// ==========================================
wss.on('connection', (ws, req) => {
  const clientIp = req.socket.remoteAddress;
  console.log(`[WS]: New client connection established from IP: ${clientIp}`);

  // client-specific isolated browser variables
  let clientBrowser = null;
  let clientPage = null;
  let isScreencastActive = false;

  // Utility to safely dispatch JSON messages down the socket connection
  const sendToClient = (type, payload) => {
    if (ws.readyState === ws.OPEN) {
      ws.send(JSON.stringify({ type, ...payload }));
    }
  };

  const logToClient = (message) => {
    console.log(`[CLIENT-LOG]: ${message}`);
    sendToClient('log', { message });
  };

  const logErrorToClient = (message, err) => {
    const errorDetails = err ? `: ${err.message || err}` : '';
    console.error(`[CLIENT-ERR]: ${message}${errorDetails}`);
    sendToClient('log', { message: `[ERROR]: ${message}${errorDetails}`, isError: true });
    sendToClient('error', { error: `${message}${errorDetails}` });
  };

  // Safe browser cleanup method
  const cleanupBrowser = async () => {
    isScreencastActive = false;
    if (clientBrowser) {
      try {
        logToClient('[SYSTEM]: Cleaning up client browser context...');
        await clientBrowser.close();
        logToClient('[SYSTEM]: Chrome context successfully closed.');
      } catch (err) {
        console.error('[SYSTEM]: Failed to close browser context gracefully:', err.message);
      } finally {
        clientBrowser = null;
        clientPage = null;
      }
    }
  };

  // Event listener for incoming client automation payloads
  ws.on('message', async (message) => {
    try {
      const data = JSON.parse(message);
      const { action } = data;

      if (!action) {
        logErrorToClient('No automation action specified in payload.');
        return;
      }

      if (action === 'launch') {
        const { token, projectId, prompt } = data;
        
        if (!token) {
          logErrorToClient('Action "launch" requires a valid authentication token.');
          return;
        }
        if (!projectId) {
          logErrorToClient('Action "launch" requires a target projectId.');
          return;
        }
        if (!prompt) {
          logErrorToClient('Action "launch" requires an automation input prompt.');
          return;
        }

        // Avoid double launching browser contexts
        if (clientBrowser) {
          logToClient('[SYSTEM]: Cleaning up existing browser session before launching new context...');
          await cleanupBrowser();
        }

        logToClient('[SYSTEM]: Initiating Puppeteer launch controller...');

        // ==========================================
        // 3. PUPPETEER LOGIC BLOCK
        // ==========================================
        try {
          clientBrowser = await puppeteer.launch({
            executablePath: CHROME_PATH,
            headless: false, // Chrome extensions are only loaded in non-headless or headless=new (modern Chrome)
            args: [
              `--disable-extensions-except=${EXTENSION_PATH}`,
              `--load-extension=${EXTENSION_PATH}`,
              '--no-sandbox',
              '--disable-setuid-sandbox',
              '--headless=new', // modern headless argument
              '--disable-dev-shm-usage',
              '--disable-gpu',
              '--disable-software-rasterizer',
              '--disable-web-security'
            ],
            defaultViewport: {
              width: 1280,
              height: 720
            }
          });

          logToClient('[CHROME]: Browser instance spawned with unpacked extensions successfully.');

          // Open target page context
          const pages = await clientBrowser.pages();
          clientPage = pages.length > 0 ? pages[0] : await clientBrowser.newPage();

          // Catch any unexpected browser crashes
          clientBrowser.on('disconnected', () => {
            logToClient('[SYSTEM]: Remote Chrome process disconnected / closed.');
            cleanupBrowser();
          });

          // ==========================================
          // 4. CREDENTIAL INJECTION & AUTOMATION
          // ==========================================
          logToClient('[SYSTEM]: Navigating to Lovable.dev root domain to set context...');
          await clientPage.goto('https://lovable.dev', { waitUntil: 'domcontentloaded', timeout: 30000 });

          logToClient('[AUTH]: Injecting user authentication token into LocalStorage...');
          await clientPage.evaluate((authToken) => {
            localStorage.setItem('sb-auth-token', authToken);
          }, token);

          const targetUrl = `https://lovable.dev/projects/${projectId}`;
          logToClient(`[SYSTEM]: Navigating to target workspace: ${targetUrl}`);
          await clientPage.goto(targetUrl, { waitUntil: 'load', timeout: 45000 });

          // Wait for editor or workspace text input area to render
          logToClient('[AUTOMATION]: Waiting for prompt textarea input element...');
          const textSelector = 'textarea, div[contenteditable="true"]';
          await clientPage.waitForSelector(textSelector, { visible: true, timeout: 25000 });

          logToClient('[AUTOMATION]: Typing user prompt payload...');
          await clientPage.focus(textSelector);
          await clientPage.keyboard.type(prompt, { delay: 30 });

          logToClient('[AUTOMATION]: Dispatching user prompt payload to Lovable engine...');
          await clientPage.keyboard.press('Control+Enter');
          logToClient('[SYSTEM]: Prompt submission dispatched successfully.');

          // ==========================================
          // 5. GRAPHICAL SCREENCAST PIPELINE
          // ==========================================
          logToClient('[STREAM]: Starting graphical screencast captures (JPEG quality 50%)...');
          const clientDevTools = await clientPage.target().createCDPSession();
          
          await clientDevTools.send('Page.startScreencast', {
            format: 'jpeg',
            quality: 50,
            everyNthFrame: 1
          });

          isScreencastActive = true;

          clientDevTools.on('Page.screencastFrame', async (frame) => {
            if (!isScreencastActive) return;
            
            try {
              // Dispatch base64 frame back down the WebSocket client pipeline
              sendToClient('screen', { data: frame.data });
              
              // Acknowledge the frame to prevent rendering backlog bottlenecks
              await clientDevTools.send('Page.ackScreencastFrame', {
                sessionId: frame.sessionId
              });
            } catch (frameErr) {
              console.error('[STREAM]: Error processing screencast frame:', frameErr.message);
            }
          });

        } catch (automationErr) {
          logErrorToClient('Automation sequence interrupted by exception', automationErr);
          await cleanupBrowser();
        }
      } else if (action === 'stop') {
        logToClient('[SYSTEM]: Stop automation session requested by client.');
        await cleanupBrowser();
      } else {
        logErrorToClient(`Unknown action type: "${action}"`);
      }

    } catch (parseErr) {
      logErrorToClient('Failed to parse incoming WebSocket text message', parseErr);
    }
  });

  // ==========================================
  // 6. TEARDOWN & LIFECYCLE MANAGEMENT
  // ==========================================
  ws.on('close', async () => {
    console.log(`[WS]: Client connection closed for IP: ${clientIp}`);
    await cleanupBrowser();
  });

  ws.on('error', async (socketErr) => {
    console.error(`[WS]: Error encountered for IP: ${clientIp}:`, socketErr.message);
    await cleanupBrowser();
  });
});

// Bind server to port
server.listen(PORT, () => {
  console.log(`\n======================================================`);
  console.log(`🚀 Server successfully launched on port ${PORT}`);
  console.log(`🏥 Health Check endpoint: http://localhost:${PORT}/health`);
  console.log(`======================================================\n`);
});