api-response-manager 2.6.0 → 2.6.1

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.
Files changed (2) hide show
  1. package/commands/serve.js +148 -104
  2. package/package.json +1 -1
package/commands/serve.js CHANGED
@@ -443,125 +443,169 @@ async function connectFileTunnel(tunnelId, subdomain, localPort) {
443
443
  const token = api.getToken();
444
444
  const userId = config.get('userId');
445
445
 
446
- const ws = new WebSocket(tunnelServerUrl);
447
-
448
- ws.on('open', () => {
449
- console.log(chalk.green('āœ“ Connected to tunnel server'));
450
-
451
- ws.send(JSON.stringify({
452
- type: 'register',
453
- tunnelId,
454
- subdomain,
455
- localPort,
456
- protocol: 'http',
457
- authToken: token,
458
- userId
459
- }));
460
-
461
- // Heartbeat
462
- setInterval(() => {
463
- if (ws.readyState === WebSocket.OPEN) {
464
- ws.send(JSON.stringify({ type: 'heartbeat', tunnelId, subdomain }));
465
- }
466
- }, 30000);
467
- });
468
-
469
- // Track total bytes transferred
446
+ // Track total bytes transferred (persists across reconnects)
470
447
  let totalBytesTransferred = 0;
471
448
  let limitReached = false;
449
+ let heartbeatInterval = null;
450
+ let reconnectAttempts = 0;
451
+ const maxReconnectAttempts = 50; // Allow many reconnects
452
+ let isShuttingDown = false;
472
453
 
473
- ws.on('message', async (data) => {
474
- const message = JSON.parse(data.toString());
454
+ function connect() {
455
+ if (isShuttingDown) return;
475
456
 
476
- if (message.type === 'registered') {
477
- console.log(chalk.green.bold('\nšŸŽ‰ File Server is now publicly accessible!\n'));
478
- console.log(chalk.white('Share this URL:'));
479
- console.log(chalk.cyan.bold(` ${message.publicUrl}\n`));
480
- } else if (message.type === 'request') {
481
- const timestamp = new Date().toLocaleTimeString();
457
+ const ws = new WebSocket(tunnelServerUrl);
458
+
459
+ ws.on('open', () => {
460
+ reconnectAttempts = 0; // Reset on successful connection
461
+ console.log(chalk.green('āœ“ Connected to tunnel server'));
482
462
 
483
- // Check if limit was reached
484
- if (limitReached) {
485
- console.log(chalk.red(`[${timestamp}]`), chalk.red('BLOCKED'), chalk.white(message.path), chalk.red('- Transfer limit reached'));
486
- ws.send(JSON.stringify({
487
- type: 'response',
488
- requestId: message.requestId,
489
- statusCode: 429,
490
- headers: { 'content-type': 'application/json' },
491
- body: Buffer.from(JSON.stringify({
492
- error: 'Monthly file transfer limit reached',
493
- message: 'Upgrade your plan at https://tunnelapi.in/pricing for more transfer quota.'
494
- })).toString('base64'),
495
- encoding: 'base64'
496
- }));
497
- return;
463
+ ws.send(JSON.stringify({
464
+ type: 'register',
465
+ tunnelId,
466
+ subdomain,
467
+ localPort,
468
+ protocol: 'http',
469
+ authToken: token,
470
+ userId,
471
+ persistent: true // File serving tunnels should not have idle timeout
472
+ }));
473
+
474
+ // Clear any existing heartbeat
475
+ if (heartbeatInterval) {
476
+ clearInterval(heartbeatInterval);
498
477
  }
499
478
 
500
- console.log(chalk.gray(`[${timestamp}]`), chalk.blue(message.method), chalk.white(message.path));
501
-
502
- try {
503
- const localUrl = `http://localhost:${localPort}${message.path}`;
479
+ // Heartbeat every 15 seconds to keep connection alive
480
+ heartbeatInterval = setInterval(() => {
481
+ if (ws.readyState === WebSocket.OPEN) {
482
+ ws.send(JSON.stringify({ type: 'heartbeat', tunnelId, subdomain }));
483
+ }
484
+ }, 15000);
485
+ });
486
+
487
+ ws.on('message', async (data) => {
488
+ const message = JSON.parse(data.toString());
489
+
490
+ if (message.type === 'registered') {
491
+ console.log(chalk.green.bold('\nšŸŽ‰ File Server is now publicly accessible!\n'));
492
+ console.log(chalk.white('Share this URL:'));
493
+ console.log(chalk.cyan.bold(` ${message.publicUrl}\n`));
494
+ } else if (message.type === 'request') {
495
+ const timestamp = new Date().toLocaleTimeString();
496
+
497
+ // Check if limit was reached
498
+ if (limitReached) {
499
+ console.log(chalk.red(`[${timestamp}]`), chalk.red('BLOCKED'), chalk.white(message.path), chalk.red('- Transfer limit reached'));
500
+ ws.send(JSON.stringify({
501
+ type: 'response',
502
+ requestId: message.requestId,
503
+ statusCode: 429,
504
+ headers: { 'content-type': 'application/json' },
505
+ body: Buffer.from(JSON.stringify({
506
+ error: 'Monthly file transfer limit reached',
507
+ message: 'Upgrade your plan at https://tunnelapi.in/pricing for more transfer quota.'
508
+ })).toString('base64'),
509
+ encoding: 'base64'
510
+ }));
511
+ return;
512
+ }
513
+
514
+ console.log(chalk.gray(`[${timestamp}]`), chalk.blue(message.method), chalk.white(message.path));
504
515
 
505
- const response = await axios({
506
- method: message.method.toLowerCase(),
507
- url: localUrl,
508
- headers: { ...message.headers, host: `localhost:${localPort}` },
509
- data: message.body,
510
- validateStatus: () => true,
511
- responseType: 'arraybuffer',
512
- maxRedirects: 0,
513
- timeout: 30000
514
- });
515
-
516
- const cleanHeaders = { ...response.headers };
517
- delete cleanHeaders['transfer-encoding'];
518
- delete cleanHeaders['connection'];
519
-
520
- const responseData = Buffer.from(response.data);
521
- const bodyBase64 = responseData.toString('base64');
522
- const bytesTransferred = responseData.length;
523
-
524
- // Track file transfer usage
525
516
  try {
526
- await api.trackFileTransfer(userId, bytesTransferred);
527
- totalBytesTransferred += bytesTransferred;
528
- } catch (trackError) {
529
- if (trackError.response?.data?.limitReached) {
530
- limitReached = true;
531
- console.log(chalk.red.bold('\nāš ļø Monthly file transfer limit reached!'));
532
- console.log(chalk.yellow('šŸ’” Upgrade your plan at https://tunnelapi.in/pricing for more transfer quota.\n'));
517
+ const localUrl = `http://localhost:${localPort}${message.path}`;
518
+
519
+ const response = await axios({
520
+ method: message.method.toLowerCase(),
521
+ url: localUrl,
522
+ headers: { ...message.headers, host: `localhost:${localPort}` },
523
+ data: message.body,
524
+ validateStatus: () => true,
525
+ responseType: 'arraybuffer',
526
+ maxRedirects: 0,
527
+ timeout: 30000
528
+ });
529
+
530
+ const cleanHeaders = { ...response.headers };
531
+ delete cleanHeaders['transfer-encoding'];
532
+ delete cleanHeaders['connection'];
533
+
534
+ const responseData = Buffer.from(response.data);
535
+ const bodyBase64 = responseData.toString('base64');
536
+ const bytesTransferred = responseData.length;
537
+
538
+ // Track file transfer usage
539
+ try {
540
+ await api.trackFileTransfer(userId, bytesTransferred);
541
+ totalBytesTransferred += bytesTransferred;
542
+ } catch (trackError) {
543
+ if (trackError.response?.data?.limitReached) {
544
+ limitReached = true;
545
+ console.log(chalk.red.bold('\nāš ļø Monthly file transfer limit reached!'));
546
+ console.log(chalk.yellow('šŸ’” Upgrade your plan at https://tunnelapi.in/pricing for more transfer quota.\n'));
547
+ }
533
548
  }
549
+
550
+ ws.send(JSON.stringify({
551
+ type: 'response',
552
+ requestId: message.requestId,
553
+ statusCode: response.status,
554
+ headers: cleanHeaders,
555
+ body: bodyBase64,
556
+ encoding: 'base64'
557
+ }));
558
+ } catch (error) {
559
+ console.error(chalk.red(`Error: ${error.message}`));
560
+ ws.send(JSON.stringify({
561
+ type: 'response',
562
+ requestId: message.requestId,
563
+ statusCode: 502,
564
+ headers: { 'content-type': 'application/json' },
565
+ body: Buffer.from(JSON.stringify({ error: 'File server error' })).toString('base64'),
566
+ encoding: 'base64'
567
+ }));
534
568
  }
569
+ }
570
+ });
571
+
572
+ ws.on('error', (error) => {
573
+ console.error(chalk.red(`WebSocket error: ${error.message}`));
574
+ });
535
575
 
536
- ws.send(JSON.stringify({
537
- type: 'response',
538
- requestId: message.requestId,
539
- statusCode: response.status,
540
- headers: cleanHeaders,
541
- body: bodyBase64,
542
- encoding: 'base64'
543
- }));
544
- } catch (error) {
545
- console.error(chalk.red(`Error: ${error.message}`));
546
- ws.send(JSON.stringify({
547
- type: 'response',
548
- requestId: message.requestId,
549
- statusCode: 502,
550
- headers: { 'content-type': 'application/json' },
551
- body: Buffer.from(JSON.stringify({ error: 'File server error' })).toString('base64'),
552
- encoding: 'base64'
553
- }));
576
+ ws.on('close', (code, reason) => {
577
+ if (heartbeatInterval) {
578
+ clearInterval(heartbeatInterval);
579
+ heartbeatInterval = null;
580
+ }
581
+
582
+ if (isShuttingDown) {
583
+ console.log(chalk.yellow('Tunnel connection closed'));
584
+ return;
554
585
  }
555
- }
556
- });
557
586
 
558
- ws.on('error', (error) => {
559
- console.error(chalk.red(`WebSocket error: ${error.message}`));
560
- });
587
+ reconnectAttempts++;
588
+ if (reconnectAttempts <= maxReconnectAttempts) {
589
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts - 1), 30000); // Exponential backoff, max 30s
590
+ console.log(chalk.yellow(`Connection lost. Reconnecting in ${delay / 1000}s... (attempt ${reconnectAttempts}/${maxReconnectAttempts})`));
591
+ setTimeout(connect, delay);
592
+ } else {
593
+ console.log(chalk.red('Max reconnection attempts reached. Please restart the serve command.'));
594
+ }
595
+ });
596
+
597
+ // Handle process shutdown
598
+ process.on('SIGINT', () => {
599
+ isShuttingDown = true;
600
+ if (heartbeatInterval) {
601
+ clearInterval(heartbeatInterval);
602
+ }
603
+ ws.close();
604
+ });
605
+ }
561
606
 
562
- ws.on('close', () => {
563
- console.log(chalk.yellow('Tunnel connection closed'));
564
- });
607
+ // Start initial connection
608
+ connect();
565
609
  }
566
610
 
567
611
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-response-manager",
3
- "version": "2.6.0",
3
+ "version": "2.6.1",
4
4
  "description": "Command-line interface for API Response Manager",
5
5
  "main": "index.js",
6
6
  "bin": {