api-response-manager 2.5.4 → 2.6.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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  Command-line interface for API Response Manager. Manage tunnels, webhooks, and projects from your terminal.
8
8
 
9
- **Version:** 2.5.3 | **Live Service:** https://tunnelapi.in
9
+ **Version:** 2.6.0 | **Live Service:** https://tunnelapi.in
10
10
 
11
11
  ## Installation
12
12
 
package/bin/arm.js CHANGED
@@ -26,6 +26,7 @@ const ingressCommand = require('../commands/ingress');
26
26
  const accountCommand = require('../commands/account');
27
27
  const installCommand = require('../commands/install');
28
28
  const uninstallCommand = require('../commands/uninstall');
29
+ const serveCommand = require('../commands/serve');
29
30
 
30
31
  // CLI setup
31
32
  program
@@ -58,9 +59,10 @@ program
58
59
  .option('-n, --name <name>', 'Tunnel name')
59
60
  .option('-a, --auth', 'Enable basic authentication')
60
61
  .option('-r, --rate-limit <limit>', 'Rate limit (requests per minute)', '60')
61
- .option('-p, --protocol <protocol>', 'Protocol (http, https, tcp, ssh, ws, wss)', 'http')
62
+ .option('-p, --protocol <protocol>', 'Protocol (http, https, tcp, ssh, ws, wss, udp)', 'http')
62
63
  .option('--ssl', 'Enable SSL/HTTPS')
63
64
  .option('-d, --domain <domain>', 'Custom domain')
65
+ .option('--e2e', 'Enable end-to-end encryption')
64
66
  .option('--json', 'Output in JSON format (for CI/CD automation)')
65
67
  .action(tunnelCommand.start);
66
68
 
@@ -143,6 +145,15 @@ program
143
145
  .option('--tls', 'Enable TLS for ingress')
144
146
  .action(tunnelCommand.configureIngress);
145
147
 
148
+ // Serve command - file sharing
149
+ program
150
+ .command('serve')
151
+ .description('Share a directory over the internet')
152
+ .argument('[directory]', 'Directory to serve (default: current directory)', '.')
153
+ .option('-p, --port <port>', 'Local port for file server', '8000')
154
+ .option('-s, --subdomain <subdomain>', 'Custom subdomain')
155
+ .action(serveCommand.serve);
156
+
146
157
  // IP Whitelist commands
147
158
  program
148
159
  .command('tunnel:ip-whitelist:add')
@@ -0,0 +1,569 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const http = require('http');
6
+ const url = require('url');
7
+ const mime = require('mime-types');
8
+ const api = require('../utils/api');
9
+ const config = require('../utils/config');
10
+
11
+ // File server for serving static files
12
+ class FileServer {
13
+ constructor(directory, port) {
14
+ this.directory = path.resolve(directory);
15
+ this.port = port;
16
+ this.server = null;
17
+ }
18
+
19
+ start() {
20
+ return new Promise((resolve, reject) => {
21
+ this.server = http.createServer((req, res) => {
22
+ this.handleRequest(req, res);
23
+ });
24
+
25
+ this.server.on('error', (err) => {
26
+ if (err.code === 'EADDRINUSE') {
27
+ reject(new Error(`Port ${this.port} is already in use`));
28
+ } else {
29
+ reject(err);
30
+ }
31
+ });
32
+
33
+ this.server.listen(this.port, () => {
34
+ resolve();
35
+ });
36
+ });
37
+ }
38
+
39
+ handleRequest(req, res) {
40
+ const parsedUrl = url.parse(req.url, true);
41
+ let pathname = decodeURIComponent(parsedUrl.pathname);
42
+
43
+ // Prevent directory traversal
44
+ pathname = pathname.replace(/\.\./g, '');
45
+
46
+ let filePath = path.join(this.directory, pathname);
47
+
48
+ // Check if path exists
49
+ fs.stat(filePath, (err, stats) => {
50
+ if (err) {
51
+ if (err.code === 'ENOENT') {
52
+ res.writeHead(404, { 'Content-Type': 'text/html' });
53
+ res.end(this.generate404Page(pathname));
54
+ } else {
55
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
56
+ res.end('Internal Server Error');
57
+ }
58
+ return;
59
+ }
60
+
61
+ if (stats.isDirectory()) {
62
+ // Check for index.html
63
+ const indexPath = path.join(filePath, 'index.html');
64
+ if (fs.existsSync(indexPath)) {
65
+ this.serveFile(indexPath, res);
66
+ } else {
67
+ // Generate directory listing
68
+ this.serveDirectoryListing(filePath, pathname, res);
69
+ }
70
+ } else {
71
+ this.serveFile(filePath, res);
72
+ }
73
+ });
74
+ }
75
+
76
+ serveFile(filePath, res) {
77
+ const ext = path.extname(filePath).toLowerCase();
78
+ // Handle JSX/TSX files as JavaScript for development
79
+ let contentType;
80
+ if (ext === '.jsx' || ext === '.tsx') {
81
+ contentType = 'application/javascript';
82
+ } else if (ext === '.ts') {
83
+ contentType = 'application/javascript';
84
+ } else {
85
+ contentType = mime.lookup(ext) || 'application/octet-stream';
86
+ }
87
+
88
+ fs.readFile(filePath, (err, data) => {
89
+ if (err) {
90
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
91
+ res.end('Error reading file');
92
+ return;
93
+ }
94
+
95
+ const stats = fs.statSync(filePath);
96
+ res.writeHead(200, {
97
+ 'Content-Type': contentType,
98
+ 'Content-Length': stats.size,
99
+ 'Cache-Control': 'no-cache',
100
+ 'Access-Control-Allow-Origin': '*'
101
+ });
102
+ res.end(data);
103
+ });
104
+ }
105
+
106
+ serveDirectoryListing(dirPath, urlPath, res) {
107
+ fs.readdir(dirPath, { withFileTypes: true }, (err, entries) => {
108
+ if (err) {
109
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
110
+ res.end('Error reading directory');
111
+ return;
112
+ }
113
+
114
+ const html = this.generateDirectoryHTML(entries, urlPath, dirPath);
115
+ res.writeHead(200, {
116
+ 'Content-Type': 'text/html',
117
+ 'Cache-Control': 'no-cache'
118
+ });
119
+ res.end(html);
120
+ });
121
+ }
122
+
123
+ generateDirectoryHTML(entries, urlPath, dirPath) {
124
+ const normalizedPath = urlPath.endsWith('/') ? urlPath : urlPath + '/';
125
+
126
+ let items = entries.map(entry => {
127
+ const isDir = entry.isDirectory();
128
+ const name = entry.name;
129
+ const href = normalizedPath + encodeURIComponent(name) + (isDir ? '/' : '');
130
+ const icon = isDir ? '📁' : this.getFileIcon(name);
131
+ const stats = fs.statSync(path.join(dirPath, name));
132
+ const size = isDir ? '-' : this.formatSize(stats.size);
133
+ const modified = stats.mtime.toLocaleString();
134
+
135
+ return `
136
+ <tr>
137
+ <td class="icon">${icon}</td>
138
+ <td class="name"><a href="${href}">${name}${isDir ? '/' : ''}</a></td>
139
+ <td class="size">${size}</td>
140
+ <td class="modified">${modified}</td>
141
+ </tr>
142
+ `;
143
+ }).join('');
144
+
145
+ // Add parent directory link if not at root
146
+ if (urlPath !== '/' && urlPath !== '') {
147
+ const parentPath = path.dirname(urlPath) || '/';
148
+ items = `
149
+ <tr>
150
+ <td class="icon">📂</td>
151
+ <td class="name"><a href="${parentPath}">..</a></td>
152
+ <td class="size">-</td>
153
+ <td class="modified">-</td>
154
+ </tr>
155
+ ` + items;
156
+ }
157
+
158
+ return `
159
+ <!DOCTYPE html>
160
+ <html>
161
+ <head>
162
+ <meta charset="UTF-8">
163
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
164
+ <title>Index of ${urlPath || '/'}</title>
165
+ <style>
166
+ * { box-sizing: border-box; margin: 0; padding: 0; }
167
+ body {
168
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
169
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
170
+ color: #e4e4e7;
171
+ min-height: 100vh;
172
+ padding: 2rem;
173
+ }
174
+ .container {
175
+ max-width: 1000px;
176
+ margin: 0 auto;
177
+ }
178
+ h1 {
179
+ font-size: 1.5rem;
180
+ margin-bottom: 1.5rem;
181
+ color: #a78bfa;
182
+ }
183
+ .path {
184
+ color: #94a3b8;
185
+ font-family: monospace;
186
+ }
187
+ table {
188
+ width: 100%;
189
+ border-collapse: collapse;
190
+ background: rgba(255,255,255,0.05);
191
+ border-radius: 0.5rem;
192
+ overflow: hidden;
193
+ }
194
+ th, td {
195
+ padding: 0.75rem 1rem;
196
+ text-align: left;
197
+ border-bottom: 1px solid rgba(255,255,255,0.1);
198
+ }
199
+ th {
200
+ background: rgba(255,255,255,0.1);
201
+ font-weight: 600;
202
+ color: #94a3b8;
203
+ font-size: 0.875rem;
204
+ }
205
+ tr:hover {
206
+ background: rgba(255,255,255,0.05);
207
+ }
208
+ a {
209
+ color: #60a5fa;
210
+ text-decoration: none;
211
+ }
212
+ a:hover {
213
+ text-decoration: underline;
214
+ }
215
+ .icon { width: 40px; text-align: center; }
216
+ .size { width: 100px; color: #94a3b8; font-size: 0.875rem; }
217
+ .modified { color: #94a3b8; font-size: 0.875rem; }
218
+ .footer {
219
+ margin-top: 2rem;
220
+ text-align: center;
221
+ color: #64748b;
222
+ font-size: 0.875rem;
223
+ }
224
+ .footer a { color: #a78bfa; }
225
+ </style>
226
+ </head>
227
+ <body>
228
+ <div class="container">
229
+ <h1>📂 Index of <span class="path">${urlPath || '/'}</span></h1>
230
+ <table>
231
+ <thead>
232
+ <tr>
233
+ <th></th>
234
+ <th>Name</th>
235
+ <th>Size</th>
236
+ <th>Modified</th>
237
+ </tr>
238
+ </thead>
239
+ <tbody>
240
+ ${items}
241
+ </tbody>
242
+ </table>
243
+ <div class="footer">
244
+ Served by <a href="https://tunnelapi.in">TunnelAPI</a> File Server
245
+ </div>
246
+ </div>
247
+ </body>
248
+ </html>
249
+ `;
250
+ }
251
+
252
+ generate404Page(pathname) {
253
+ return `
254
+ <!DOCTYPE html>
255
+ <html>
256
+ <head>
257
+ <meta charset="UTF-8">
258
+ <title>404 Not Found</title>
259
+ <style>
260
+ body {
261
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
262
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
263
+ color: #e4e4e7;
264
+ min-height: 100vh;
265
+ display: flex;
266
+ align-items: center;
267
+ justify-content: center;
268
+ text-align: center;
269
+ }
270
+ h1 { font-size: 4rem; color: #ef4444; }
271
+ p { color: #94a3b8; margin-top: 1rem; }
272
+ a { color: #60a5fa; }
273
+ </style>
274
+ </head>
275
+ <body>
276
+ <div>
277
+ <h1>404</h1>
278
+ <p>File not found: ${pathname}</p>
279
+ <p><a href="/">← Back to root</a></p>
280
+ </div>
281
+ </body>
282
+ </html>
283
+ `;
284
+ }
285
+
286
+ getFileIcon(filename) {
287
+ const ext = path.extname(filename).toLowerCase();
288
+ const icons = {
289
+ '.html': '🌐', '.htm': '🌐',
290
+ '.css': '🎨',
291
+ '.js': '📜', '.ts': '📜', '.jsx': '📜', '.tsx': '📜',
292
+ '.json': '📋',
293
+ '.md': '📝', '.txt': '📝',
294
+ '.png': '🖼️', '.jpg': '🖼️', '.jpeg': '🖼️', '.gif': '🖼️', '.svg': '🖼️', '.webp': '🖼️',
295
+ '.pdf': '📕',
296
+ '.zip': '📦', '.tar': '📦', '.gz': '📦', '.rar': '📦',
297
+ '.mp3': '🎵', '.wav': '🎵', '.ogg': '🎵',
298
+ '.mp4': '🎬', '.webm': '🎬', '.avi': '🎬', '.mov': '🎬',
299
+ '.py': '🐍',
300
+ '.go': '🐹',
301
+ '.rs': '🦀',
302
+ '.java': '☕',
303
+ '.rb': '💎',
304
+ '.php': '🐘',
305
+ '.sh': '🖥️', '.bash': '🖥️',
306
+ '.yml': '⚙️', '.yaml': '⚙️',
307
+ '.xml': '📰',
308
+ '.sql': '🗃️',
309
+ '.env': '🔐',
310
+ };
311
+ return icons[ext] || '📄';
312
+ }
313
+
314
+ formatSize(bytes) {
315
+ if (bytes === 0) return '0 B';
316
+ const k = 1024;
317
+ const sizes = ['B', 'KB', 'MB', 'GB'];
318
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
319
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
320
+ }
321
+
322
+ stop() {
323
+ return new Promise((resolve) => {
324
+ if (this.server) {
325
+ this.server.close(() => resolve());
326
+ } else {
327
+ resolve();
328
+ }
329
+ });
330
+ }
331
+ }
332
+
333
+ // Serve command - share a directory over the internet
334
+ async function serve(directory, options) {
335
+ const dir = directory || '.';
336
+ const port = parseInt(options.port) || 8000;
337
+ const subdomain = options.subdomain;
338
+
339
+ // Validate directory exists
340
+ const resolvedDir = path.resolve(dir);
341
+ if (!fs.existsSync(resolvedDir)) {
342
+ console.error(chalk.red(`\n✗ Directory not found: ${resolvedDir}\n`));
343
+ process.exit(1);
344
+ }
345
+
346
+ if (!fs.statSync(resolvedDir).isDirectory()) {
347
+ console.error(chalk.red(`\n✗ Not a directory: ${resolvedDir}\n`));
348
+ process.exit(1);
349
+ }
350
+
351
+ console.log(chalk.blue.bold('\n📂 Starting File Server...\n'));
352
+
353
+ const spinner = ora('Checking file transfer limits...').start();
354
+
355
+ try {
356
+ // Check file transfer limit before starting
357
+ const limitCheck = await api.checkFileTransferLimit();
358
+ if (!limitCheck.allowed) {
359
+ spinner.fail(chalk.red('File transfer limit reached'));
360
+ console.error(chalk.red(`\n✗ ${limitCheck.msg}\n`));
361
+ console.log(chalk.yellow('💡 Upgrade your plan at https://tunnelapi.in/pricing to get more transfer quota.\n'));
362
+ process.exit(1);
363
+ }
364
+
365
+ // Show remaining quota
366
+ if (limitCheck.limitMB) {
367
+ spinner.succeed(chalk.green(`Transfer quota: ${limitCheck.usedMB} MB / ${limitCheck.limitMB} MB used (${limitCheck.remainingMB} MB remaining)`));
368
+ } else {
369
+ spinner.succeed(chalk.green('Transfer quota: Unlimited'));
370
+ }
371
+
372
+ spinner.start('Starting local file server...');
373
+
374
+ // Start local file server
375
+ const fileServer = new FileServer(resolvedDir, port);
376
+ await fileServer.start();
377
+ spinner.succeed(chalk.green(`Local file server running on port ${port}`));
378
+
379
+ // Create tunnel
380
+ spinner.start('Creating tunnel...');
381
+
382
+ const tunnelData = {
383
+ name: subdomain || `file-server-${Date.now()}`,
384
+ subdomain: subdomain,
385
+ localPort: port,
386
+ protocol: 'http'
387
+ };
388
+
389
+ const response = await api.createTunnel(tunnelData);
390
+ const tunnel = response.tunnel;
391
+
392
+ spinner.succeed(chalk.green('Tunnel created successfully!'));
393
+
394
+ // Display info
395
+ console.log(chalk.gray('\n┌─────────────────────────────────────────────────────────┐'));
396
+ console.log(chalk.gray('│') + chalk.white.bold(' File Server Information ') + chalk.gray('│'));
397
+ console.log(chalk.gray('├─────────────────────────────────────────────────────────┤'));
398
+ console.log(chalk.gray('│') + chalk.gray(' Directory: ') + chalk.white(resolvedDir.substring(0, 40).padEnd(40)) + chalk.gray('│'));
399
+ console.log(chalk.gray('│') + chalk.gray(' Local Port: ') + chalk.white(String(port).padEnd(40)) + chalk.gray('│'));
400
+ console.log(chalk.gray('│') + chalk.gray(' Public URL: ') + chalk.cyan(tunnel.publicUrl.padEnd(40)) + chalk.gray('│'));
401
+ console.log(chalk.gray('└─────────────────────────────────────────────────────────┘\n'));
402
+
403
+ // Display QR code
404
+ const qrcode = require('qrcode-terminal');
405
+ console.log(chalk.yellow.bold('📱 Scan QR Code to access on mobile:\n'));
406
+ qrcode.generate(tunnel.publicUrl, { small: true }, (qr) => {
407
+ console.log(qr);
408
+ });
409
+
410
+ console.log(chalk.gray('\nPress Ctrl+C to stop serving files\n'));
411
+
412
+ // Connect tunnel client
413
+ const tunnelCommand = require('./tunnel');
414
+ // We need to connect the tunnel client to forward requests
415
+ await connectFileTunnel(tunnel.id || tunnel._id, tunnel.subdomain, port);
416
+
417
+ // Handle shutdown
418
+ process.on('SIGINT', async () => {
419
+ console.log(chalk.yellow('\n\n🛑 Stopping file server...'));
420
+ await fileServer.stop();
421
+ try {
422
+ await api.deleteTunnel(tunnel.id || tunnel._id);
423
+ } catch (e) {
424
+ // Ignore errors on cleanup
425
+ }
426
+ console.log(chalk.green('File server stopped.\n'));
427
+ process.exit(0);
428
+ });
429
+
430
+ } catch (error) {
431
+ spinner.fail(chalk.red('Failed to start file server'));
432
+ console.error(chalk.red(`\n✗ ${error.response?.data?.msg || error.message}\n`));
433
+ process.exit(1);
434
+ }
435
+ }
436
+
437
+ // Connect tunnel client for file serving
438
+ async function connectFileTunnel(tunnelId, subdomain, localPort) {
439
+ const WebSocket = require('ws');
440
+ const axios = require('axios');
441
+
442
+ const tunnelServerUrl = config.get('tunnelServerUrl') || 'wss://tunnel.tunnelapi.in';
443
+ const token = api.getToken();
444
+ const userId = config.get('userId');
445
+
446
+ const ws = new WebSocket(tunnelServerUrl);
447
+
448
+ ws.on('open', () => {
449
+ console.log(chalk.green('✓ Connected to tunnel server'));
450
+
451
+ ws.send(JSON.stringify({
452
+ type: 'register',
453
+ tunnelId,
454
+ subdomain,
455
+ localPort,
456
+ protocol: 'http',
457
+ authToken: token,
458
+ userId
459
+ }));
460
+
461
+ // Heartbeat
462
+ setInterval(() => {
463
+ if (ws.readyState === WebSocket.OPEN) {
464
+ ws.send(JSON.stringify({ type: 'heartbeat', tunnelId, subdomain }));
465
+ }
466
+ }, 30000);
467
+ });
468
+
469
+ // Track total bytes transferred
470
+ let totalBytesTransferred = 0;
471
+ let limitReached = false;
472
+
473
+ ws.on('message', async (data) => {
474
+ const message = JSON.parse(data.toString());
475
+
476
+ if (message.type === 'registered') {
477
+ console.log(chalk.green.bold('\n🎉 File Server is now publicly accessible!\n'));
478
+ console.log(chalk.white('Share this URL:'));
479
+ console.log(chalk.cyan.bold(` ${message.publicUrl}\n`));
480
+ } else if (message.type === 'request') {
481
+ const timestamp = new Date().toLocaleTimeString();
482
+
483
+ // Check if limit was reached
484
+ if (limitReached) {
485
+ console.log(chalk.red(`[${timestamp}]`), chalk.red('BLOCKED'), chalk.white(message.path), chalk.red('- Transfer limit reached'));
486
+ ws.send(JSON.stringify({
487
+ type: 'response',
488
+ requestId: message.requestId,
489
+ statusCode: 429,
490
+ headers: { 'content-type': 'application/json' },
491
+ body: Buffer.from(JSON.stringify({
492
+ error: 'Monthly file transfer limit reached',
493
+ message: 'Upgrade your plan at https://tunnelapi.in/pricing for more transfer quota.'
494
+ })).toString('base64'),
495
+ encoding: 'base64'
496
+ }));
497
+ return;
498
+ }
499
+
500
+ console.log(chalk.gray(`[${timestamp}]`), chalk.blue(message.method), chalk.white(message.path));
501
+
502
+ try {
503
+ const localUrl = `http://localhost:${localPort}${message.path}`;
504
+
505
+ const response = await axios({
506
+ method: message.method.toLowerCase(),
507
+ url: localUrl,
508
+ headers: { ...message.headers, host: `localhost:${localPort}` },
509
+ data: message.body,
510
+ validateStatus: () => true,
511
+ responseType: 'arraybuffer',
512
+ maxRedirects: 0,
513
+ timeout: 30000
514
+ });
515
+
516
+ const cleanHeaders = { ...response.headers };
517
+ delete cleanHeaders['transfer-encoding'];
518
+ delete cleanHeaders['connection'];
519
+
520
+ const responseData = Buffer.from(response.data);
521
+ const bodyBase64 = responseData.toString('base64');
522
+ const bytesTransferred = responseData.length;
523
+
524
+ // Track file transfer usage
525
+ try {
526
+ await api.trackFileTransfer(userId, bytesTransferred);
527
+ totalBytesTransferred += bytesTransferred;
528
+ } catch (trackError) {
529
+ if (trackError.response?.data?.limitReached) {
530
+ limitReached = true;
531
+ console.log(chalk.red.bold('\n⚠️ Monthly file transfer limit reached!'));
532
+ console.log(chalk.yellow('💡 Upgrade your plan at https://tunnelapi.in/pricing for more transfer quota.\n'));
533
+ }
534
+ }
535
+
536
+ ws.send(JSON.stringify({
537
+ type: 'response',
538
+ requestId: message.requestId,
539
+ statusCode: response.status,
540
+ headers: cleanHeaders,
541
+ body: bodyBase64,
542
+ encoding: 'base64'
543
+ }));
544
+ } catch (error) {
545
+ console.error(chalk.red(`Error: ${error.message}`));
546
+ ws.send(JSON.stringify({
547
+ type: 'response',
548
+ requestId: message.requestId,
549
+ statusCode: 502,
550
+ headers: { 'content-type': 'application/json' },
551
+ body: Buffer.from(JSON.stringify({ error: 'File server error' })).toString('base64'),
552
+ encoding: 'base64'
553
+ }));
554
+ }
555
+ }
556
+ });
557
+
558
+ ws.on('error', (error) => {
559
+ console.error(chalk.red(`WebSocket error: ${error.message}`));
560
+ });
561
+
562
+ ws.on('close', () => {
563
+ console.log(chalk.yellow('Tunnel connection closed'));
564
+ });
565
+ }
566
+
567
+ module.exports = {
568
+ serve
569
+ };
@@ -4,12 +4,21 @@ const WebSocket = require('ws');
4
4
  const Table = require('cli-table3');
5
5
  const axios = require('axios');
6
6
  const net = require('net');
7
+ const qrcode = require('qrcode-terminal');
7
8
  const api = require('../utils/api');
8
9
  const config = require('../utils/config');
9
10
 
10
11
  // Track TCP connections for SSH/TCP tunnels
11
12
  const tcpConnections = new Map();
12
13
 
14
+ // Generate and display QR code for URL
15
+ function displayQRCode(url, small = true) {
16
+ console.log(chalk.yellow.bold('\n📱 Scan QR Code to access on mobile:\n'));
17
+ qrcode.generate(url, { small }, (qr) => {
18
+ console.log(qr);
19
+ });
20
+ }
21
+
13
22
  // Start tunnel
14
23
  async function start(port, options) {
15
24
  const jsonOutput = options.json || false;
@@ -157,6 +166,9 @@ async function connectTunnelClient(tunnelId, subdomain, localPort, protocol = 'h
157
166
  console.log(chalk.gray('SSH with Key (passwordless):'));
158
167
  console.log(chalk.cyan(` ssh -i ~/.ssh/your_key.pem -o ProxyCommand="echo 'SUBDOMAIN:${subdomain}' | nc %h %p" user@${message.tcpHost} -p ${message.tcpPort}\n`));
159
168
  }
169
+ } else {
170
+ // Display QR code for HTTP/HTTPS tunnels (not for TCP/SSH)
171
+ displayQRCode(message.publicUrl);
160
172
  }
161
173
 
162
174
  console.log(chalk.gray('Press Ctrl+C to stop the tunnel\n'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-response-manager",
3
- "version": "2.5.4",
3
+ "version": "2.6.0",
4
4
  "description": "Command-line interface for API Response Manager",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -62,8 +62,10 @@
62
62
  "conf": "^10.2.0",
63
63
  "dotenv": "^16.3.1",
64
64
  "inquirer": "^8.2.5",
65
+ "mime-types": "^3.0.2",
65
66
  "open": "^8.4.0",
66
67
  "ora": "^5.4.1",
68
+ "qrcode-terminal": "^0.12.0",
67
69
  "update-notifier": "^6.0.2",
68
70
  "ws": "^8.14.2"
69
71
  },
package/utils/api.js CHANGED
@@ -109,6 +109,17 @@ class APIClient {
109
109
  return response.data;
110
110
  }
111
111
 
112
+ // File Transfer
113
+ async checkFileTransferLimit() {
114
+ const response = await this.client.get('/tunnels/file-transfer/check');
115
+ return response.data;
116
+ }
117
+
118
+ async trackFileTransfer(userId, bytes) {
119
+ const response = await this.client.post('/tunnels/file-transfer/track', { userId, bytes });
120
+ return response.data;
121
+ }
122
+
112
123
  // Webhooks
113
124
  async createWebhook(data) {
114
125
  const response = await this.client.post('/webhooks', data);
@@ -0,0 +1,162 @@
1
+ const crypto = require('crypto');
2
+
3
+ class E2EEncryptionClient {
4
+ constructor() {
5
+ this.algorithm = 'aes-256-gcm';
6
+ this.keyLength = 32;
7
+ this.ivLength = 16;
8
+ this.authTagLength = 16;
9
+ this.ecdh = null;
10
+ this.publicKey = null;
11
+ this.privateKey = null;
12
+ this.sharedSecret = null;
13
+ this.serverPublicKey = null;
14
+ }
15
+
16
+ // Initialize client-side encryption
17
+ initialize() {
18
+ this.ecdh = crypto.createECDH('secp256k1');
19
+ this.ecdh.generateKeys();
20
+ this.publicKey = this.ecdh.getPublicKey('base64');
21
+ this.privateKey = this.ecdh.getPrivateKey('base64');
22
+ return this.publicKey;
23
+ }
24
+
25
+ // Set server public key and compute shared secret
26
+ setServerPublicKey(serverPublicKey) {
27
+ this.serverPublicKey = serverPublicKey;
28
+ const publicKeyBuffer = Buffer.from(serverPublicKey, 'base64');
29
+ const sharedSecret = this.ecdh.computeSecret(publicKeyBuffer);
30
+ this.sharedSecret = crypto.createHash('sha256').update(sharedSecret).digest();
31
+ return true;
32
+ }
33
+
34
+ // Check if E2E is ready
35
+ isReady() {
36
+ return this.sharedSecret !== null;
37
+ }
38
+
39
+ // Get client public key
40
+ getPublicKey() {
41
+ return this.publicKey;
42
+ }
43
+
44
+ // Encrypt data
45
+ encrypt(plaintext) {
46
+ if (!this.sharedSecret) {
47
+ throw new Error('E2E encryption not initialized');
48
+ }
49
+
50
+ const iv = crypto.randomBytes(this.ivLength);
51
+ const cipher = crypto.createCipheriv(this.algorithm, this.sharedSecret, iv, {
52
+ authTagLength: this.authTagLength
53
+ });
54
+
55
+ let encrypted = cipher.update(plaintext, 'utf8', 'base64');
56
+ encrypted += cipher.final('base64');
57
+ const authTag = cipher.getAuthTag();
58
+
59
+ return {
60
+ encrypted,
61
+ iv: iv.toString('base64'),
62
+ authTag: authTag.toString('base64')
63
+ };
64
+ }
65
+
66
+ // Decrypt data
67
+ decrypt(encryptedData) {
68
+ if (!this.sharedSecret) {
69
+ throw new Error('E2E encryption not initialized');
70
+ }
71
+
72
+ const { encrypted, iv, authTag } = encryptedData;
73
+ const ivBuffer = Buffer.from(iv, 'base64');
74
+ const authTagBuffer = Buffer.from(authTag, 'base64');
75
+
76
+ const decipher = crypto.createDecipheriv(this.algorithm, this.sharedSecret, ivBuffer, {
77
+ authTagLength: this.authTagLength
78
+ });
79
+ decipher.setAuthTag(authTagBuffer);
80
+
81
+ let decrypted = decipher.update(encrypted, 'base64', 'utf8');
82
+ decrypted += decipher.final('utf8');
83
+
84
+ return decrypted;
85
+ }
86
+
87
+ // Encrypt binary data
88
+ encryptBinary(buffer) {
89
+ if (!this.sharedSecret) {
90
+ throw new Error('E2E encryption not initialized');
91
+ }
92
+
93
+ const iv = crypto.randomBytes(this.ivLength);
94
+ const cipher = crypto.createCipheriv(this.algorithm, this.sharedSecret, iv, {
95
+ authTagLength: this.authTagLength
96
+ });
97
+
98
+ const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
99
+ const authTag = cipher.getAuthTag();
100
+
101
+ return {
102
+ encrypted: encrypted.toString('base64'),
103
+ iv: iv.toString('base64'),
104
+ authTag: authTag.toString('base64')
105
+ };
106
+ }
107
+
108
+ // Decrypt binary data
109
+ decryptBinary(encryptedData) {
110
+ if (!this.sharedSecret) {
111
+ throw new Error('E2E encryption not initialized');
112
+ }
113
+
114
+ const { encrypted, iv, authTag } = encryptedData;
115
+ const encryptedBuffer = Buffer.from(encrypted, 'base64');
116
+ const ivBuffer = Buffer.from(iv, 'base64');
117
+ const authTagBuffer = Buffer.from(authTag, 'base64');
118
+
119
+ const decipher = crypto.createDecipheriv(this.algorithm, this.sharedSecret, ivBuffer, {
120
+ authTagLength: this.authTagLength
121
+ });
122
+ decipher.setAuthTag(authTagBuffer);
123
+
124
+ return Buffer.concat([decipher.update(encryptedBuffer), decipher.final()]);
125
+ }
126
+
127
+ // Encrypt a message object
128
+ encryptMessage(message) {
129
+ if (!this.isReady()) {
130
+ return message;
131
+ }
132
+
133
+ const plaintext = JSON.stringify(message);
134
+ const encrypted = this.encrypt(plaintext);
135
+
136
+ return {
137
+ type: 'e2e-encrypted',
138
+ payload: encrypted
139
+ };
140
+ }
141
+
142
+ // Decrypt a message object
143
+ decryptMessage(encryptedMessage) {
144
+ if (encryptedMessage.type !== 'e2e-encrypted') {
145
+ return encryptedMessage;
146
+ }
147
+
148
+ const decrypted = this.decrypt(encryptedMessage.payload);
149
+ return JSON.parse(decrypted);
150
+ }
151
+
152
+ // Reset encryption state
153
+ reset() {
154
+ this.ecdh = null;
155
+ this.publicKey = null;
156
+ this.privateKey = null;
157
+ this.sharedSecret = null;
158
+ this.serverPublicKey = null;
159
+ }
160
+ }
161
+
162
+ module.exports = E2EEncryptionClient;