api-response-manager 2.5.0 → 2.5.2
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/bin/arm.js +125 -0
- package/commands/account.js +189 -0
- package/commands/gateway.js +275 -0
- package/commands/ingress.js +275 -0
- package/commands/tunnel.js +18 -4
- package/package.json +1 -1
- package/utils/config.js +4 -0
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
|
+
};
|
package/commands/tunnel.js
CHANGED
|
@@ -110,18 +110,31 @@ async function connectTunnelClient(tunnelId, subdomain, localPort) {
|
|
|
110
110
|
// Forward request to local server
|
|
111
111
|
try {
|
|
112
112
|
const localUrl = `http://localhost:${localPort}${message.path}`;
|
|
113
|
+
|
|
114
|
+
// Remove problematic headers that cause issues with local server
|
|
115
|
+
const forwardHeaders = { ...message.headers };
|
|
116
|
+
delete forwardHeaders['host'];
|
|
117
|
+
delete forwardHeaders['connection'];
|
|
118
|
+
delete forwardHeaders['accept-encoding']; // Prevent compression issues
|
|
119
|
+
|
|
113
120
|
const response = await axios({
|
|
114
121
|
method: message.method.toLowerCase(),
|
|
115
122
|
url: localUrl,
|
|
116
|
-
headers:
|
|
123
|
+
headers: forwardHeaders,
|
|
117
124
|
data: message.body,
|
|
118
|
-
validateStatus: () => true // Accept any status code
|
|
125
|
+
validateStatus: () => true, // Accept any status code
|
|
126
|
+
responseType: 'arraybuffer', // Handle binary data properly
|
|
127
|
+
maxRedirects: 0, // Don't follow redirects, let the client handle them
|
|
128
|
+
timeout: 25000 // 25 second timeout
|
|
119
129
|
});
|
|
120
130
|
|
|
121
131
|
// Clean up headers to avoid conflicts
|
|
122
132
|
const cleanHeaders = { ...response.headers };
|
|
123
133
|
delete cleanHeaders['transfer-encoding'];
|
|
124
|
-
delete cleanHeaders['
|
|
134
|
+
delete cleanHeaders['connection'];
|
|
135
|
+
|
|
136
|
+
// Convert binary data to base64 for JSON transport
|
|
137
|
+
const bodyBase64 = Buffer.from(response.data).toString('base64');
|
|
125
138
|
|
|
126
139
|
// Send response back to tunnel server
|
|
127
140
|
ws.send(JSON.stringify({
|
|
@@ -129,7 +142,8 @@ async function connectTunnelClient(tunnelId, subdomain, localPort) {
|
|
|
129
142
|
requestId: message.requestId,
|
|
130
143
|
statusCode: response.status,
|
|
131
144
|
headers: cleanHeaders,
|
|
132
|
-
body:
|
|
145
|
+
body: bodyBase64,
|
|
146
|
+
encoding: 'base64'
|
|
133
147
|
}));
|
|
134
148
|
} catch (error) {
|
|
135
149
|
console.error(chalk.red(`Error forwarding request: ${error.message}`));
|
package/package.json
CHANGED
package/utils/config.js
CHANGED
|
@@ -6,6 +6,7 @@ const config = new Conf({
|
|
|
6
6
|
defaults: {
|
|
7
7
|
apiUrl: 'https://api.tunnelapi.in/api',
|
|
8
8
|
API_URL: 'https://api.tunnelapi.in/api', // Support both camelCase and UPPER_CASE
|
|
9
|
+
tunnelServerUrl: 'wss://tunnel.tunnelapi.in',
|
|
9
10
|
token: null,
|
|
10
11
|
userId: null,
|
|
11
12
|
email: null,
|
|
@@ -21,6 +22,9 @@ const config = new Conf({
|
|
|
21
22
|
type: 'string',
|
|
22
23
|
format: 'uri'
|
|
23
24
|
},
|
|
25
|
+
tunnelServerUrl: {
|
|
26
|
+
type: 'string'
|
|
27
|
+
},
|
|
24
28
|
token: {
|
|
25
29
|
type: ['string', 'null']
|
|
26
30
|
},
|