api-response-manager 2.3.0 → 2.5.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Vijay Singh Purohit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,12 +1,18 @@
1
1
  # ARM CLI - API Response Manager Command Line Interface
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/api-response-manager.svg)](https://www.npmjs.com/package/api-response-manager)
4
+ [![npm downloads](https://img.shields.io/npm/dt/api-response-manager.svg)](https://www.npmjs.com/package/api-response-manager)
5
+ [![License: Proprietary](https://img.shields.io/badge/License-Proprietary-red.svg)](https://github.com/vijaypurohit322/api-response-manager/blob/main/LICENSE)
6
+
3
7
  Command-line interface for API Response Manager. Manage tunnels, webhooks, and projects from your terminal.
4
8
 
9
+ **Version:** 2.5.0 | **Live Service:** https://tunnelapi.in
10
+
5
11
  ## Installation
6
12
 
7
13
  ### Option 1: Install from npm (Recommended)
8
14
  ```bash
9
- npm install -g @vijaypurohit322-arm/cli
15
+ npm install -g api-response-manager
10
16
  ```
11
17
 
12
18
  After installation, verify:
@@ -33,9 +39,9 @@ arm --version
33
39
 
34
40
  ### Option 3: Use with npx (No Installation)
35
41
  ```bash
36
- npx @vijaypurohit322-arm/cli login
37
- npx @vijaypurohit322-arm/cli tunnel 3000
38
- npx @vijaypurohit322-arm/cli webhook
42
+ npx api-response-manager login
43
+ npx api-response-manager tunnel 3000
44
+ npx api-response-manager webhook
39
45
  ```
40
46
 
41
47
  ## Quick Start
@@ -444,12 +450,31 @@ Default configuration:
444
450
  # Login
445
451
  arm login
446
452
 
447
- # Start HTTPS tunnel on port 3000
448
- arm tunnel 3000 --protocol https --ssl --subdomain myapp
453
+ # Start tunnel on port 3000 with custom subdomain
454
+ arm tunnel 3000 --subdomain myapp
449
455
 
450
456
  # Your local server is now accessible at:
451
- # https://myapp.tunnel.arm.dev
452
- ```
457
+ # https://myapp.free-tunnelapi.app
458
+
459
+ # Output:
460
+ # 🚇 Starting Tunnel...
461
+ # ✔ Tunnel created successfully!
462
+ # ┌─────────────────────────────────────────────┐
463
+ # │ Tunnel Information │
464
+ # ├─────────────────────────────────────────────┤
465
+ # │ Name: myapp │
466
+ # │ Public URL: https://myapp.free-tunnelapi.app│
467
+ # │ Local Port: 3000 │
468
+ # └─────────────────────────────────────────────┘
469
+ # 🎉 Tunnel Active!
470
+ ```
471
+
472
+ ### Tunnel Timeouts (Industry Standard)
473
+ | Setting | Value | Description |
474
+ |---------|-------|-------------|
475
+ | Heartbeat | 30 seconds | CLI sends keepalive every 30s |
476
+ | Idle Timeout | 2 hours | Closes after 2 hours of no requests |
477
+ | Max Session | 24 hours | Requires reconnect after 24 hours |
453
478
 
454
479
  ### Secure Tunnel with OAuth Authentication
455
480
  ```bash
@@ -563,7 +588,7 @@ source ~/.bashrc
563
588
 
564
589
  **Alternative - Use npx:**
565
590
  ```bash
566
- npx @vijaypurohit322-arm/cli login
591
+ npx api-response-manager login
567
592
  ```
568
593
 
569
594
  ## Publishing to npm
@@ -591,4 +616,11 @@ npm publish --access public
591
616
 
592
617
  ## License
593
618
 
594
- MIT License - see LICENSE file for details
619
+ This software is proprietary. See [LICENSE](https://github.com/vijaypurohit322/api-response-manager/blob/main/LICENSE) for details.
620
+
621
+ **Key Points:**
622
+ - ✅ Personal and educational use allowed
623
+ - ✅ Self-hosting for non-commercial use allowed
624
+ - ❌ Commercial use requires separate license
625
+ - ❌ Resale or redistribution prohibited
626
+ - 📧 Contact: vijaypurohit322@gmail.com
package/bin/arm.js CHANGED
@@ -17,6 +17,10 @@ const webhookCommand = require('../commands/webhook');
17
17
  const projectCommand = require('../commands/project');
18
18
  const logsCommand = require('../commands/logs');
19
19
  const configCommand = require('../commands/config');
20
+ const ipWhitelistCommand = require('../commands/ipWhitelist');
21
+ const ipBlacklistCommand = require('../commands/ipBlacklist');
22
+ const rateLimitCommand = require('../commands/rateLimit');
23
+ const healthCommand = require('../commands/health');
20
24
 
21
25
  // CLI setup
22
26
  program
@@ -127,6 +131,85 @@ program
127
131
  .option('--tls', 'Enable TLS for ingress')
128
132
  .action(tunnelCommand.configureIngress);
129
133
 
134
+ // IP Whitelist commands
135
+ program
136
+ .command('tunnel:ip-whitelist:add')
137
+ .description('Add IP to tunnel whitelist')
138
+ .argument('<tunnelId>', 'Tunnel ID')
139
+ .argument('<ip>', 'IP address or CIDR range (e.g., 192.168.1.100 or 10.0.0.0/8)')
140
+ .action(ipWhitelistCommand.add);
141
+
142
+ program
143
+ .command('tunnel:ip-whitelist:remove')
144
+ .description('Remove IP from tunnel whitelist')
145
+ .argument('<tunnelId>', 'Tunnel ID')
146
+ .argument('<ip>', 'IP address or CIDR range')
147
+ .action(ipWhitelistCommand.remove);
148
+
149
+ program
150
+ .command('tunnel:ip-whitelist:list')
151
+ .description('List tunnel IP whitelist')
152
+ .argument('<tunnelId>', 'Tunnel ID')
153
+ .action(ipWhitelistCommand.list);
154
+
155
+ program
156
+ .command('tunnel:ip-whitelist:clear')
157
+ .description('Clear tunnel IP whitelist')
158
+ .argument('<tunnelId>', 'Tunnel ID')
159
+ .action(ipWhitelistCommand.clear);
160
+
161
+ // IP Blacklist commands
162
+ program
163
+ .command('tunnel:ip-blacklist:add')
164
+ .description('Add IP to tunnel blacklist')
165
+ .argument('<tunnelId>', 'Tunnel ID')
166
+ .argument('<ip>', 'IP address or CIDR range')
167
+ .action(ipBlacklistCommand.add);
168
+
169
+ program
170
+ .command('tunnel:ip-blacklist:remove')
171
+ .description('Remove IP from tunnel blacklist')
172
+ .argument('<tunnelId>', 'Tunnel ID')
173
+ .argument('<ip>', 'IP address or CIDR range')
174
+ .action(ipBlacklistCommand.remove);
175
+
176
+ program
177
+ .command('tunnel:ip-blacklist:list')
178
+ .description('List tunnel IP blacklist')
179
+ .argument('<tunnelId>', 'Tunnel ID')
180
+ .action(ipBlacklistCommand.list);
181
+
182
+ program
183
+ .command('tunnel:ip-blacklist:clear')
184
+ .description('Clear tunnel IP blacklist')
185
+ .argument('<tunnelId>', 'Tunnel ID')
186
+ .action(ipBlacklistCommand.clear);
187
+
188
+ // Rate Limit commands
189
+ program
190
+ .command('tunnel:rate-limit')
191
+ .description('Configure tunnel rate limits')
192
+ .argument('<tunnelId>', 'Tunnel ID')
193
+ .option('--rpm <number>', 'Requests per minute')
194
+ .option('--rph <number>', 'Requests per hour')
195
+ .option('--rpd <number>', 'Requests per day')
196
+ .option('--enable', 'Enable rate limiting')
197
+ .option('--disable', 'Disable rate limiting')
198
+ .action(rateLimitCommand.configure);
199
+
200
+ program
201
+ .command('tunnel:rate-limit:show')
202
+ .description('Show tunnel rate limits')
203
+ .argument('<tunnelId>', 'Tunnel ID')
204
+ .action(rateLimitCommand.show);
205
+
206
+ // Health check command
207
+ program
208
+ .command('health')
209
+ .description('Check server health')
210
+ .option('-d, --detailed', 'Show detailed health information')
211
+ .action(healthCommand.check);
212
+
130
213
  // Webhook commands
131
214
  program
132
215
  .command('webhook')
@@ -0,0 +1,114 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const axios = require('axios');
4
+ const config = require('../utils/config');
5
+
6
+ // Check basic health
7
+ async function check(options) {
8
+ const apiUrl = config.get('apiUrl') || 'http://localhost:5000/api';
9
+ // Remove /api prefix since apiUrl already includes it
10
+ const endpoint = options.detailed ? '/health/detailed' : '/health';
11
+
12
+ const spinner = ora('Checking server health...').start();
13
+
14
+ try {
15
+ const response = await axios.get(`${apiUrl}${endpoint}`, {
16
+ validateStatus: (status) => status < 500 || status === 503 // Accept 503 for degraded status
17
+ });
18
+ const health = response.data;
19
+
20
+ spinner.stop();
21
+
22
+ if (options.detailed) {
23
+ displayDetailedHealth(health);
24
+ } else {
25
+ displayBasicHealth(health);
26
+ }
27
+
28
+ // Exit with 0 for ok/degraded, 1 for error
29
+ process.exit(health.status === 'ok' || health.status === 'degraded' ? 0 : 1);
30
+ } catch (error) {
31
+ spinner.fail(chalk.red('Server is unreachable'));
32
+ console.error(chalk.red(`\n✗ ${error.message}\n`));
33
+ process.exit(1);
34
+ }
35
+ }
36
+
37
+ function displayBasicHealth(health) {
38
+ console.log(chalk.blue.bold('\n🏥 Server Health Check\n'));
39
+
40
+ const statusColor = health.status === 'ok' ? chalk.green : chalk.red;
41
+ const statusIcon = health.status === 'ok' ? '✓' : '✗';
42
+
43
+ console.log(chalk.gray('┌─────────────────────────────────────┐'));
44
+ console.log(chalk.gray('│') + chalk.white(' Status: ') + statusColor(`${statusIcon} ${health.status.toUpperCase()}`).padEnd(28) + chalk.gray('│'));
45
+ console.log(chalk.gray('│') + chalk.white(' Uptime: ') + chalk.cyan(formatUptime(health.uptime)).padEnd(20) + chalk.gray('│'));
46
+ console.log(chalk.gray('│') + chalk.white(' Environment: ') + chalk.cyan(health.environment).padEnd(20) + chalk.gray('│'));
47
+ console.log(chalk.gray('│') + chalk.white(' Timestamp: ') + chalk.gray(new Date(health.timestamp).toLocaleString()).padEnd(20) + chalk.gray('│'));
48
+ console.log(chalk.gray('└─────────────────────────────────────┘\n'));
49
+ }
50
+
51
+ function displayDetailedHealth(health) {
52
+ console.log(chalk.blue.bold('\n🏥 Detailed Server Health Check\n'));
53
+
54
+ const statusColor = health.status === 'ok' ? chalk.green : (health.status === 'degraded' ? chalk.yellow : chalk.red);
55
+ const statusIcon = health.status === 'ok' ? '✓' : (health.status === 'degraded' ? '⚠' : '✗');
56
+
57
+ console.log(chalk.gray('┌─────────────────────────────────────────────────────┐'));
58
+ console.log(chalk.gray('│') + chalk.white.bold(' Overall Status ') + chalk.gray('│'));
59
+ console.log(chalk.gray('├─────────────────────────────────────────────────────┤'));
60
+ console.log(chalk.gray('│') + chalk.white(' Status: ') + statusColor(`${statusIcon} ${health.status.toUpperCase()}`).padEnd(42) + chalk.gray('│'));
61
+ console.log(chalk.gray('│') + chalk.white(' Uptime: ') + chalk.cyan(formatUptime(health.uptime)).padEnd(34) + chalk.gray('│'));
62
+ console.log(chalk.gray('│') + chalk.white(' Environment: ') + chalk.cyan(health.environment).padEnd(34) + chalk.gray('│'));
63
+ console.log(chalk.gray('│') + chalk.white(' Version: ') + chalk.cyan(health.version || 'N/A').padEnd(34) + chalk.gray('│'));
64
+ console.log(chalk.gray('└─────────────────────────────────────────────────────┘\n'));
65
+
66
+ if (health.checks) {
67
+ // Database
68
+ console.log(chalk.white.bold('Database:'));
69
+ displayCheckStatus(health.checks.database);
70
+
71
+ // Redis
72
+ console.log(chalk.white.bold('\nRedis:'));
73
+ displayCheckStatus(health.checks.redis);
74
+
75
+ // Memory
76
+ console.log(chalk.white.bold('\nMemory:'));
77
+ displayCheckStatus(health.checks.memory);
78
+
79
+ console.log();
80
+ }
81
+ }
82
+
83
+ function displayCheckStatus(check) {
84
+ const statusColor = check.status === 'ok' ? chalk.green :
85
+ check.status === 'warning' ? chalk.yellow :
86
+ check.status === 'not configured' ? chalk.gray : chalk.red;
87
+ const statusIcon = check.status === 'ok' ? '✓' :
88
+ check.status === 'warning' ? '⚠' :
89
+ check.status === 'not configured' ? '○' : '✗';
90
+
91
+ console.log(` ${statusColor(statusIcon)} ${statusColor(check.status.toUpperCase())}`);
92
+
93
+ Object.keys(check).forEach(key => {
94
+ if (key !== 'status') {
95
+ console.log(` ${chalk.gray(key + ':')} ${chalk.white(check[key])}`);
96
+ }
97
+ });
98
+ }
99
+
100
+ function formatUptime(seconds) {
101
+ const days = Math.floor(seconds / 86400);
102
+ const hours = Math.floor((seconds % 86400) / 3600);
103
+ const minutes = Math.floor((seconds % 3600) / 60);
104
+ const secs = Math.floor(seconds % 60);
105
+
106
+ if (days > 0) return `${days}d ${hours}h ${minutes}m`;
107
+ if (hours > 0) return `${hours}h ${minutes}m ${secs}s`;
108
+ if (minutes > 0) return `${minutes}m ${secs}s`;
109
+ return `${secs}s`;
110
+ }
111
+
112
+ module.exports = {
113
+ check
114
+ };
@@ -0,0 +1,121 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const Table = require('cli-table3');
4
+ const api = require('../utils/api');
5
+
6
+ // Add IP to blacklist
7
+ async function add(tunnelId, ip) {
8
+ const spinner = ora('Adding IP to blacklist...').start();
9
+
10
+ try {
11
+ // Get current tunnel
12
+ const tunnel = await api.getTunnel(tunnelId);
13
+ const currentBlacklist = tunnel.tunnel.ipBlacklist || [];
14
+
15
+ // Check if IP already exists
16
+ if (currentBlacklist.includes(ip)) {
17
+ spinner.fail(chalk.yellow(`IP ${ip} is already in the blacklist`));
18
+ return;
19
+ }
20
+
21
+ // Add new IP
22
+ const updatedBlacklist = [...currentBlacklist, ip];
23
+ await api.updateIPBlacklist(tunnelId, updatedBlacklist);
24
+
25
+ spinner.succeed(chalk.green(`IP ${ip} added to blacklist`));
26
+ console.log(chalk.gray(`\nTotal blacklisted IPs: ${updatedBlacklist.length}\n`));
27
+ } catch (error) {
28
+ spinner.fail(chalk.red('Failed to add IP to blacklist'));
29
+ console.error(chalk.red(`\n✗ ${error.response?.data?.msg || error.message}\n`));
30
+ process.exit(1);
31
+ }
32
+ }
33
+
34
+ // Remove IP from blacklist
35
+ async function remove(tunnelId, ip) {
36
+ const spinner = ora('Removing IP from blacklist...').start();
37
+
38
+ try {
39
+ // Get current tunnel
40
+ const tunnel = await api.getTunnel(tunnelId);
41
+ const currentBlacklist = tunnel.tunnel.ipBlacklist || [];
42
+
43
+ // Check if IP exists
44
+ if (!currentBlacklist.includes(ip)) {
45
+ spinner.fail(chalk.yellow(`IP ${ip} is not in the blacklist`));
46
+ return;
47
+ }
48
+
49
+ // Remove IP
50
+ const updatedBlacklist = currentBlacklist.filter(item => item !== ip);
51
+ await api.updateIPBlacklist(tunnelId, updatedBlacklist);
52
+
53
+ spinner.succeed(chalk.green(`IP ${ip} removed from blacklist`));
54
+ console.log(chalk.gray(`\nTotal blacklisted IPs: ${updatedBlacklist.length}\n`));
55
+ } catch (error) {
56
+ spinner.fail(chalk.red('Failed to remove IP from blacklist'));
57
+ console.error(chalk.red(`\n✗ ${error.response?.data?.msg || error.message}\n`));
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ // List blacklisted IPs
63
+ async function list(tunnelId) {
64
+ const spinner = ora('Fetching IP blacklist...').start();
65
+
66
+ try {
67
+ const tunnel = await api.getTunnel(tunnelId);
68
+ const blacklist = tunnel.tunnel.ipBlacklist || [];
69
+
70
+ spinner.stop();
71
+
72
+ if (blacklist.length === 0) {
73
+ console.log(chalk.yellow('\n⚠ No IP blacklist configured. No IPs are blocked.\n'));
74
+ return;
75
+ }
76
+
77
+ console.log(chalk.blue.bold(`\n🚫 IP Blacklist (${blacklist.length} IPs)\n`));
78
+
79
+ const table = new Table({
80
+ head: [chalk.white('IP Address / CIDR')],
81
+ style: {
82
+ head: ['cyan'],
83
+ border: ['gray']
84
+ }
85
+ });
86
+
87
+ blacklist.forEach(ip => {
88
+ table.push([chalk.red(ip)]);
89
+ });
90
+
91
+ console.log(table.toString());
92
+ console.log();
93
+ } catch (error) {
94
+ spinner.fail(chalk.red('Failed to fetch IP blacklist'));
95
+ console.error(chalk.red(`\n✗ ${error.response?.data?.msg || error.message}\n`));
96
+ process.exit(1);
97
+ }
98
+ }
99
+
100
+ // Clear all blacklisted IPs
101
+ async function clear(tunnelId) {
102
+ const spinner = ora('Clearing IP blacklist...').start();
103
+
104
+ try {
105
+ await api.updateIPBlacklist(tunnelId, []);
106
+
107
+ spinner.succeed(chalk.green('IP blacklist cleared'));
108
+ console.log(chalk.gray('\nNo IPs are blocked.\n'));
109
+ } catch (error) {
110
+ spinner.fail(chalk.red('Failed to clear IP blacklist'));
111
+ console.error(chalk.red(`\n✗ ${error.response?.data?.msg || error.message}\n`));
112
+ process.exit(1);
113
+ }
114
+ }
115
+
116
+ module.exports = {
117
+ add,
118
+ remove,
119
+ list,
120
+ clear
121
+ };
@@ -0,0 +1,121 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const Table = require('cli-table3');
4
+ const api = require('../utils/api');
5
+
6
+ // Add IP to whitelist
7
+ async function add(tunnelId, ip) {
8
+ const spinner = ora('Adding IP to whitelist...').start();
9
+
10
+ try {
11
+ // Get current tunnel
12
+ const tunnel = await api.getTunnel(tunnelId);
13
+ const currentWhitelist = tunnel.tunnel.ipWhitelist || [];
14
+
15
+ // Check if IP already exists
16
+ if (currentWhitelist.includes(ip)) {
17
+ spinner.fail(chalk.yellow(`IP ${ip} is already in the whitelist`));
18
+ return;
19
+ }
20
+
21
+ // Add new IP
22
+ const updatedWhitelist = [...currentWhitelist, ip];
23
+ await api.updateIPWhitelist(tunnelId, updatedWhitelist);
24
+
25
+ spinner.succeed(chalk.green(`IP ${ip} added to whitelist`));
26
+ console.log(chalk.gray(`\nTotal whitelisted IPs: ${updatedWhitelist.length}\n`));
27
+ } catch (error) {
28
+ spinner.fail(chalk.red('Failed to add IP to whitelist'));
29
+ console.error(chalk.red(`\n✗ ${error.response?.data?.msg || error.message}\n`));
30
+ process.exit(1);
31
+ }
32
+ }
33
+
34
+ // Remove IP from whitelist
35
+ async function remove(tunnelId, ip) {
36
+ const spinner = ora('Removing IP from whitelist...').start();
37
+
38
+ try {
39
+ // Get current tunnel
40
+ const tunnel = await api.getTunnel(tunnelId);
41
+ const currentWhitelist = tunnel.tunnel.ipWhitelist || [];
42
+
43
+ // Check if IP exists
44
+ if (!currentWhitelist.includes(ip)) {
45
+ spinner.fail(chalk.yellow(`IP ${ip} is not in the whitelist`));
46
+ return;
47
+ }
48
+
49
+ // Remove IP
50
+ const updatedWhitelist = currentWhitelist.filter(item => item !== ip);
51
+ await api.updateIPWhitelist(tunnelId, updatedWhitelist);
52
+
53
+ spinner.succeed(chalk.green(`IP ${ip} removed from whitelist`));
54
+ console.log(chalk.gray(`\nTotal whitelisted IPs: ${updatedWhitelist.length}\n`));
55
+ } catch (error) {
56
+ spinner.fail(chalk.red('Failed to remove IP from whitelist'));
57
+ console.error(chalk.red(`\n✗ ${error.response?.data?.msg || error.message}\n`));
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ // List whitelisted IPs
63
+ async function list(tunnelId) {
64
+ const spinner = ora('Fetching IP whitelist...').start();
65
+
66
+ try {
67
+ const tunnel = await api.getTunnel(tunnelId);
68
+ const whitelist = tunnel.tunnel.ipWhitelist || [];
69
+
70
+ spinner.stop();
71
+
72
+ if (whitelist.length === 0) {
73
+ console.log(chalk.yellow('\n⚠ No IP whitelist configured. All IPs are allowed.\n'));
74
+ return;
75
+ }
76
+
77
+ console.log(chalk.blue.bold(`\n🔒 IP Whitelist (${whitelist.length} IPs)\n`));
78
+
79
+ const table = new Table({
80
+ head: [chalk.white('IP Address / CIDR')],
81
+ style: {
82
+ head: ['cyan'],
83
+ border: ['gray']
84
+ }
85
+ });
86
+
87
+ whitelist.forEach(ip => {
88
+ table.push([chalk.green(ip)]);
89
+ });
90
+
91
+ console.log(table.toString());
92
+ console.log();
93
+ } catch (error) {
94
+ spinner.fail(chalk.red('Failed to fetch IP whitelist'));
95
+ console.error(chalk.red(`\n✗ ${error.response?.data?.msg || error.message}\n`));
96
+ process.exit(1);
97
+ }
98
+ }
99
+
100
+ // Clear all whitelisted IPs
101
+ async function clear(tunnelId) {
102
+ const spinner = ora('Clearing IP whitelist...').start();
103
+
104
+ try {
105
+ await api.updateIPWhitelist(tunnelId, []);
106
+
107
+ spinner.succeed(chalk.green('IP whitelist cleared'));
108
+ console.log(chalk.gray('\nAll IPs are now allowed.\n'));
109
+ } catch (error) {
110
+ spinner.fail(chalk.red('Failed to clear IP whitelist'));
111
+ console.error(chalk.red(`\n✗ ${error.response?.data?.msg || error.message}\n`));
112
+ process.exit(1);
113
+ }
114
+ }
115
+
116
+ module.exports = {
117
+ add,
118
+ remove,
119
+ list,
120
+ clear
121
+ };
@@ -0,0 +1,69 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const api = require('../utils/api');
4
+
5
+ // Configure rate limits
6
+ async function configure(tunnelId, options) {
7
+ const spinner = ora('Updating rate limits...').start();
8
+
9
+ try {
10
+ // Get current tunnel
11
+ const tunnel = await api.getTunnel(tunnelId);
12
+ const currentRateLimit = tunnel.tunnel.rateLimit || {};
13
+
14
+ // Build update object
15
+ const updates = {
16
+ rateLimit: {
17
+ enabled: options.disable ? false : (options.enable ? true : currentRateLimit.enabled),
18
+ requestsPerMinute: options.rpm || currentRateLimit.requestsPerMinute,
19
+ requestsPerHour: options.rph || currentRateLimit.requestsPerHour,
20
+ requestsPerDay: options.rpd || currentRateLimit.requestsPerDay
21
+ }
22
+ };
23
+
24
+ await api.updateTunnel(tunnelId, updates);
25
+
26
+ spinner.succeed(chalk.green('Rate limits updated successfully'));
27
+
28
+ console.log(chalk.blue.bold('\n⚡ Rate Limit Configuration\n'));
29
+ console.log(chalk.gray('┌─────────────────────────────────────┐'));
30
+ console.log(chalk.gray('│') + chalk.white(' Status: ') + (updates.rateLimit.enabled ? chalk.green('Enabled') : chalk.red('Disabled')).padEnd(28) + chalk.gray('│'));
31
+ console.log(chalk.gray('│') + chalk.white(' Per Minute: ') + chalk.cyan(String(updates.rateLimit.requestsPerMinute)).padEnd(20) + chalk.gray('│'));
32
+ console.log(chalk.gray('│') + chalk.white(' Per Hour: ') + chalk.cyan(String(updates.rateLimit.requestsPerHour)).padEnd(20) + chalk.gray('│'));
33
+ console.log(chalk.gray('│') + chalk.white(' Per Day: ') + chalk.cyan(String(updates.rateLimit.requestsPerDay)).padEnd(20) + chalk.gray('│'));
34
+ console.log(chalk.gray('└─────────────────────────────────────┘\n'));
35
+ } catch (error) {
36
+ spinner.fail(chalk.red('Failed to update rate limits'));
37
+ console.error(chalk.red(`\n✗ ${error.response?.data?.msg || error.message}\n`));
38
+ process.exit(1);
39
+ }
40
+ }
41
+
42
+ // Show current rate limits
43
+ async function show(tunnelId) {
44
+ const spinner = ora('Fetching rate limits...').start();
45
+
46
+ try {
47
+ const tunnel = await api.getTunnel(tunnelId);
48
+ const rateLimit = tunnel.tunnel.rateLimit || {};
49
+
50
+ spinner.stop();
51
+
52
+ console.log(chalk.blue.bold('\n⚡ Rate Limit Configuration\n'));
53
+ console.log(chalk.gray('┌─────────────────────────────────────┐'));
54
+ console.log(chalk.gray('│') + chalk.white(' Status: ') + (rateLimit.enabled ? chalk.green('Enabled') : chalk.red('Disabled')).padEnd(28) + chalk.gray('│'));
55
+ console.log(chalk.gray('│') + chalk.white(' Per Minute: ') + chalk.cyan(String(rateLimit.requestsPerMinute || 60)).padEnd(20) + chalk.gray('│'));
56
+ console.log(chalk.gray('│') + chalk.white(' Per Hour: ') + chalk.cyan(String(rateLimit.requestsPerHour || 1000)).padEnd(20) + chalk.gray('│'));
57
+ console.log(chalk.gray('│') + chalk.white(' Per Day: ') + chalk.cyan(String(rateLimit.requestsPerDay || 10000)).padEnd(20) + chalk.gray('│'));
58
+ console.log(chalk.gray('└─────────────────────────────────────┘\n'));
59
+ } catch (error) {
60
+ spinner.fail(chalk.red('Failed to fetch rate limits'));
61
+ console.error(chalk.red(`\n✗ ${error.response?.data?.msg || error.message}\n`));
62
+ process.exit(1);
63
+ }
64
+ }
65
+
66
+ module.exports = {
67
+ configure,
68
+ show
69
+ };
@@ -2,6 +2,7 @@ const chalk = require('chalk');
2
2
  const ora = require('ora');
3
3
  const WebSocket = require('ws');
4
4
  const Table = require('cli-table3');
5
+ const axios = require('axios');
5
6
  const api = require('../utils/api');
6
7
  const config = require('../utils/config');
7
8
 
@@ -38,18 +39,24 @@ async function start(port, options) {
38
39
 
39
40
  spinner.succeed(chalk.green('Tunnel created successfully!'));
40
41
 
42
+ // Safe string padding function
43
+ const safePadEnd = (str, length) => {
44
+ const s = String(str || '');
45
+ return s.length >= length ? s : s + ' '.repeat(length - s.length);
46
+ };
47
+
41
48
  console.log(chalk.gray('\n┌─────────────────────────────────────────────┐'));
42
49
  console.log(chalk.gray('│') + chalk.white.bold(' Tunnel Information ') + chalk.gray('│'));
43
50
  console.log(chalk.gray('├─────────────────────────────────────────────┤'));
44
- console.log(chalk.gray('│') + chalk.gray(' Name: ') + chalk.white(tunnel.name.padEnd(28)) + chalk.gray('│'));
45
- console.log(chalk.gray('│') + chalk.gray(' Public URL: ') + chalk.cyan(tunnel.publicUrl.padEnd(28)) + chalk.gray('│'));
46
- console.log(chalk.gray('│') + chalk.gray(' Local Port: ') + chalk.white(String(tunnel.localPort).padEnd(28)) + chalk.gray('│'));
47
- console.log(chalk.gray('│') + chalk.gray(' Tunnel ID: ') + chalk.yellow(tunnel._id.padEnd(28)) + chalk.gray('│'));
51
+ console.log(chalk.gray('│') + chalk.gray(' Name: ') + chalk.white(safePadEnd(tunnel.subdomain || 'Unknown', 28)) + chalk.gray('│'));
52
+ console.log(chalk.gray('│') + chalk.gray(' Public URL: ') + chalk.cyan(safePadEnd(tunnel.publicUrl || 'Unknown', 28)) + chalk.gray('│'));
53
+ console.log(chalk.gray('│') + chalk.gray(' Local Port: ') + chalk.white(safePadEnd(tunnel.localPort || 'Unknown', 28)) + chalk.gray('│'));
54
+ console.log(chalk.gray('│') + chalk.gray(' Tunnel ID: ') + chalk.yellow(safePadEnd(tunnel.id || tunnel._id || 'Unknown', 28)) + chalk.gray('│'));
48
55
  console.log(chalk.gray('└─────────────────────────────────────────────┘\n'));
49
56
 
50
57
  // Connect tunnel client
51
58
  console.log(chalk.blue('Connecting tunnel client...\n'));
52
- await connectTunnelClient(tunnel._id, tunnel.subdomain, tunnel.localPort);
59
+ await connectTunnelClient(tunnel.id || tunnel._id, tunnel.subdomain, tunnel.localPort);
53
60
 
54
61
  } catch (error) {
55
62
  spinner.fail(chalk.red('Failed to create tunnel'));
@@ -60,12 +67,14 @@ async function start(port, options) {
60
67
 
61
68
  // Connect tunnel client
62
69
  async function connectTunnelClient(tunnelId, subdomain, localPort) {
63
- const tunnelServerUrl = config.get('tunnelServerUrl') || 'ws://localhost:9000';
70
+ const tunnelServerUrl = config.get('tunnelServerUrl') || 'ws://localhost:8080';
64
71
  const token = api.getToken();
65
72
  const userId = config.get('userId');
66
73
 
67
74
  const ws = new WebSocket(tunnelServerUrl);
68
75
 
76
+ let heartbeatInterval;
77
+
69
78
  ws.on('open', () => {
70
79
  console.log(chalk.green('✓ Connected to tunnel server'));
71
80
 
@@ -77,6 +86,13 @@ async function connectTunnelClient(tunnelId, subdomain, localPort) {
77
86
  authToken: token,
78
87
  userId
79
88
  }));
89
+
90
+ // Send heartbeat every 30 seconds to keep connection alive (industry standard)
91
+ heartbeatInterval = setInterval(() => {
92
+ if (ws.readyState === WebSocket.OPEN) {
93
+ ws.send(JSON.stringify({ type: 'heartbeat', tunnelId, subdomain }));
94
+ }
95
+ }, 30000);
80
96
  });
81
97
 
82
98
  ws.on('message', async (data) => {
@@ -90,23 +106,69 @@ async function connectTunnelClient(tunnelId, subdomain, localPort) {
90
106
  } else if (message.type === 'request') {
91
107
  const timestamp = new Date().toLocaleTimeString();
92
108
  console.log(chalk.gray(`[${timestamp}]`), chalk.blue(message.method), chalk.white(message.path));
109
+
110
+ // Forward request to local server
111
+ try {
112
+ const localUrl = `http://localhost:${localPort}${message.path}`;
113
+ const response = await axios({
114
+ method: message.method.toLowerCase(),
115
+ url: localUrl,
116
+ headers: message.headers || {},
117
+ data: message.body,
118
+ validateStatus: () => true // Accept any status code
119
+ });
120
+
121
+ // Clean up headers to avoid conflicts
122
+ const cleanHeaders = { ...response.headers };
123
+ delete cleanHeaders['transfer-encoding'];
124
+ delete cleanHeaders['content-length'];
125
+
126
+ // Send response back to tunnel server
127
+ ws.send(JSON.stringify({
128
+ type: 'response',
129
+ requestId: message.requestId,
130
+ statusCode: response.status,
131
+ headers: cleanHeaders,
132
+ body: response.data
133
+ }));
134
+ } catch (error) {
135
+ console.error(chalk.red(`Error forwarding request: ${error.message}`));
136
+ ws.send(JSON.stringify({
137
+ type: 'response',
138
+ requestId: message.requestId,
139
+ statusCode: 500,
140
+ headers: { 'Content-Type': 'application/json' },
141
+ body: { error: 'Tunnel client error', message: error.message }
142
+ }));
143
+ }
144
+ } else if (message.type === 'timeout') {
145
+ // Handle server-side timeout notifications
146
+ if (message.reason === 'idle') {
147
+ console.log(chalk.yellow('\n⏰ Tunnel closed due to 2 hours of inactivity.'));
148
+ } else if (message.reason === 'max_session') {
149
+ console.log(chalk.yellow('\n⏰ Tunnel session expired after 24 hours.'));
150
+ }
151
+ console.log(chalk.gray(message.message || 'Please reconnect to continue.'));
93
152
  } else if (message.type === 'error') {
94
153
  console.error(chalk.red(`Error: ${message.error}`));
95
154
  }
96
155
  });
97
156
 
98
157
  ws.on('close', () => {
158
+ if (heartbeatInterval) clearInterval(heartbeatInterval);
99
159
  console.log(chalk.yellow('\n⚠ Tunnel disconnected'));
100
160
  process.exit(0);
101
161
  });
102
162
 
103
163
  ws.on('error', (error) => {
164
+ if (heartbeatInterval) clearInterval(heartbeatInterval);
104
165
  console.error(chalk.red(`\n✗ Connection error: ${error.message}`));
105
166
  process.exit(1);
106
167
  });
107
168
 
108
169
  // Handle Ctrl+C
109
170
  process.on('SIGINT', () => {
171
+ if (heartbeatInterval) clearInterval(heartbeatInterval);
110
172
  console.log(chalk.yellow('\n\n⚠ Stopping tunnel...'));
111
173
  ws.close();
112
174
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-response-manager",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "Command-line interface for API Response Manager",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -8,7 +8,17 @@
8
8
  },
9
9
  "scripts": {
10
10
  "test": "jest",
11
- "link": "npm link"
11
+ "link": "npm link",
12
+ "prepublishOnly": "echo 'Publishing api-response-manager CLI...'"
13
+ },
14
+ "files": [
15
+ "bin/",
16
+ "commands/",
17
+ "utils/",
18
+ "README.md"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
12
22
  },
13
23
  "keywords": [
14
24
  "api",
@@ -19,7 +29,7 @@
19
29
  "developer-tools"
20
30
  ],
21
31
  "author": "Vijay Singh Purohit",
22
- "license": "MIT",
32
+ "license": "SEE LICENSE IN LICENSE",
23
33
  "dependencies": {
24
34
  "axios": "^1.6.2",
25
35
  "boxen": "^5.1.2",
@@ -47,5 +57,6 @@
47
57
  "bugs": {
48
58
  "url": "https://github.com/vijaypurohit322/api-response-manager/issues"
49
59
  },
50
- "homepage": "https://github.com/vijaypurohit322/api-response-manager#readme"
60
+ "homepage": "https://github.com/vijaypurohit322/api-response-manager#readme",
61
+ "preferGlobal": true
51
62
  }
package/utils/api.js CHANGED
@@ -4,7 +4,8 @@ const chalk = require('chalk');
4
4
 
5
5
  class APIClient {
6
6
  constructor() {
7
- this.baseURL = config.get('apiUrl') || 'http://localhost:5000/api';
7
+ // Check both API_URL and apiUrl for backwards compatibility
8
+ this.baseURL = config.get('API_URL') || config.get('apiUrl') || 'https://api.tunnelapi.in/api';
8
9
  this.client = axios.create({
9
10
  baseURL: this.baseURL,
10
11
  timeout: 30000,
@@ -185,6 +186,23 @@ class APIClient {
185
186
  const response = await this.client.post(`/tunnels/${id}/ingress`, data);
186
187
  return response.data;
187
188
  }
189
+
190
+ // IP Management
191
+ async updateIPWhitelist(id, ipWhitelist) {
192
+ const response = await this.client.put(`/tunnels/${id}/ip-whitelist`, { ipWhitelist });
193
+ return response.data;
194
+ }
195
+
196
+ async updateIPBlacklist(id, ipBlacklist) {
197
+ const response = await this.client.put(`/tunnels/${id}/ip-blacklist`, { ipBlacklist });
198
+ return response.data;
199
+ }
200
+
201
+ // Update Tunnel
202
+ async updateTunnel(id, updates) {
203
+ const response = await this.client.put(`/tunnels/${id}`, updates);
204
+ return response.data;
205
+ }
188
206
  }
189
207
 
190
208
  module.exports = new APIClient();
package/utils/config.js CHANGED
@@ -4,7 +4,8 @@ const path = require('path');
4
4
  const config = new Conf({
5
5
  projectName: 'arm-cli',
6
6
  defaults: {
7
- apiUrl: 'http://localhost:5000/api',
7
+ apiUrl: 'https://api.tunnelapi.in/api',
8
+ API_URL: 'https://api.tunnelapi.in/api', // Support both camelCase and UPPER_CASE
8
9
  token: null,
9
10
  userId: null,
10
11
  email: null,
@@ -16,6 +17,10 @@ const config = new Conf({
16
17
  type: 'string',
17
18
  format: 'uri'
18
19
  },
20
+ API_URL: {
21
+ type: 'string',
22
+ format: 'uri'
23
+ },
19
24
  token: {
20
25
  type: ['string', 'null']
21
26
  },
package/CLI_UPDATES.md DELETED
@@ -1,196 +0,0 @@
1
- # CLI Updates for Advanced Tunnel Features
2
-
3
- ## ✅ Changes Made
4
-
5
- ### 1. Enhanced `tunnel` Command
6
- Added new options to the main tunnel command:
7
- ```bash
8
- arm tunnel [port] [options]
9
-
10
- Options:
11
- -s, --subdomain <subdomain> Custom subdomain
12
- -n, --name <name> Tunnel name
13
- -a, --auth Enable basic authentication
14
- -r, --rate-limit <limit> Rate limit (requests per minute)
15
- -p, --protocol <protocol> Protocol (http, https, tcp, ws, wss)
16
- --ssl Enable SSL/HTTPS
17
- -d, --domain <domain> Custom domain
18
- ```
19
-
20
- ### 2. New Commands Added
21
-
22
- #### Custom Domain
23
- ```bash
24
- arm tunnel:domain <tunnelId> <domain>
25
- ```
26
- Example:
27
- ```bash
28
- arm tunnel:domain abc123 api.yourdomain.com
29
- ```
30
-
31
- #### SSL Certificate Upload
32
- ```bash
33
- arm tunnel:ssl <tunnelId> --cert <path> --key <path> [--ca <path>]
34
- ```
35
- Example:
36
- ```bash
37
- arm tunnel:ssl abc123 --cert cert.pem --key key.pem --ca ca.pem
38
- ```
39
-
40
- #### OAuth Configuration
41
- ```bash
42
- arm tunnel:auth:oauth <tunnelId> [options]
43
-
44
- Options:
45
- --provider <provider> OAuth provider (google, github, microsoft, custom)
46
- --client-id <id> OAuth client ID
47
- --client-secret <secret> OAuth client secret
48
- --callback-url <url> OAuth callback URL
49
- --scope <scope> OAuth scope (comma-separated)
50
- ```
51
- Example:
52
- ```bash
53
- arm tunnel:auth:oauth abc123 \
54
- --provider google \
55
- --client-id YOUR_CLIENT_ID \
56
- --client-secret YOUR_SECRET \
57
- --callback-url https://yourtunnel.arm.dev/auth/callback \
58
- --scope openid,email,profile
59
- ```
60
-
61
- #### OIDC Configuration
62
- ```bash
63
- arm tunnel:auth:oidc <tunnelId> [options]
64
-
65
- Options:
66
- --issuer <url> OIDC issuer URL
67
- --client-id <id> OIDC client ID
68
- --client-secret <secret> OIDC client secret
69
- --callback-url <url> OIDC callback URL
70
- ```
71
- Example:
72
- ```bash
73
- arm tunnel:auth:oidc abc123 \
74
- --issuer https://accounts.google.com \
75
- --client-id YOUR_CLIENT_ID \
76
- --client-secret YOUR_SECRET \
77
- --callback-url https://yourtunnel.arm.dev/auth/callback
78
- ```
79
-
80
- #### SAML Configuration
81
- ```bash
82
- arm tunnel:auth:saml <tunnelId> [options]
83
-
84
- Options:
85
- --entry-point <url> SAML entry point URL
86
- --issuer <issuer> SAML issuer
87
- --cert <path> Path to IdP certificate file
88
- --callback-url <url> SAML callback URL
89
- ```
90
- Example:
91
- ```bash
92
- arm tunnel:auth:saml abc123 \
93
- --entry-point https://idp.example.com/saml/sso \
94
- --issuer https://yourtunnel.arm.dev \
95
- --cert idp-cert.pem \
96
- --callback-url https://yourtunnel.arm.dev/auth/saml/callback
97
- ```
98
-
99
- #### Ingress Configuration
100
- ```bash
101
- arm tunnel:ingress <tunnelId> <rules> [--tls]
102
-
103
- Rules format: "/path=host:port,/path2=host:port"
104
- ```
105
- Example:
106
- ```bash
107
- arm tunnel:ingress abc123 "/api=localhost:3000,/admin=localhost:4000" --tls
108
- ```
109
-
110
- ## 📝 Usage Examples
111
-
112
- ### Example 1: Create HTTPS Tunnel
113
- ```bash
114
- arm tunnel 3000 --protocol https --ssl --subdomain myapi
115
- ```
116
-
117
- ### Example 2: Create TCP Tunnel
118
- ```bash
119
- arm tunnel 5432 --protocol tcp --subdomain postgres
120
- ```
121
-
122
- ### Example 3: Create Tunnel with Custom Domain
123
- ```bash
124
- # Create tunnel
125
- arm tunnel 3000 --protocol https --ssl
126
-
127
- # Set custom domain
128
- arm tunnel:domain <tunnel-id> api.mycompany.com
129
- ```
130
-
131
- ### Example 4: Create Tunnel with OAuth
132
- ```bash
133
- # Create tunnel
134
- arm tunnel 3000 --protocol https --ssl --subdomain myapi
135
-
136
- # Configure OAuth
137
- arm tunnel:auth:oauth <tunnel-id> \
138
- --provider google \
139
- --client-id YOUR_CLIENT_ID \
140
- --client-secret YOUR_SECRET \
141
- --callback-url https://myapi.tunnel.arm.dev/auth/callback
142
- ```
143
-
144
- ### Example 5: Create Tunnel with Ingress
145
- ```bash
146
- # Create tunnel
147
- arm tunnel 3000 --protocol https --ssl
148
-
149
- # Configure ingress rules
150
- arm tunnel:ingress <tunnel-id> \
151
- "/v1=localhost:3000,/v2=localhost:4000" \
152
- --tls
153
- ```
154
-
155
- ## 🔧 Updated Files
156
-
157
- 1. **cli/bin/arm.js** - Added new commands
158
- 2. **cli/commands/tunnel.js** - Added new functions
159
- 3. **cli/utils/api.js** - Added new API methods
160
-
161
- ## 📦 No New Dependencies Required
162
-
163
- All new features use existing dependencies.
164
-
165
- ## 🚀 Testing
166
-
167
- Test the new commands:
168
- ```bash
169
- # Test HTTPS tunnel
170
- arm tunnel 3000 --protocol https --ssl
171
-
172
- # Test custom domain
173
- arm tunnel:domain <tunnel-id> test.example.com
174
-
175
- # Test SSL upload
176
- arm tunnel:ssl <tunnel-id> --cert cert.pem --key key.pem
177
-
178
- # Test OAuth
179
- arm tunnel:auth:oauth <tunnel-id> --provider google --client-id test --client-secret test --callback-url http://localhost/callback
180
-
181
- # Test ingress
182
- arm tunnel:ingress <tunnel-id> "/api=localhost:3000" --tls
183
- ```
184
-
185
- ## 📖 Help Output
186
-
187
- ```bash
188
- arm --help
189
- arm tunnel --help
190
- arm tunnel:domain --help
191
- arm tunnel:ssl --help
192
- arm tunnel:auth:oauth --help
193
- arm tunnel:auth:oidc --help
194
- arm tunnel:auth:saml --help
195
- arm tunnel:ingress --help
196
- ```