nstantpage-agent 0.5.32 → 0.5.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -180,10 +180,10 @@ export class LocalServer {
180
180
  }
181
181
  async start() {
182
182
  this.server = http.createServer(async (req, res) => {
183
- // CORS
183
+ // CORS — match gateway's headers so direct-localhost works from nstantpage.com
184
184
  res.setHeader('Access-Control-Allow-Origin', '*');
185
185
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
186
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
186
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-Project-Id');
187
187
  if (req.method === 'OPTIONS') {
188
188
  res.statusCode = 204;
189
189
  res.end();
@@ -201,6 +201,127 @@ export class LocalServer {
201
201
  }
202
202
  }
203
203
  });
204
+ // Handle WebSocket upgrades for direct-localhost terminal connections
205
+ // (same path as gateway: /live/terminal/ws/{projectId}/{sessionId})
206
+ this.server.on('upgrade', (req, socket, head) => {
207
+ const url = req.url || '';
208
+ const match = url.match(/^\/live\/terminal\/ws\/([^/]+)\/([^/?]+)/);
209
+ if (!match) {
210
+ socket.destroy();
211
+ return;
212
+ }
213
+ const sessionId = match[2];
214
+ const session = getTerminalSession(sessionId);
215
+ if (!session) {
216
+ socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
217
+ socket.destroy();
218
+ return;
219
+ }
220
+ // Complete WebSocket handshake (RFC 6455)
221
+ const key = req.headers['sec-websocket-key'];
222
+ if (!key) {
223
+ socket.destroy();
224
+ return;
225
+ }
226
+ const crypto = require('crypto');
227
+ const accept = crypto.createHash('sha1')
228
+ .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
229
+ .digest('base64');
230
+ socket.write('HTTP/1.1 101 Switching Protocols\r\n' +
231
+ 'Upgrade: websocket\r\n' +
232
+ 'Connection: Upgrade\r\n' +
233
+ `Sec-WebSocket-Accept: ${accept}\r\n` +
234
+ 'Access-Control-Allow-Origin: *\r\n' +
235
+ '\r\n');
236
+ // Wire PTY ↔ WebSocket (same JSON protocol as tunnel relay)
237
+ const sendFrame = (data) => {
238
+ const buf = Buffer.from(data, 'utf-8');
239
+ const frame = Buffer.alloc(buf.length < 126 ? 2 + buf.length : 4 + buf.length);
240
+ frame[0] = 0x81; // text frame, FIN
241
+ if (buf.length < 126) {
242
+ frame[1] = buf.length;
243
+ buf.copy(frame, 2);
244
+ }
245
+ else {
246
+ frame[1] = 126;
247
+ frame.writeUInt16BE(buf.length, 2);
248
+ buf.copy(frame, 4);
249
+ }
250
+ if (!socket.destroyed)
251
+ socket.write(frame);
252
+ };
253
+ // Send connection confirmation
254
+ sendFrame(JSON.stringify({ type: 'connected', sessionId, projectId: match[1] }));
255
+ const cleanup = attachTerminalClient(sessionId, {
256
+ onData: (data) => sendFrame(JSON.stringify({ type: 'output', data })),
257
+ onExit: (code) => {
258
+ sendFrame(JSON.stringify({ type: 'exit', exitCode: code }));
259
+ socket.end();
260
+ },
261
+ });
262
+ // Parse incoming WebSocket frames from browser
263
+ let pending = Buffer.alloc(0);
264
+ socket.on('data', (chunk) => {
265
+ pending = Buffer.concat([pending, chunk]);
266
+ while (pending.length >= 2) {
267
+ const masked = (pending[1] & 0x80) !== 0;
268
+ let payloadLen = pending[1] & 0x7f;
269
+ let offset = 2;
270
+ if (payloadLen === 126) {
271
+ if (pending.length < 4)
272
+ return;
273
+ payloadLen = pending.readUInt16BE(2);
274
+ offset = 4;
275
+ }
276
+ else if (payloadLen === 127) {
277
+ if (pending.length < 10)
278
+ return;
279
+ payloadLen = Number(pending.readBigUInt64BE(2));
280
+ offset = 10;
281
+ }
282
+ if (masked)
283
+ offset += 4;
284
+ if (pending.length < offset + payloadLen)
285
+ return;
286
+ const opcode = pending[0] & 0x0f;
287
+ if (opcode === 0x08) { // close
288
+ socket.end();
289
+ break;
290
+ }
291
+ let payload = pending.subarray(offset, offset + payloadLen);
292
+ if (masked) {
293
+ const maskKey = pending.subarray(offset - 4, offset);
294
+ payload = Buffer.from(payload);
295
+ for (let i = 0; i < payload.length; i++) {
296
+ payload[i] ^= maskKey[i % 4];
297
+ }
298
+ }
299
+ if (opcode === 0x01) { // text — JSON messages from frontend
300
+ try {
301
+ const msg = JSON.parse(payload.toString('utf-8'));
302
+ if (msg.type === 'input' && typeof msg.data === 'string') {
303
+ writeToTerminalSession(session, msg.data);
304
+ session.lastActivity = Date.now();
305
+ }
306
+ else if (msg.type === 'resize' && typeof msg.cols === 'number' && typeof msg.rows === 'number') {
307
+ resizeTerminalSession(session, msg.cols, msg.rows);
308
+ }
309
+ }
310
+ catch { /* invalid JSON — ignore */ }
311
+ }
312
+ else if (opcode === 0x09) { // ping
313
+ const pong = Buffer.alloc(2 + payloadLen);
314
+ pong[0] = 0x8a;
315
+ pong[1] = payloadLen;
316
+ payload.copy(pong, 2);
317
+ socket.write(pong);
318
+ }
319
+ pending = pending.subarray(offset + payloadLen);
320
+ }
321
+ });
322
+ socket.on('close', () => cleanup?.());
323
+ socket.on('error', () => cleanup?.());
324
+ });
204
325
  await new Promise((resolve, reject) => {
205
326
  this.server.listen(this.options.apiPort, '127.0.0.1', () => {
206
327
  console.log(` [LocalServer] API server on port ${this.options.apiPort}`);
@@ -519,7 +640,14 @@ export class LocalServer {
519
640
  const rows = parsed.rows || 30;
520
641
  // Determine shell
521
642
  const shellCmd = process.platform === 'win32' ? 'cmd.exe' : (process.env.SHELL || '/bin/bash');
522
- const shellEnv = { ...process.env, TERM: 'xterm-256color', COLUMNS: String(cols), LINES: String(rows) };
643
+ const shellEnv = {
644
+ ...process.env,
645
+ TERM: 'xterm-256color',
646
+ COLORTERM: 'truecolor',
647
+ FORCE_COLOR: '3',
648
+ COLUMNS: String(cols),
649
+ LINES: String(rows),
650
+ };
523
651
  sessionCounter++;
524
652
  const label = parsed.label || `Terminal ${sessionCounter}`;
525
653
  let shell = null;
package/dist/tunnel.js CHANGED
@@ -475,6 +475,10 @@ export class TunnelClient {
475
475
  case 'start-project':
476
476
  this.handleStartProject(msg.projectId, msg.clean);
477
477
  break;
478
+ case 'force-disconnect':
479
+ console.log(' [Tunnel] Received force-disconnect — will not reconnect');
480
+ this.shouldReconnect = false;
481
+ break;
478
482
  default:
479
483
  console.warn(` [Tunnel] Unknown message type: ${msg.type}`);
480
484
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nstantpage-agent",
3
- "version": "0.5.32",
3
+ "version": "0.5.33",
4
4
  "description": "Local development agent for nstantpage.com — run your projects locally, preview in the cloud. Replaces cloud containers for faster builds.",
5
5
  "type": "module",
6
6
  "bin": {