api-response-manager 2.3.1 → 2.5.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.
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
@@ -21,6 +21,9 @@ const ipWhitelistCommand = require('../commands/ipWhitelist');
21
21
  const ipBlacklistCommand = require('../commands/ipBlacklist');
22
22
  const rateLimitCommand = require('../commands/rateLimit');
23
23
  const healthCommand = require('../commands/health');
24
+ const gatewayCommand = require('../commands/gateway');
25
+ const ingressCommand = require('../commands/ingress');
26
+ const accountCommand = require('../commands/account');
24
27
 
25
28
  // CLI setup
26
29
  program
@@ -313,6 +316,128 @@ program
313
316
  .argument('<key>', 'Configuration key')
314
317
  .action(configCommand.delete);
315
318
 
319
+ // Gateway commands (paid feature)
320
+ program
321
+ .command('gateway:list')
322
+ .description('List all API gateways')
323
+ .action(gatewayCommand.list);
324
+
325
+ program
326
+ .command('gateway:create')
327
+ .description('Create a new API gateway')
328
+ .argument('<name>', 'Gateway name')
329
+ .argument('<subdomain>', 'Subdomain for the gateway')
330
+ .option('-d, --description <description>', 'Gateway description')
331
+ .action(gatewayCommand.create);
332
+
333
+ program
334
+ .command('gateway:get')
335
+ .description('Get gateway details')
336
+ .argument('<gatewayId>', 'Gateway ID')
337
+ .action(gatewayCommand.get);
338
+
339
+ program
340
+ .command('gateway:delete')
341
+ .description('Delete a gateway')
342
+ .argument('<gatewayId>', 'Gateway ID')
343
+ .action(gatewayCommand.remove);
344
+
345
+ program
346
+ .command('gateway:route:add')
347
+ .description('Add route to gateway')
348
+ .argument('<gatewayId>', 'Gateway ID')
349
+ .argument('<path>', 'Route path (e.g., /api/v1)')
350
+ .argument('<backendUrl>', 'Backend URL')
351
+ .option('-n, --name <name>', 'Route name')
352
+ .option('-p, --protocol <protocol>', 'Protocol (http, websocket, kafka, mqtt)', 'http')
353
+ .option('-t, --pathType <type>', 'Path type (prefix, exact, regex)', 'prefix')
354
+ .option('-m, --methods <methods>', 'HTTP methods (comma-separated)', 'GET,POST,PUT,DELETE')
355
+ .option('--brokers <brokers>', 'Kafka brokers (comma-separated)')
356
+ .option('--broker <broker>', 'MQTT broker URL')
357
+ .option('--topic <topic>', 'Kafka/MQTT topic')
358
+ .action(gatewayCommand.addRoute);
359
+
360
+ program
361
+ .command('gateway:route:remove')
362
+ .description('Remove route from gateway')
363
+ .argument('<gatewayId>', 'Gateway ID')
364
+ .argument('<routeId>', 'Route ID')
365
+ .action(gatewayCommand.removeRoute);
366
+
367
+ program
368
+ .command('gateway:analytics')
369
+ .description('View gateway analytics')
370
+ .argument('<gatewayId>', 'Gateway ID')
371
+ .option('-p, --period <period>', 'Time period (1h, 24h, 7d, 30d)', '24h')
372
+ .action(gatewayCommand.analytics);
373
+
374
+ // Ingress commands (paid feature)
375
+ program
376
+ .command('ingress:list')
377
+ .description('List all ingress rules')
378
+ .action(ingressCommand.list);
379
+
380
+ program
381
+ .command('ingress:create')
382
+ .description('Create ingress rule')
383
+ .argument('<name>', 'Ingress name')
384
+ .option('-n, --namespace <namespace>', 'Namespace', 'default')
385
+ .option('-h, --host <host>', 'Host')
386
+ .option('-p, --path <path>', 'Path', '/')
387
+ .option('-t, --pathType <type>', 'Path type (Prefix, Exact)', 'Prefix')
388
+ .option('-b, --backend <url>', 'Backend URL')
389
+ .action(ingressCommand.create);
390
+
391
+ program
392
+ .command('ingress:apply')
393
+ .description('Create ingress from YAML file')
394
+ .argument('<yamlFile>', 'Path to YAML file')
395
+ .action(ingressCommand.createFromYaml);
396
+
397
+ program
398
+ .command('ingress:get')
399
+ .description('Get ingress details')
400
+ .argument('<name>', 'Ingress name')
401
+ .option('-n, --namespace <namespace>', 'Namespace', 'default')
402
+ .action(ingressCommand.get);
403
+
404
+ program
405
+ .command('ingress:delete')
406
+ .description('Delete ingress rule')
407
+ .argument('<name>', 'Ingress name')
408
+ .option('-n, --namespace <namespace>', 'Namespace', 'default')
409
+ .action(ingressCommand.remove);
410
+
411
+ program
412
+ .command('ingress:export')
413
+ .description('Export ingress as YAML')
414
+ .argument('<name>', 'Ingress name')
415
+ .option('-n, --namespace <namespace>', 'Namespace', 'default')
416
+ .option('-o, --output <file>', 'Output file path')
417
+ .action(ingressCommand.exportYaml);
418
+
419
+ program
420
+ .command('ingress:validate')
421
+ .description('Validate ingress YAML')
422
+ .argument('<yamlFile>', 'Path to YAML file')
423
+ .action(ingressCommand.validate);
424
+
425
+ // Account commands
426
+ program
427
+ .command('account')
428
+ .description('View account information')
429
+ .action(accountCommand.info);
430
+
431
+ program
432
+ .command('account:features')
433
+ .description('View available features for your plan')
434
+ .action(accountCommand.features);
435
+
436
+ program
437
+ .command('account:usage')
438
+ .description('View usage statistics')
439
+ .action(accountCommand.usage);
440
+
316
441
  // Parse arguments
317
442
  program.parse(process.argv);
318
443
 
@@ -0,0 +1,189 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const Table = require('cli-table3');
4
+ const { getApiClient, requireAuth } = require('../utils/api');
5
+
6
+ /**
7
+ * Get current user info
8
+ */
9
+ async function info() {
10
+ const spinner = ora('Fetching account info...').start();
11
+
12
+ try {
13
+ const api = await getApiClient();
14
+ await requireAuth(api);
15
+
16
+ const response = await api.get('/auth/me');
17
+ const user = response.data.user;
18
+
19
+ spinner.stop();
20
+
21
+ console.log('\n' + chalk.green('Account Information:'));
22
+ console.log(chalk.gray('─'.repeat(50)));
23
+ console.log(` ${chalk.bold('Name:')} ${user.name || '-'}`);
24
+ console.log(` ${chalk.bold('Email:')} ${user.email}`);
25
+ console.log(` ${chalk.bold('Role:')} ${user.role}`);
26
+ console.log(` ${chalk.bold('Tier:')} ${getTierBadge(user.tier)}`);
27
+ console.log(` ${chalk.bold('Verified:')} ${user.emailVerified ? chalk.green('Yes') : chalk.yellow('No')}`);
28
+
29
+ if (user.subscription) {
30
+ console.log('\n' + chalk.cyan('Subscription:'));
31
+ console.log(` ${chalk.bold('Status:')} ${getStatusBadge(user.subscription.status)}`);
32
+ if (user.subscription.endDate) {
33
+ console.log(` ${chalk.bold('Expires:')} ${new Date(user.subscription.endDate).toLocaleDateString()}`);
34
+ }
35
+ }
36
+
37
+ if (user.usage) {
38
+ console.log('\n' + chalk.cyan('Usage This Month:'));
39
+ console.log(` ${chalk.bold('HTTP Requests:')} ${user.usage.httpRequests || 0}`);
40
+ console.log(` ${chalk.bold('Bandwidth:')} ${formatBytes(user.usage.bandwidth || 0)}`);
41
+ }
42
+ } catch (error) {
43
+ spinner.fail('Failed to fetch account info');
44
+ console.error(chalk.red(error.response?.data?.error || error.message));
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Get user's feature access
50
+ */
51
+ async function features() {
52
+ const spinner = ora('Fetching feature access...').start();
53
+
54
+ try {
55
+ const api = await getApiClient();
56
+ await requireAuth(api);
57
+
58
+ const response = await api.get('/auth/features');
59
+ const { tier, features } = response.data;
60
+
61
+ spinner.stop();
62
+
63
+ console.log('\n' + chalk.green(`Feature Access (${getTierBadge(tier)} Plan):`));
64
+ console.log(chalk.gray('─'.repeat(60)));
65
+
66
+ // Group by category
67
+ const categories = {};
68
+ Object.entries(features).forEach(([key, feature]) => {
69
+ const cat = feature.category || 'other';
70
+ if (!categories[cat]) categories[cat] = [];
71
+ categories[cat].push({ key, ...feature });
72
+ });
73
+
74
+ Object.entries(categories).forEach(([category, featureList]) => {
75
+ console.log('\n' + chalk.cyan(category.toUpperCase()));
76
+
77
+ const table = new Table({
78
+ head: [chalk.cyan('Feature'), chalk.cyan('Status'), chalk.cyan('Limit')],
79
+ colWidths: [30, 12, 20]
80
+ });
81
+
82
+ featureList.forEach(f => {
83
+ const status = f.enabled ? chalk.green('✓ Enabled') : chalk.red('✗ Disabled');
84
+ const limit = f.limit === null ? 'Unlimited' : (f.limit || '0');
85
+ table.push([f.name, status, limit]);
86
+ });
87
+
88
+ console.log(table.toString());
89
+ });
90
+
91
+ console.log('\n' + chalk.gray('Upgrade your plan at https://tunnelapi.in/pricing'));
92
+ } catch (error) {
93
+ spinner.fail('Failed to fetch features');
94
+ console.error(chalk.red(error.response?.data?.error || error.message));
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Get usage statistics
100
+ */
101
+ async function usage() {
102
+ const spinner = ora('Fetching usage stats...').start();
103
+
104
+ try {
105
+ const api = await getApiClient();
106
+ await requireAuth(api);
107
+
108
+ const [userRes, featuresRes] = await Promise.all([
109
+ api.get('/auth/me'),
110
+ api.get('/auth/features')
111
+ ]);
112
+
113
+ const user = userRes.data.user;
114
+ const { tier, features } = featuresRes.data;
115
+
116
+ spinner.stop();
117
+
118
+ console.log('\n' + chalk.green('Usage Statistics:'));
119
+ console.log(chalk.gray('─'.repeat(50)));
120
+
121
+ // HTTP Requests
122
+ const httpLimit = features.http_requests?.limit;
123
+ const httpUsed = user.usage?.httpRequests || 0;
124
+ console.log(`\n${chalk.bold('HTTP Requests:')}`);
125
+ console.log(` Used: ${httpUsed} / ${httpLimit || 'Unlimited'}`);
126
+ if (httpLimit) {
127
+ const percent = Math.round((httpUsed / httpLimit) * 100);
128
+ console.log(` ${getProgressBar(percent)} ${percent}%`);
129
+ }
130
+
131
+ // Bandwidth
132
+ const bwLimit = features.bandwidth?.limit;
133
+ const bwUsed = user.usage?.bandwidth || 0;
134
+ console.log(`\n${chalk.bold('Bandwidth:')}`);
135
+ console.log(` Used: ${formatBytes(bwUsed)} / ${bwLimit ? formatBytes(bwLimit * 1024 * 1024) : 'Unlimited'}`);
136
+ if (bwLimit) {
137
+ const bwLimitBytes = bwLimit * 1024 * 1024;
138
+ const percent = Math.round((bwUsed / bwLimitBytes) * 100);
139
+ console.log(` ${getProgressBar(percent)} ${percent}%`);
140
+ }
141
+
142
+ console.log('\n' + chalk.gray(`Last reset: ${new Date(user.usage?.lastResetAt || Date.now()).toLocaleDateString()}`));
143
+ } catch (error) {
144
+ spinner.fail('Failed to fetch usage');
145
+ console.error(chalk.red(error.response?.data?.error || error.message));
146
+ }
147
+ }
148
+
149
+ // Helper functions
150
+ function getTierBadge(tier) {
151
+ const badges = {
152
+ free: chalk.gray('Free'),
153
+ solo: chalk.blue('Solo'),
154
+ team: chalk.magenta('Team'),
155
+ enterprise: chalk.yellow('Enterprise')
156
+ };
157
+ return badges[tier] || tier;
158
+ }
159
+
160
+ function getStatusBadge(status) {
161
+ const badges = {
162
+ active: chalk.green('Active'),
163
+ trial: chalk.blue('Trial'),
164
+ cancelled: chalk.yellow('Cancelled'),
165
+ expired: chalk.red('Expired')
166
+ };
167
+ return badges[status] || status;
168
+ }
169
+
170
+ function formatBytes(bytes) {
171
+ if (bytes === 0) return '0 B';
172
+ const k = 1024;
173
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
174
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
175
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
176
+ }
177
+
178
+ function getProgressBar(percent) {
179
+ const filled = Math.round(percent / 5);
180
+ const empty = 20 - filled;
181
+ const color = percent > 80 ? chalk.red : percent > 50 ? chalk.yellow : chalk.green;
182
+ return color('█'.repeat(filled) + '░'.repeat(empty));
183
+ }
184
+
185
+ module.exports = {
186
+ info,
187
+ features,
188
+ usage
189
+ };
@@ -0,0 +1,275 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const Table = require('cli-table3');
4
+ const { getApiClient, requireAuth } = require('../utils/api');
5
+
6
+ /**
7
+ * List all gateways
8
+ */
9
+ async function list() {
10
+ const spinner = ora('Fetching gateways...').start();
11
+
12
+ try {
13
+ const api = await getApiClient();
14
+ await requireAuth(api);
15
+
16
+ const response = await api.get('/gateways');
17
+ const gateways = response.data.gateways || [];
18
+
19
+ spinner.stop();
20
+
21
+ if (gateways.length === 0) {
22
+ console.log(chalk.yellow('\nNo gateways found. Create one with: arm gateway:create <name> <subdomain>'));
23
+ return;
24
+ }
25
+
26
+ const table = new Table({
27
+ head: [
28
+ chalk.cyan('Name'),
29
+ chalk.cyan('Subdomain'),
30
+ chalk.cyan('Status'),
31
+ chalk.cyan('Routes'),
32
+ chalk.cyan('Public URL')
33
+ ],
34
+ colWidths: [20, 15, 10, 8, 45]
35
+ });
36
+
37
+ gateways.forEach(gw => {
38
+ table.push([
39
+ gw.name,
40
+ gw.subdomain,
41
+ gw.status === 'active' ? chalk.green('active') : chalk.red(gw.status),
42
+ gw.routes?.length || 0,
43
+ chalk.blue(gw.publicUrl || '-')
44
+ ]);
45
+ });
46
+
47
+ console.log('\n' + table.toString());
48
+ console.log(chalk.gray(`\nTotal: ${gateways.length} gateway(s)`));
49
+ } catch (error) {
50
+ spinner.fail('Failed to fetch gateways');
51
+ if (error.response?.status === 403) {
52
+ console.log(chalk.red('\n⚠ API Gateway is a paid feature. Upgrade your plan to access it.'));
53
+ } else {
54
+ console.error(chalk.red(error.response?.data?.error || error.message));
55
+ }
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Create a new gateway
61
+ */
62
+ async function create(name, subdomain, options) {
63
+ const spinner = ora('Creating gateway...').start();
64
+
65
+ try {
66
+ const api = await getApiClient();
67
+ await requireAuth(api);
68
+
69
+ const response = await api.post('/gateways', {
70
+ name,
71
+ subdomain,
72
+ description: options.description || ''
73
+ });
74
+
75
+ spinner.succeed('Gateway created successfully!');
76
+
77
+ const gw = response.data.gateway;
78
+ console.log('\n' + chalk.green('Gateway Details:'));
79
+ console.log(chalk.gray('─'.repeat(50)));
80
+ console.log(` ${chalk.bold('Name:')} ${gw.name}`);
81
+ console.log(` ${chalk.bold('Subdomain:')} ${gw.subdomain}`);
82
+ console.log(` ${chalk.bold('Public URL:')} ${chalk.blue(gw.publicUrl)}`);
83
+ console.log(` ${chalk.bold('Status:')} ${chalk.green(gw.status)}`);
84
+ console.log('\n' + chalk.gray('Add routes with: arm gateway:route:add <gateway-id> <path> <backend-url>'));
85
+ } catch (error) {
86
+ spinner.fail('Failed to create gateway');
87
+ if (error.response?.status === 403) {
88
+ console.log(chalk.red('\n⚠ API Gateway is a paid feature. Upgrade your plan to access it.'));
89
+ console.log(chalk.yellow('Visit https://tunnelapi.in/pricing to upgrade.'));
90
+ } else {
91
+ console.error(chalk.red(error.response?.data?.error || error.message));
92
+ }
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Get gateway details
98
+ */
99
+ async function get(gatewayId) {
100
+ const spinner = ora('Fetching gateway...').start();
101
+
102
+ try {
103
+ const api = await getApiClient();
104
+ await requireAuth(api);
105
+
106
+ const response = await api.get(`/gateways/${gatewayId}`);
107
+ const gw = response.data.gateway;
108
+
109
+ spinner.stop();
110
+
111
+ console.log('\n' + chalk.green('Gateway Details:'));
112
+ console.log(chalk.gray('─'.repeat(50)));
113
+ console.log(` ${chalk.bold('ID:')} ${gw._id}`);
114
+ console.log(` ${chalk.bold('Name:')} ${gw.name}`);
115
+ console.log(` ${chalk.bold('Subdomain:')} ${gw.subdomain}`);
116
+ console.log(` ${chalk.bold('Public URL:')} ${chalk.blue(gw.publicUrl)}`);
117
+ console.log(` ${chalk.bold('Status:')} ${gw.status === 'active' ? chalk.green('active') : chalk.red(gw.status)}`);
118
+
119
+ if (gw.routes && gw.routes.length > 0) {
120
+ console.log('\n' + chalk.cyan('Routes:'));
121
+ const table = new Table({
122
+ head: [chalk.cyan('Path'), chalk.cyan('Protocol'), chalk.cyan('Backend'), chalk.cyan('Methods')],
123
+ colWidths: [20, 12, 35, 25]
124
+ });
125
+
126
+ gw.routes.forEach(route => {
127
+ const backend = route.backends?.[0]?.url ||
128
+ (route.kafka?.brokers?.join(',')) ||
129
+ (route.mqtt?.broker) || '-';
130
+ table.push([
131
+ route.path,
132
+ route.protocol || 'http',
133
+ backend,
134
+ (route.methods || ['ALL']).join(', ')
135
+ ]);
136
+ });
137
+
138
+ console.log(table.toString());
139
+ } else {
140
+ console.log(chalk.yellow('\nNo routes configured.'));
141
+ }
142
+ } catch (error) {
143
+ spinner.fail('Failed to fetch gateway');
144
+ console.error(chalk.red(error.response?.data?.error || error.message));
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Delete a gateway
150
+ */
151
+ async function remove(gatewayId) {
152
+ const spinner = ora('Deleting gateway...').start();
153
+
154
+ try {
155
+ const api = await getApiClient();
156
+ await requireAuth(api);
157
+
158
+ await api.delete(`/gateways/${gatewayId}`);
159
+
160
+ spinner.succeed('Gateway deleted successfully!');
161
+ } catch (error) {
162
+ spinner.fail('Failed to delete gateway');
163
+ console.error(chalk.red(error.response?.data?.error || error.message));
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Add route to gateway
169
+ */
170
+ async function addRoute(gatewayId, path, backendUrl, options) {
171
+ const spinner = ora('Adding route...').start();
172
+
173
+ try {
174
+ const api = await getApiClient();
175
+ await requireAuth(api);
176
+
177
+ const routeData = {
178
+ name: options.name || `Route ${path}`,
179
+ protocol: options.protocol || 'http',
180
+ path,
181
+ pathType: options.pathType || 'prefix',
182
+ methods: options.methods ? options.methods.split(',') : ['GET', 'POST', 'PUT', 'DELETE'],
183
+ backends: [{ url: backendUrl, weight: 100 }]
184
+ };
185
+
186
+ // Add protocol-specific config
187
+ if (options.protocol === 'kafka') {
188
+ routeData.kafka = {
189
+ brokers: options.brokers ? options.brokers.split(',') : [],
190
+ topic: options.topic || ''
191
+ };
192
+ } else if (options.protocol === 'mqtt') {
193
+ routeData.mqtt = {
194
+ broker: options.broker || '',
195
+ topic: options.topic || ''
196
+ };
197
+ }
198
+
199
+ const response = await api.post(`/gateways/${gatewayId}/routes`, routeData);
200
+
201
+ spinner.succeed('Route added successfully!');
202
+ console.log(chalk.gray(`Path: ${path} → ${backendUrl}`));
203
+ } catch (error) {
204
+ spinner.fail('Failed to add route');
205
+ if (error.response?.status === 403) {
206
+ const msg = error.response?.data?.message || '';
207
+ if (msg.includes('protocol')) {
208
+ console.log(chalk.red('\n⚠ This protocol is not available on your plan.'));
209
+ console.log(chalk.yellow('Upgrade to Team or Enterprise for Kafka/MQTT support.'));
210
+ } else {
211
+ console.log(chalk.red('\n⚠ ' + msg));
212
+ }
213
+ } else {
214
+ console.error(chalk.red(error.response?.data?.error || error.message));
215
+ }
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Remove route from gateway
221
+ */
222
+ async function removeRoute(gatewayId, routeId) {
223
+ const spinner = ora('Removing route...').start();
224
+
225
+ try {
226
+ const api = await getApiClient();
227
+ await requireAuth(api);
228
+
229
+ await api.delete(`/gateways/${gatewayId}/routes/${routeId}`);
230
+
231
+ spinner.succeed('Route removed successfully!');
232
+ } catch (error) {
233
+ spinner.fail('Failed to remove route');
234
+ console.error(chalk.red(error.response?.data?.error || error.message));
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Get gateway analytics
240
+ */
241
+ async function analytics(gatewayId, options) {
242
+ const spinner = ora('Fetching analytics...').start();
243
+
244
+ try {
245
+ const api = await getApiClient();
246
+ await requireAuth(api);
247
+
248
+ const response = await api.get(`/gateways/${gatewayId}/analytics`, {
249
+ params: { period: options.period || '24h' }
250
+ });
251
+
252
+ spinner.stop();
253
+
254
+ const stats = response.data;
255
+ console.log('\n' + chalk.green('Gateway Analytics:'));
256
+ console.log(chalk.gray('─'.repeat(50)));
257
+ console.log(` ${chalk.bold('Total Requests:')} ${stats.totalRequests || 0}`);
258
+ console.log(` ${chalk.bold('Success Rate:')} ${stats.successRate || 0}%`);
259
+ console.log(` ${chalk.bold('Avg Latency:')} ${stats.avgLatency || 0}ms`);
260
+ console.log(` ${chalk.bold('Errors:')} ${stats.errors || 0}`);
261
+ } catch (error) {
262
+ spinner.fail('Failed to fetch analytics');
263
+ console.error(chalk.red(error.response?.data?.error || error.message));
264
+ }
265
+ }
266
+
267
+ module.exports = {
268
+ list,
269
+ create,
270
+ get,
271
+ remove,
272
+ addRoute,
273
+ removeRoute,
274
+ analytics
275
+ };
@@ -0,0 +1,275 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const Table = require('cli-table3');
4
+ const fs = require('fs');
5
+ const { getApiClient, requireAuth } = require('../utils/api');
6
+
7
+ /**
8
+ * List all ingress rules
9
+ */
10
+ async function list() {
11
+ const spinner = ora('Fetching ingress rules...').start();
12
+
13
+ try {
14
+ const api = await getApiClient();
15
+ await requireAuth(api);
16
+
17
+ const response = await api.get('/ingress');
18
+ const rules = response.data.ingresses || [];
19
+
20
+ spinner.stop();
21
+
22
+ if (rules.length === 0) {
23
+ console.log(chalk.yellow('\nNo ingress rules found. Create one with: arm ingress:create <name>'));
24
+ return;
25
+ }
26
+
27
+ const table = new Table({
28
+ head: [
29
+ chalk.cyan('Name'),
30
+ chalk.cyan('Namespace'),
31
+ chalk.cyan('Host'),
32
+ chalk.cyan('Paths'),
33
+ chalk.cyan('Status')
34
+ ],
35
+ colWidths: [20, 15, 40, 8, 10]
36
+ });
37
+
38
+ rules.forEach(rule => {
39
+ const pathCount = rule.spec?.rules?.[0]?.http?.paths?.length || 0;
40
+ table.push([
41
+ rule.metadata?.name || '-',
42
+ rule.metadata?.namespace || 'default',
43
+ rule.spec?.rules?.[0]?.host || '-',
44
+ pathCount,
45
+ rule.status === 'active' ? chalk.green('active') : chalk.yellow(rule.status || 'pending')
46
+ ]);
47
+ });
48
+
49
+ console.log('\n' + table.toString());
50
+ console.log(chalk.gray(`\nTotal: ${rules.length} ingress rule(s)`));
51
+ } catch (error) {
52
+ spinner.fail('Failed to fetch ingress rules');
53
+ if (error.response?.status === 403) {
54
+ console.log(chalk.red('\n⚠ Kubernetes Ingress is a paid feature. Upgrade your plan to access it.'));
55
+ } else {
56
+ console.error(chalk.red(error.response?.data?.error || error.message));
57
+ }
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Create ingress from YAML file
63
+ */
64
+ async function createFromYaml(yamlFile) {
65
+ const spinner = ora('Creating ingress from YAML...').start();
66
+
67
+ try {
68
+ if (!fs.existsSync(yamlFile)) {
69
+ spinner.fail('YAML file not found');
70
+ console.error(chalk.red(`File not found: ${yamlFile}`));
71
+ return;
72
+ }
73
+
74
+ const yamlContent = fs.readFileSync(yamlFile, 'utf8');
75
+
76
+ const api = await getApiClient();
77
+ await requireAuth(api);
78
+
79
+ const response = await api.post('/ingress/yaml', { yaml: yamlContent });
80
+
81
+ spinner.succeed('Ingress created successfully!');
82
+
83
+ const ingress = response.data.ingress;
84
+ console.log('\n' + chalk.green('Ingress Details:'));
85
+ console.log(chalk.gray('─'.repeat(50)));
86
+ console.log(` ${chalk.bold('Name:')} ${ingress.metadata?.name}`);
87
+ console.log(` ${chalk.bold('Namespace:')} ${ingress.metadata?.namespace || 'default'}`);
88
+ console.log(` ${chalk.bold('Host:')} ${ingress.spec?.rules?.[0]?.host || '-'}`);
89
+ } catch (error) {
90
+ spinner.fail('Failed to create ingress');
91
+ if (error.response?.status === 403) {
92
+ console.log(chalk.red('\n⚠ Kubernetes Ingress is a paid feature. Upgrade your plan to access it.'));
93
+ console.log(chalk.yellow('Visit https://tunnelapi.in/pricing to upgrade.'));
94
+ } else {
95
+ console.error(chalk.red(error.response?.data?.error || error.message));
96
+ }
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Create ingress interactively
102
+ */
103
+ async function create(name, options) {
104
+ const spinner = ora('Creating ingress...').start();
105
+
106
+ try {
107
+ const api = await getApiClient();
108
+ await requireAuth(api);
109
+
110
+ const ingressData = {
111
+ name,
112
+ namespace: options.namespace || 'default',
113
+ host: options.host,
114
+ paths: [{
115
+ path: options.path || '/',
116
+ pathType: options.pathType || 'Prefix',
117
+ backend: {
118
+ url: options.backend
119
+ }
120
+ }]
121
+ };
122
+
123
+ const response = await api.post('/ingress', ingressData);
124
+
125
+ spinner.succeed('Ingress created successfully!');
126
+
127
+ const ingress = response.data.ingress;
128
+ console.log('\n' + chalk.green('Ingress Details:'));
129
+ console.log(chalk.gray('─'.repeat(50)));
130
+ console.log(` ${chalk.bold('Name:')} ${ingress.metadata?.name}`);
131
+ console.log(` ${chalk.bold('Namespace:')} ${ingress.metadata?.namespace || 'default'}`);
132
+ console.log(` ${chalk.bold('Host:')} ${ingress.spec?.rules?.[0]?.host || '-'}`);
133
+ console.log(` ${chalk.bold('URL:')} ${chalk.blue(ingress.publicUrl || '-')}`);
134
+ } catch (error) {
135
+ spinner.fail('Failed to create ingress');
136
+ if (error.response?.status === 403) {
137
+ console.log(chalk.red('\n⚠ Kubernetes Ingress is a paid feature. Upgrade your plan to access it.'));
138
+ console.log(chalk.yellow('Visit https://tunnelapi.in/pricing to upgrade.'));
139
+ } else {
140
+ console.error(chalk.red(error.response?.data?.error || error.message));
141
+ }
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Get ingress details
147
+ */
148
+ async function get(name, options) {
149
+ const spinner = ora('Fetching ingress...').start();
150
+
151
+ try {
152
+ const api = await getApiClient();
153
+ await requireAuth(api);
154
+
155
+ const namespace = options.namespace || 'default';
156
+ const response = await api.get(`/ingress/${namespace}/${name}`);
157
+ const ingress = response.data.ingress;
158
+
159
+ spinner.stop();
160
+
161
+ console.log('\n' + chalk.green('Ingress Details:'));
162
+ console.log(chalk.gray('─'.repeat(50)));
163
+ console.log(` ${chalk.bold('Name:')} ${ingress.metadata?.name}`);
164
+ console.log(` ${chalk.bold('Namespace:')} ${ingress.metadata?.namespace || 'default'}`);
165
+
166
+ const rules = ingress.spec?.rules || [];
167
+ if (rules.length > 0) {
168
+ console.log('\n' + chalk.cyan('Rules:'));
169
+ rules.forEach((rule, i) => {
170
+ console.log(` ${chalk.bold(`Rule ${i + 1}:`)} ${rule.host || '*'}`);
171
+ const paths = rule.http?.paths || [];
172
+ paths.forEach(p => {
173
+ const backend = p.backend?.service?.name || p.backend?.url || '-';
174
+ console.log(` ${p.path} → ${backend}`);
175
+ });
176
+ });
177
+ }
178
+ } catch (error) {
179
+ spinner.fail('Failed to fetch ingress');
180
+ console.error(chalk.red(error.response?.data?.error || error.message));
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Delete ingress
186
+ */
187
+ async function remove(name, options) {
188
+ const spinner = ora('Deleting ingress...').start();
189
+
190
+ try {
191
+ const api = await getApiClient();
192
+ await requireAuth(api);
193
+
194
+ const namespace = options.namespace || 'default';
195
+ await api.delete(`/ingress/${namespace}/${name}`);
196
+
197
+ spinner.succeed('Ingress deleted successfully!');
198
+ } catch (error) {
199
+ spinner.fail('Failed to delete ingress');
200
+ console.error(chalk.red(error.response?.data?.error || error.message));
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Export ingress as YAML
206
+ */
207
+ async function exportYaml(name, options) {
208
+ const spinner = ora('Exporting ingress...').start();
209
+
210
+ try {
211
+ const api = await getApiClient();
212
+ await requireAuth(api);
213
+
214
+ const namespace = options.namespace || 'default';
215
+ const response = await api.get(`/ingress/${namespace}/${name}/yaml`);
216
+
217
+ spinner.stop();
218
+
219
+ const yaml = response.data.yaml;
220
+
221
+ if (options.output) {
222
+ fs.writeFileSync(options.output, yaml);
223
+ console.log(chalk.green(`✓ YAML exported to ${options.output}`));
224
+ } else {
225
+ console.log('\n' + chalk.cyan('Ingress YAML:'));
226
+ console.log(chalk.gray('─'.repeat(50)));
227
+ console.log(yaml);
228
+ }
229
+ } catch (error) {
230
+ spinner.fail('Failed to export ingress');
231
+ console.error(chalk.red(error.response?.data?.error || error.message));
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Validate YAML
237
+ */
238
+ async function validate(yamlFile) {
239
+ const spinner = ora('Validating YAML...').start();
240
+
241
+ try {
242
+ if (!fs.existsSync(yamlFile)) {
243
+ spinner.fail('YAML file not found');
244
+ console.error(chalk.red(`File not found: ${yamlFile}`));
245
+ return;
246
+ }
247
+
248
+ const yamlContent = fs.readFileSync(yamlFile, 'utf8');
249
+
250
+ const api = await getApiClient();
251
+ await requireAuth(api);
252
+
253
+ const response = await api.post('/ingress/validate', { yaml: yamlContent });
254
+
255
+ if (response.data.valid) {
256
+ spinner.succeed('YAML is valid!');
257
+ } else {
258
+ spinner.fail('YAML validation failed');
259
+ console.error(chalk.red(response.data.errors?.join('\n') || 'Unknown error'));
260
+ }
261
+ } catch (error) {
262
+ spinner.fail('Validation failed');
263
+ console.error(chalk.red(error.response?.data?.error || error.message));
264
+ }
265
+ }
266
+
267
+ module.exports = {
268
+ list,
269
+ create,
270
+ createFromYaml,
271
+ get,
272
+ remove,
273
+ exportYaml,
274
+ validate
275
+ };
@@ -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.1",
3
+ "version": "2.5.1",
4
4
  "description": "Command-line interface for API Response Manager",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -29,7 +29,7 @@
29
29
  "developer-tools"
30
30
  ],
31
31
  "author": "Vijay Singh Purohit",
32
- "license": "MIT",
32
+ "license": "SEE LICENSE IN LICENSE",
33
33
  "dependencies": {
34
34
  "axios": "^1.6.2",
35
35
  "boxen": "^5.1.2",
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,
package/utils/config.js CHANGED
@@ -4,7 +4,9 @@ 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
9
+ tunnelServerUrl: 'wss://tunnel.tunnelapi.in',
8
10
  token: null,
9
11
  userId: null,
10
12
  email: null,
@@ -16,6 +18,13 @@ const config = new Conf({
16
18
  type: 'string',
17
19
  format: 'uri'
18
20
  },
21
+ API_URL: {
22
+ type: 'string',
23
+ format: 'uri'
24
+ },
25
+ tunnelServerUrl: {
26
+ type: 'string'
27
+ },
19
28
  token: {
20
29
  type: ['string', 'null']
21
30
  },