api-response-manager 1.0.1 → 2.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Vijay Singh Purohit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -41,12 +41,36 @@ npx @vijaypurohit322-arm/cli webhook
41
41
  ## Quick Start
42
42
 
43
43
  ### 1. Login
44
+
45
+ **Interactive Login (Recommended)**
44
46
  ```bash
45
47
  arm login
46
- # Or provide credentials directly
48
+ # Choose from: Email/Password, Google, GitHub, or Microsoft
49
+ ```
50
+
51
+ **Email & Password**
52
+ ```bash
47
53
  arm login -e your@email.com -p yourpassword
48
54
  ```
49
55
 
56
+ **Social Login (OAuth)**
57
+ ```bash
58
+ # Login with Google
59
+ arm login --provider google
60
+
61
+ # Login with GitHub
62
+ arm login --provider github
63
+
64
+ # Login with Microsoft
65
+ arm login --provider microsoft
66
+ ```
67
+
68
+ The CLI will:
69
+ 1. Generate a unique device code
70
+ 2. Open your browser automatically
71
+ 3. Wait for you to authenticate
72
+ 4. Store your credentials securely
73
+
50
74
  ### 2. Start a Tunnel
51
75
  ```bash
52
76
  # Expose local port 3000
@@ -77,9 +101,59 @@ arm webhook --tunnel <tunnel-id>
77
101
 
78
102
  #### `arm login`
79
103
  Authenticate with API Response Manager
104
+
105
+ **Options:**
106
+ - `-e, --email <email>` - Email address (for email/password login)
107
+ - `-p, --password <password>` - Password (for email/password login)
108
+ - `--provider <provider>` - OAuth provider: google, github, or microsoft
109
+
110
+ **Examples:**
80
111
  ```bash
112
+ # Interactive login (choose method)
81
113
  arm login
82
- arm login -e email@example.com -p password
114
+
115
+ # Email & Password
116
+ arm login -e user@example.com -p mypassword
117
+
118
+ # Social Login (OAuth Device Flow)
119
+ arm login --provider github
120
+ arm login --provider google
121
+ arm login --provider microsoft
122
+ ```
123
+
124
+ **OAuth Device Flow:**
125
+ When using social login, the CLI will:
126
+ 1. Generate a unique device code (e.g., `ABCD-EFGH`)
127
+ 2. Display a verification URL
128
+ 3. Automatically open your browser
129
+ 4. Wait for you to complete authentication
130
+ 5. Store your token securely
131
+
132
+ Example output:
133
+ ```bash
134
+ $ arm login --provider github
135
+
136
+ 🌐 Logging in with github...
137
+
138
+ šŸ“‹ Please complete authentication:
139
+
140
+ 1. Visit: https://localhost:5173/device
141
+ 2. Enter code: ABCD-EFGH
142
+
143
+ Code expires in 600 seconds
144
+
145
+ ? Open browser automatically? (Y/n)
146
+
147
+ āœ“ Browser opened
148
+
149
+ ā ‹ Waiting for authentication...
150
+ āœ“ Authentication successful!
151
+
152
+ User: John Doe
153
+ Provider: github
154
+ Token saved to: ~/.config/arm-cli/config.json
155
+
156
+ āœ“ You can now use all ARM CLI commands
83
157
  ```
84
158
 
85
159
  #### `arm logout`
@@ -93,9 +167,23 @@ arm logout
93
167
  #### `arm tunnel <port>`
94
168
  Start a tunnel to expose local server
95
169
  ```bash
170
+ # Basic HTTP tunnel
96
171
  arm tunnel 3000
97
- arm tunnel 3000 --subdomain myapi --name "My API"
98
- arm tunnel 3000 --auth --rate-limit 100
172
+
173
+ # HTTPS tunnel with SSL
174
+ arm tunnel 3000 --protocol https --ssl
175
+
176
+ # TCP tunnel (for databases, etc.)
177
+ arm tunnel 5432 --protocol tcp --subdomain postgres
178
+
179
+ # WebSocket tunnel
180
+ arm tunnel 8080 --protocol ws
181
+
182
+ # With custom subdomain and authentication
183
+ arm tunnel 3000 --subdomain myapi --name "My API" --auth --rate-limit 100
184
+
185
+ # With custom domain
186
+ arm tunnel 3000 --protocol https --ssl --domain api.yourdomain.com
99
187
  ```
100
188
 
101
189
  Options:
@@ -103,6 +191,9 @@ Options:
103
191
  - `-n, --name <name>` - Tunnel name
104
192
  - `-a, --auth` - Enable basic authentication
105
193
  - `-r, --rate-limit <limit>` - Rate limit (requests per minute, default: 60)
194
+ - `-p, --protocol <protocol>` - Protocol: http, https, tcp, ws, wss (default: http)
195
+ - `--ssl` - Enable SSL/HTTPS
196
+ - `-d, --domain <domain>` - Custom domain
106
197
 
107
198
  #### `arm tunnel:list`
108
199
  List all active tunnels
@@ -128,6 +219,91 @@ Options:
128
219
  - `-f, --follow` - Follow log output (real-time)
129
220
  - `-n, --lines <number>` - Number of lines to show (default: 50)
130
221
 
222
+ #### `arm tunnel:domain <tunnelId> <domain>`
223
+ Set custom domain for tunnel
224
+ ```bash
225
+ arm tunnel:domain 507f1f77bcf86cd799439011 api.yourdomain.com
226
+ ```
227
+
228
+ #### `arm tunnel:ssl <tunnelId>`
229
+ Upload SSL certificate for tunnel
230
+ ```bash
231
+ arm tunnel:ssl 507f1f77bcf86cd799439011 --cert cert.pem --key key.pem
232
+ arm tunnel:ssl 507f1f77bcf86cd799439011 --cert cert.pem --key key.pem --ca ca.pem
233
+ ```
234
+
235
+ Options:
236
+ - `--cert <path>` - Path to certificate file
237
+ - `--key <path>` - Path to private key file
238
+ - `--ca <path>` - Path to CA certificate file (optional)
239
+
240
+ #### `arm tunnel:auth:oauth <tunnelId>`
241
+ Configure OAuth authentication for tunnel
242
+ ```bash
243
+ arm tunnel:auth:oauth 507f1f77bcf86cd799439011 \
244
+ --provider google \
245
+ --client-id YOUR_CLIENT_ID \
246
+ --client-secret YOUR_SECRET \
247
+ --callback-url https://yourtunnel.arm.dev/auth/callback \
248
+ --scope openid,email,profile
249
+ ```
250
+
251
+ Options:
252
+ - `--provider <provider>` - OAuth provider: google, github, microsoft, custom
253
+ - `--client-id <id>` - OAuth client ID
254
+ - `--client-secret <secret>` - OAuth client secret
255
+ - `--callback-url <url>` - OAuth callback URL
256
+ - `--scope <scope>` - OAuth scope (comma-separated)
257
+
258
+ #### `arm tunnel:auth:oidc <tunnelId>`
259
+ Configure OpenID Connect authentication for tunnel
260
+ ```bash
261
+ arm tunnel:auth:oidc 507f1f77bcf86cd799439011 \
262
+ --issuer https://accounts.google.com \
263
+ --client-id YOUR_CLIENT_ID \
264
+ --client-secret YOUR_SECRET \
265
+ --callback-url https://yourtunnel.arm.dev/auth/callback
266
+ ```
267
+
268
+ Options:
269
+ - `--issuer <url>` - OIDC issuer URL
270
+ - `--client-id <id>` - OIDC client ID
271
+ - `--client-secret <secret>` - OIDC client secret
272
+ - `--callback-url <url>` - OIDC callback URL
273
+
274
+ #### `arm tunnel:auth:saml <tunnelId>`
275
+ Configure SAML authentication for tunnel
276
+ ```bash
277
+ arm tunnel:auth:saml 507f1f77bcf86cd799439011 \
278
+ --entry-point https://idp.example.com/saml/sso \
279
+ --issuer https://yourtunnel.arm.dev \
280
+ --cert idp-cert.pem \
281
+ --callback-url https://yourtunnel.arm.dev/auth/saml/callback
282
+ ```
283
+
284
+ Options:
285
+ - `--entry-point <url>` - SAML entry point URL
286
+ - `--issuer <issuer>` - SAML issuer
287
+ - `--cert <path>` - Path to IdP certificate file
288
+ - `--callback-url <url>` - SAML callback URL
289
+
290
+ #### `arm tunnel:ingress <tunnelId> <rules>`
291
+ Configure ingress rules for tunnel (path-based routing)
292
+ ```bash
293
+ # Route different paths to different backends
294
+ arm tunnel:ingress 507f1f77bcf86cd799439011 \
295
+ "/api=localhost:3000,/admin=localhost:4000" \
296
+ --tls
297
+
298
+ # Single rule
299
+ arm tunnel:ingress 507f1f77bcf86cd799439011 "/api=localhost:3000"
300
+ ```
301
+
302
+ Options:
303
+ - `--tls` - Enable TLS for ingress
304
+
305
+ Rules format: `/path=host:port,/path2=host:port`
306
+
131
307
  ### Webhooks
132
308
 
133
309
  #### `arm webhook`
@@ -268,13 +444,53 @@ Default configuration:
268
444
  # Login
269
445
  arm login
270
446
 
271
- # Start tunnel on port 3000
272
- arm tunnel 3000 --subdomain myapp
447
+ # Start HTTPS tunnel on port 3000
448
+ arm tunnel 3000 --protocol https --ssl --subdomain myapp
273
449
 
274
450
  # Your local server is now accessible at:
275
451
  # https://myapp.tunnel.arm.dev
276
452
  ```
277
453
 
454
+ ### Secure Tunnel with OAuth Authentication
455
+ ```bash
456
+ # Create HTTPS tunnel
457
+ arm tunnel 3000 --protocol https --ssl --subdomain myapi
458
+
459
+ # Configure Google OAuth
460
+ arm tunnel:auth:oauth <tunnel-id> \
461
+ --provider google \
462
+ --client-id YOUR_GOOGLE_CLIENT_ID \
463
+ --client-secret YOUR_GOOGLE_SECRET \
464
+ --callback-url https://myapi.tunnel.arm.dev/auth/callback
465
+
466
+ # Now your tunnel requires Google login to access
467
+ ```
468
+
469
+ ### TCP Tunnel for Database
470
+ ```bash
471
+ # Expose PostgreSQL database
472
+ arm tunnel 5432 --protocol tcp --subdomain mydb
473
+
474
+ # Connect from anywhere:
475
+ # psql -h mydb.tunnel.arm.dev -p 5432 -U username -d database
476
+ ```
477
+
478
+ ### Multi-Service Routing with Ingress
479
+ ```bash
480
+ # Create tunnel
481
+ arm tunnel 3000 --protocol https --ssl
482
+
483
+ # Configure path-based routing
484
+ arm tunnel:ingress <tunnel-id> \
485
+ "/api/v1=localhost:3000,/api/v2=localhost:4000,/admin=localhost:5000" \
486
+ --tls
487
+
488
+ # Now:
489
+ # https://yourtunnel.arm.dev/api/v1 -> localhost:3000
490
+ # https://yourtunnel.arm.dev/api/v2 -> localhost:4000
491
+ # https://yourtunnel.arm.dev/admin -> localhost:5000
492
+ ```
493
+
278
494
  ### Test Webhooks Locally
279
495
  ```bash
280
496
  # Create webhook that forwards to local server
@@ -328,6 +544,28 @@ arm config:set apiUrl https://your-api-url.com/api
328
544
  arm config:get
329
545
  ```
330
546
 
547
+ ## Troubleshooting
548
+
549
+ ### Command Not Found After Installation
550
+
551
+ If you get `arm: command not found` after installing:
552
+
553
+ **Windows:**
554
+ 1. Check npm prefix: `npm config get prefix`
555
+ 2. Add to PATH: `C:\Users\<YourUsername>\AppData\Roaming\npm`
556
+ 3. Restart terminal
557
+
558
+ **macOS/Linux:**
559
+ ```bash
560
+ echo 'export PATH="$(npm config get prefix)/bin:$PATH"' >> ~/.bashrc
561
+ source ~/.bashrc
562
+ ```
563
+
564
+ **Alternative - Use npx:**
565
+ ```bash
566
+ npx @vijaypurohit322-arm/cli login
567
+ ```
568
+
331
569
  ## Publishing to npm
332
570
 
333
571
  See [PUBLISHING.md](./PUBLISHING.md) for detailed instructions on publishing this package to npm.
package/bin/arm.js CHANGED
@@ -17,6 +17,10 @@ const webhookCommand = require('../commands/webhook');
17
17
  const projectCommand = require('../commands/project');
18
18
  const logsCommand = require('../commands/logs');
19
19
  const configCommand = require('../commands/config');
20
+ const ipWhitelistCommand = require('../commands/ipWhitelist');
21
+ const ipBlacklistCommand = require('../commands/ipBlacklist');
22
+ const rateLimitCommand = require('../commands/rateLimit');
23
+ const healthCommand = require('../commands/health');
20
24
 
21
25
  // CLI setup
22
26
  program
@@ -30,6 +34,7 @@ program
30
34
  .description('Authenticate with API Response Manager')
31
35
  .option('-e, --email <email>', 'Email address')
32
36
  .option('-p, --password <password>', 'Password')
37
+ .option('--provider <provider>', 'OAuth provider (google, github, microsoft)')
33
38
  .action(loginCommand);
34
39
 
35
40
  // Logout command
@@ -47,6 +52,9 @@ program
47
52
  .option('-n, --name <name>', 'Tunnel name')
48
53
  .option('-a, --auth', 'Enable basic authentication')
49
54
  .option('-r, --rate-limit <limit>', 'Rate limit (requests per minute)', '60')
55
+ .option('-p, --protocol <protocol>', 'Protocol (http, https, tcp, ws, wss)', 'http')
56
+ .option('--ssl', 'Enable SSL/HTTPS')
57
+ .option('-d, --domain <domain>', 'Custom domain')
50
58
  .action(tunnelCommand.start);
51
59
 
52
60
  program
@@ -68,6 +76,140 @@ program
68
76
  .option('-n, --lines <number>', 'Number of lines to show', '50')
69
77
  .action(tunnelCommand.logs);
70
78
 
79
+ program
80
+ .command('tunnel:domain')
81
+ .description('Set custom domain for tunnel')
82
+ .argument('<tunnelId>', 'Tunnel ID')
83
+ .argument('<domain>', 'Custom domain (e.g., api.yourdomain.com)')
84
+ .action(tunnelCommand.setDomain);
85
+
86
+ program
87
+ .command('tunnel:ssl')
88
+ .description('Upload SSL certificate for tunnel')
89
+ .argument('<tunnelId>', 'Tunnel ID')
90
+ .option('--cert <path>', 'Path to certificate file')
91
+ .option('--key <path>', 'Path to private key file')
92
+ .option('--ca <path>', 'Path to CA certificate file (optional)')
93
+ .action(tunnelCommand.uploadSSL);
94
+
95
+ program
96
+ .command('tunnel:auth:oauth')
97
+ .description('Configure OAuth authentication')
98
+ .argument('<tunnelId>', 'Tunnel ID')
99
+ .option('--provider <provider>', 'OAuth provider (google, github, microsoft, custom)')
100
+ .option('--client-id <id>', 'OAuth client ID')
101
+ .option('--client-secret <secret>', 'OAuth client secret')
102
+ .option('--callback-url <url>', 'OAuth callback URL')
103
+ .option('--scope <scope>', 'OAuth scope (comma-separated)')
104
+ .action(tunnelCommand.configureOAuth);
105
+
106
+ program
107
+ .command('tunnel:auth:oidc')
108
+ .description('Configure OIDC authentication')
109
+ .argument('<tunnelId>', 'Tunnel ID')
110
+ .option('--issuer <url>', 'OIDC issuer URL')
111
+ .option('--client-id <id>', 'OIDC client ID')
112
+ .option('--client-secret <secret>', 'OIDC client secret')
113
+ .option('--callback-url <url>', 'OIDC callback URL')
114
+ .action(tunnelCommand.configureOIDC);
115
+
116
+ program
117
+ .command('tunnel:auth:saml')
118
+ .description('Configure SAML authentication')
119
+ .argument('<tunnelId>', 'Tunnel ID')
120
+ .option('--entry-point <url>', 'SAML entry point URL')
121
+ .option('--issuer <issuer>', 'SAML issuer')
122
+ .option('--cert <path>', 'Path to IdP certificate file')
123
+ .option('--callback-url <url>', 'SAML callback URL')
124
+ .action(tunnelCommand.configureSAML);
125
+
126
+ program
127
+ .command('tunnel:ingress')
128
+ .description('Configure ingress rules for tunnel')
129
+ .argument('<tunnelId>', 'Tunnel ID')
130
+ .argument('<rules>', 'Ingress rules (e.g., "/api=localhost:3000,/admin=localhost:4000")')
131
+ .option('--tls', 'Enable TLS for ingress')
132
+ .action(tunnelCommand.configureIngress);
133
+
134
+ // IP Whitelist commands
135
+ program
136
+ .command('tunnel:ip-whitelist:add')
137
+ .description('Add IP to tunnel whitelist')
138
+ .argument('<tunnelId>', 'Tunnel ID')
139
+ .argument('<ip>', 'IP address or CIDR range (e.g., 192.168.1.100 or 10.0.0.0/8)')
140
+ .action(ipWhitelistCommand.add);
141
+
142
+ program
143
+ .command('tunnel:ip-whitelist:remove')
144
+ .description('Remove IP from tunnel whitelist')
145
+ .argument('<tunnelId>', 'Tunnel ID')
146
+ .argument('<ip>', 'IP address or CIDR range')
147
+ .action(ipWhitelistCommand.remove);
148
+
149
+ program
150
+ .command('tunnel:ip-whitelist:list')
151
+ .description('List tunnel IP whitelist')
152
+ .argument('<tunnelId>', 'Tunnel ID')
153
+ .action(ipWhitelistCommand.list);
154
+
155
+ program
156
+ .command('tunnel:ip-whitelist:clear')
157
+ .description('Clear tunnel IP whitelist')
158
+ .argument('<tunnelId>', 'Tunnel ID')
159
+ .action(ipWhitelistCommand.clear);
160
+
161
+ // IP Blacklist commands
162
+ program
163
+ .command('tunnel:ip-blacklist:add')
164
+ .description('Add IP to tunnel blacklist')
165
+ .argument('<tunnelId>', 'Tunnel ID')
166
+ .argument('<ip>', 'IP address or CIDR range')
167
+ .action(ipBlacklistCommand.add);
168
+
169
+ program
170
+ .command('tunnel:ip-blacklist:remove')
171
+ .description('Remove IP from tunnel blacklist')
172
+ .argument('<tunnelId>', 'Tunnel ID')
173
+ .argument('<ip>', 'IP address or CIDR range')
174
+ .action(ipBlacklistCommand.remove);
175
+
176
+ program
177
+ .command('tunnel:ip-blacklist:list')
178
+ .description('List tunnel IP blacklist')
179
+ .argument('<tunnelId>', 'Tunnel ID')
180
+ .action(ipBlacklistCommand.list);
181
+
182
+ program
183
+ .command('tunnel:ip-blacklist:clear')
184
+ .description('Clear tunnel IP blacklist')
185
+ .argument('<tunnelId>', 'Tunnel ID')
186
+ .action(ipBlacklistCommand.clear);
187
+
188
+ // Rate Limit commands
189
+ program
190
+ .command('tunnel:rate-limit')
191
+ .description('Configure tunnel rate limits')
192
+ .argument('<tunnelId>', 'Tunnel ID')
193
+ .option('--rpm <number>', 'Requests per minute')
194
+ .option('--rph <number>', 'Requests per hour')
195
+ .option('--rpd <number>', 'Requests per day')
196
+ .option('--enable', 'Enable rate limiting')
197
+ .option('--disable', 'Disable rate limiting')
198
+ .action(rateLimitCommand.configure);
199
+
200
+ program
201
+ .command('tunnel:rate-limit:show')
202
+ .description('Show tunnel rate limits')
203
+ .argument('<tunnelId>', 'Tunnel ID')
204
+ .action(rateLimitCommand.show);
205
+
206
+ // Health check command
207
+ program
208
+ .command('health')
209
+ .description('Check server health')
210
+ .option('-d, --detailed', 'Show detailed health information')
211
+ .action(healthCommand.check);
212
+
71
213
  // Webhook commands
72
214
  program
73
215
  .command('webhook')
@@ -0,0 +1,114 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const axios = require('axios');
4
+ const config = require('../utils/config');
5
+
6
+ // Check basic health
7
+ async function check(options) {
8
+ const apiUrl = config.get('apiUrl') || 'http://localhost:5000/api';
9
+ // Remove /api prefix since apiUrl already includes it
10
+ const endpoint = options.detailed ? '/health/detailed' : '/health';
11
+
12
+ const spinner = ora('Checking server health...').start();
13
+
14
+ try {
15
+ const response = await axios.get(`${apiUrl}${endpoint}`, {
16
+ validateStatus: (status) => status < 500 || status === 503 // Accept 503 for degraded status
17
+ });
18
+ const health = response.data;
19
+
20
+ spinner.stop();
21
+
22
+ if (options.detailed) {
23
+ displayDetailedHealth(health);
24
+ } else {
25
+ displayBasicHealth(health);
26
+ }
27
+
28
+ // Exit with 0 for ok/degraded, 1 for error
29
+ process.exit(health.status === 'ok' || health.status === 'degraded' ? 0 : 1);
30
+ } catch (error) {
31
+ spinner.fail(chalk.red('Server is unreachable'));
32
+ console.error(chalk.red(`\nāœ— ${error.message}\n`));
33
+ process.exit(1);
34
+ }
35
+ }
36
+
37
+ function displayBasicHealth(health) {
38
+ console.log(chalk.blue.bold('\nšŸ„ Server Health Check\n'));
39
+
40
+ const statusColor = health.status === 'ok' ? chalk.green : chalk.red;
41
+ const statusIcon = health.status === 'ok' ? 'āœ“' : 'āœ—';
42
+
43
+ console.log(chalk.gray('ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”'));
44
+ console.log(chalk.gray('│') + chalk.white(' Status: ') + statusColor(`${statusIcon} ${health.status.toUpperCase()}`).padEnd(28) + chalk.gray('│'));
45
+ console.log(chalk.gray('│') + chalk.white(' Uptime: ') + chalk.cyan(formatUptime(health.uptime)).padEnd(20) + chalk.gray('│'));
46
+ console.log(chalk.gray('│') + chalk.white(' Environment: ') + chalk.cyan(health.environment).padEnd(20) + chalk.gray('│'));
47
+ console.log(chalk.gray('│') + chalk.white(' Timestamp: ') + chalk.gray(new Date(health.timestamp).toLocaleString()).padEnd(20) + chalk.gray('│'));
48
+ console.log(chalk.gray('ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜\n'));
49
+ }
50
+
51
+ function displayDetailedHealth(health) {
52
+ console.log(chalk.blue.bold('\nšŸ„ Detailed Server Health Check\n'));
53
+
54
+ const statusColor = health.status === 'ok' ? chalk.green : (health.status === 'degraded' ? chalk.yellow : chalk.red);
55
+ const statusIcon = health.status === 'ok' ? 'āœ“' : (health.status === 'degraded' ? '⚠' : 'āœ—');
56
+
57
+ console.log(chalk.gray('ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”'));
58
+ console.log(chalk.gray('│') + chalk.white.bold(' Overall Status ') + chalk.gray('│'));
59
+ console.log(chalk.gray('ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤'));
60
+ console.log(chalk.gray('│') + chalk.white(' Status: ') + statusColor(`${statusIcon} ${health.status.toUpperCase()}`).padEnd(42) + chalk.gray('│'));
61
+ console.log(chalk.gray('│') + chalk.white(' Uptime: ') + chalk.cyan(formatUptime(health.uptime)).padEnd(34) + chalk.gray('│'));
62
+ console.log(chalk.gray('│') + chalk.white(' Environment: ') + chalk.cyan(health.environment).padEnd(34) + chalk.gray('│'));
63
+ console.log(chalk.gray('│') + chalk.white(' Version: ') + chalk.cyan(health.version || 'N/A').padEnd(34) + chalk.gray('│'));
64
+ console.log(chalk.gray('ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜\n'));
65
+
66
+ if (health.checks) {
67
+ // Database
68
+ console.log(chalk.white.bold('Database:'));
69
+ displayCheckStatus(health.checks.database);
70
+
71
+ // Redis
72
+ console.log(chalk.white.bold('\nRedis:'));
73
+ displayCheckStatus(health.checks.redis);
74
+
75
+ // Memory
76
+ console.log(chalk.white.bold('\nMemory:'));
77
+ displayCheckStatus(health.checks.memory);
78
+
79
+ console.log();
80
+ }
81
+ }
82
+
83
+ function displayCheckStatus(check) {
84
+ const statusColor = check.status === 'ok' ? chalk.green :
85
+ check.status === 'warning' ? chalk.yellow :
86
+ check.status === 'not configured' ? chalk.gray : chalk.red;
87
+ const statusIcon = check.status === 'ok' ? 'āœ“' :
88
+ check.status === 'warning' ? '⚠' :
89
+ check.status === 'not configured' ? 'ā—‹' : 'āœ—';
90
+
91
+ console.log(` ${statusColor(statusIcon)} ${statusColor(check.status.toUpperCase())}`);
92
+
93
+ Object.keys(check).forEach(key => {
94
+ if (key !== 'status') {
95
+ console.log(` ${chalk.gray(key + ':')} ${chalk.white(check[key])}`);
96
+ }
97
+ });
98
+ }
99
+
100
+ function formatUptime(seconds) {
101
+ const days = Math.floor(seconds / 86400);
102
+ const hours = Math.floor((seconds % 86400) / 3600);
103
+ const minutes = Math.floor((seconds % 3600) / 60);
104
+ const secs = Math.floor(seconds % 60);
105
+
106
+ if (days > 0) return `${days}d ${hours}h ${minutes}m`;
107
+ if (hours > 0) return `${hours}h ${minutes}m ${secs}s`;
108
+ if (minutes > 0) return `${minutes}m ${secs}s`;
109
+ return `${secs}s`;
110
+ }
111
+
112
+ module.exports = {
113
+ check
114
+ };
@@ -0,0 +1,121 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const Table = require('cli-table3');
4
+ const api = require('../utils/api');
5
+
6
+ // Add IP to blacklist
7
+ async function add(tunnelId, ip) {
8
+ const spinner = ora('Adding IP to blacklist...').start();
9
+
10
+ try {
11
+ // Get current tunnel
12
+ const tunnel = await api.getTunnel(tunnelId);
13
+ const currentBlacklist = tunnel.tunnel.ipBlacklist || [];
14
+
15
+ // Check if IP already exists
16
+ if (currentBlacklist.includes(ip)) {
17
+ spinner.fail(chalk.yellow(`IP ${ip} is already in the blacklist`));
18
+ return;
19
+ }
20
+
21
+ // Add new IP
22
+ const updatedBlacklist = [...currentBlacklist, ip];
23
+ await api.updateIPBlacklist(tunnelId, updatedBlacklist);
24
+
25
+ spinner.succeed(chalk.green(`IP ${ip} added to blacklist`));
26
+ console.log(chalk.gray(`\nTotal blacklisted IPs: ${updatedBlacklist.length}\n`));
27
+ } catch (error) {
28
+ spinner.fail(chalk.red('Failed to add IP to blacklist'));
29
+ console.error(chalk.red(`\nāœ— ${error.response?.data?.msg || error.message}\n`));
30
+ process.exit(1);
31
+ }
32
+ }
33
+
34
+ // Remove IP from blacklist
35
+ async function remove(tunnelId, ip) {
36
+ const spinner = ora('Removing IP from blacklist...').start();
37
+
38
+ try {
39
+ // Get current tunnel
40
+ const tunnel = await api.getTunnel(tunnelId);
41
+ const currentBlacklist = tunnel.tunnel.ipBlacklist || [];
42
+
43
+ // Check if IP exists
44
+ if (!currentBlacklist.includes(ip)) {
45
+ spinner.fail(chalk.yellow(`IP ${ip} is not in the blacklist`));
46
+ return;
47
+ }
48
+
49
+ // Remove IP
50
+ const updatedBlacklist = currentBlacklist.filter(item => item !== ip);
51
+ await api.updateIPBlacklist(tunnelId, updatedBlacklist);
52
+
53
+ spinner.succeed(chalk.green(`IP ${ip} removed from blacklist`));
54
+ console.log(chalk.gray(`\nTotal blacklisted IPs: ${updatedBlacklist.length}\n`));
55
+ } catch (error) {
56
+ spinner.fail(chalk.red('Failed to remove IP from blacklist'));
57
+ console.error(chalk.red(`\nāœ— ${error.response?.data?.msg || error.message}\n`));
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ // List blacklisted IPs
63
+ async function list(tunnelId) {
64
+ const spinner = ora('Fetching IP blacklist...').start();
65
+
66
+ try {
67
+ const tunnel = await api.getTunnel(tunnelId);
68
+ const blacklist = tunnel.tunnel.ipBlacklist || [];
69
+
70
+ spinner.stop();
71
+
72
+ if (blacklist.length === 0) {
73
+ console.log(chalk.yellow('\n⚠ No IP blacklist configured. No IPs are blocked.\n'));
74
+ return;
75
+ }
76
+
77
+ console.log(chalk.blue.bold(`\n🚫 IP Blacklist (${blacklist.length} IPs)\n`));
78
+
79
+ const table = new Table({
80
+ head: [chalk.white('IP Address / CIDR')],
81
+ style: {
82
+ head: ['cyan'],
83
+ border: ['gray']
84
+ }
85
+ });
86
+
87
+ blacklist.forEach(ip => {
88
+ table.push([chalk.red(ip)]);
89
+ });
90
+
91
+ console.log(table.toString());
92
+ console.log();
93
+ } catch (error) {
94
+ spinner.fail(chalk.red('Failed to fetch IP blacklist'));
95
+ console.error(chalk.red(`\nāœ— ${error.response?.data?.msg || error.message}\n`));
96
+ process.exit(1);
97
+ }
98
+ }
99
+
100
+ // Clear all blacklisted IPs
101
+ async function clear(tunnelId) {
102
+ const spinner = ora('Clearing IP blacklist...').start();
103
+
104
+ try {
105
+ await api.updateIPBlacklist(tunnelId, []);
106
+
107
+ spinner.succeed(chalk.green('IP blacklist cleared'));
108
+ console.log(chalk.gray('\nNo IPs are blocked.\n'));
109
+ } catch (error) {
110
+ spinner.fail(chalk.red('Failed to clear IP blacklist'));
111
+ console.error(chalk.red(`\nāœ— ${error.response?.data?.msg || error.message}\n`));
112
+ process.exit(1);
113
+ }
114
+ }
115
+
116
+ module.exports = {
117
+ add,
118
+ remove,
119
+ list,
120
+ clear
121
+ };
@@ -0,0 +1,121 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const Table = require('cli-table3');
4
+ const api = require('../utils/api');
5
+
6
+ // Add IP to whitelist
7
+ async function add(tunnelId, ip) {
8
+ const spinner = ora('Adding IP to whitelist...').start();
9
+
10
+ try {
11
+ // Get current tunnel
12
+ const tunnel = await api.getTunnel(tunnelId);
13
+ const currentWhitelist = tunnel.tunnel.ipWhitelist || [];
14
+
15
+ // Check if IP already exists
16
+ if (currentWhitelist.includes(ip)) {
17
+ spinner.fail(chalk.yellow(`IP ${ip} is already in the whitelist`));
18
+ return;
19
+ }
20
+
21
+ // Add new IP
22
+ const updatedWhitelist = [...currentWhitelist, ip];
23
+ await api.updateIPWhitelist(tunnelId, updatedWhitelist);
24
+
25
+ spinner.succeed(chalk.green(`IP ${ip} added to whitelist`));
26
+ console.log(chalk.gray(`\nTotal whitelisted IPs: ${updatedWhitelist.length}\n`));
27
+ } catch (error) {
28
+ spinner.fail(chalk.red('Failed to add IP to whitelist'));
29
+ console.error(chalk.red(`\nāœ— ${error.response?.data?.msg || error.message}\n`));
30
+ process.exit(1);
31
+ }
32
+ }
33
+
34
+ // Remove IP from whitelist
35
+ async function remove(tunnelId, ip) {
36
+ const spinner = ora('Removing IP from whitelist...').start();
37
+
38
+ try {
39
+ // Get current tunnel
40
+ const tunnel = await api.getTunnel(tunnelId);
41
+ const currentWhitelist = tunnel.tunnel.ipWhitelist || [];
42
+
43
+ // Check if IP exists
44
+ if (!currentWhitelist.includes(ip)) {
45
+ spinner.fail(chalk.yellow(`IP ${ip} is not in the whitelist`));
46
+ return;
47
+ }
48
+
49
+ // Remove IP
50
+ const updatedWhitelist = currentWhitelist.filter(item => item !== ip);
51
+ await api.updateIPWhitelist(tunnelId, updatedWhitelist);
52
+
53
+ spinner.succeed(chalk.green(`IP ${ip} removed from whitelist`));
54
+ console.log(chalk.gray(`\nTotal whitelisted IPs: ${updatedWhitelist.length}\n`));
55
+ } catch (error) {
56
+ spinner.fail(chalk.red('Failed to remove IP from whitelist'));
57
+ console.error(chalk.red(`\nāœ— ${error.response?.data?.msg || error.message}\n`));
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ // List whitelisted IPs
63
+ async function list(tunnelId) {
64
+ const spinner = ora('Fetching IP whitelist...').start();
65
+
66
+ try {
67
+ const tunnel = await api.getTunnel(tunnelId);
68
+ const whitelist = tunnel.tunnel.ipWhitelist || [];
69
+
70
+ spinner.stop();
71
+
72
+ if (whitelist.length === 0) {
73
+ console.log(chalk.yellow('\n⚠ No IP whitelist configured. All IPs are allowed.\n'));
74
+ return;
75
+ }
76
+
77
+ console.log(chalk.blue.bold(`\nšŸ”’ IP Whitelist (${whitelist.length} IPs)\n`));
78
+
79
+ const table = new Table({
80
+ head: [chalk.white('IP Address / CIDR')],
81
+ style: {
82
+ head: ['cyan'],
83
+ border: ['gray']
84
+ }
85
+ });
86
+
87
+ whitelist.forEach(ip => {
88
+ table.push([chalk.green(ip)]);
89
+ });
90
+
91
+ console.log(table.toString());
92
+ console.log();
93
+ } catch (error) {
94
+ spinner.fail(chalk.red('Failed to fetch IP whitelist'));
95
+ console.error(chalk.red(`\nāœ— ${error.response?.data?.msg || error.message}\n`));
96
+ process.exit(1);
97
+ }
98
+ }
99
+
100
+ // Clear all whitelisted IPs
101
+ async function clear(tunnelId) {
102
+ const spinner = ora('Clearing IP whitelist...').start();
103
+
104
+ try {
105
+ await api.updateIPWhitelist(tunnelId, []);
106
+
107
+ spinner.succeed(chalk.green('IP whitelist cleared'));
108
+ console.log(chalk.gray('\nAll IPs are now allowed.\n'));
109
+ } catch (error) {
110
+ spinner.fail(chalk.red('Failed to clear IP whitelist'));
111
+ console.error(chalk.red(`\nāœ— ${error.response?.data?.msg || error.message}\n`));
112
+ process.exit(1);
113
+ }
114
+ }
115
+
116
+ module.exports = {
117
+ add,
118
+ remove,
119
+ list,
120
+ clear
121
+ };
package/commands/login.js CHANGED
@@ -1,12 +1,38 @@
1
1
  const inquirer = require('inquirer');
2
2
  const chalk = require('chalk');
3
3
  const ora = require('ora');
4
+ const open = require('open');
4
5
  const api = require('../utils/api');
5
6
  const config = require('../utils/config');
6
7
 
7
8
  async function login(options) {
8
9
  console.log(chalk.blue.bold('\nšŸ” API Response Manager - Login\n'));
9
10
 
11
+ // Check if social login is requested
12
+ if (options.provider) {
13
+ return await socialLogin(options.provider);
14
+ }
15
+
16
+ // Ask for login method
17
+ const { method } = await inquirer.prompt([
18
+ {
19
+ type: 'list',
20
+ name: 'method',
21
+ message: 'Choose login method:',
22
+ choices: [
23
+ { name: 'šŸ“§ Email & Password', value: 'email' },
24
+ { name: '🌐 Google', value: 'google' },
25
+ { name: 'šŸ™ GitHub', value: 'github' },
26
+ { name: '🪟 Microsoft', value: 'microsoft' }
27
+ ]
28
+ }
29
+ ]);
30
+
31
+ if (method !== 'email') {
32
+ return await socialLogin(method);
33
+ }
34
+
35
+ // Traditional email/password login
10
36
  let email = options.email;
11
37
  let password = options.password;
12
38
 
@@ -85,4 +111,100 @@ async function login(options) {
85
111
  }
86
112
  }
87
113
 
114
+ async function socialLogin(provider) {
115
+ console.log(chalk.blue(`\n🌐 Logging in with ${provider}...\n`));
116
+
117
+ const spinner = ora('Initiating OAuth flow...').start();
118
+
119
+ try {
120
+ // Request device code from backend
121
+ const deviceResponse = await api.requestDeviceCode(provider);
122
+
123
+ spinner.stop();
124
+
125
+ const { device_code, user_code, verification_uri, expires_in } = deviceResponse;
126
+
127
+ console.log(chalk.yellow('\nšŸ“‹ Please complete authentication:\n'));
128
+ console.log(chalk.white(' 1. Visit:'), chalk.cyan.underline(verification_uri));
129
+ console.log(chalk.white(' 2. Enter code:'), chalk.green.bold(user_code));
130
+ console.log(chalk.gray(`\n Code expires in ${expires_in} seconds\n`));
131
+
132
+ // Open browser automatically
133
+ const { openBrowser } = await inquirer.prompt([
134
+ {
135
+ type: 'confirm',
136
+ name: 'openBrowser',
137
+ message: 'Open browser automatically?',
138
+ default: true
139
+ }
140
+ ]);
141
+
142
+ if (openBrowser) {
143
+ await open(verification_uri);
144
+ console.log(chalk.gray(' āœ“ Browser opened\n'));
145
+ }
146
+
147
+ const pollSpinner = ora('Waiting for authentication...').start();
148
+
149
+ // Poll for token
150
+ const pollInterval = 5000; // 5 seconds
151
+ const maxAttempts = Math.ceil(expires_in / (pollInterval / 1000));
152
+ let attempts = 0;
153
+
154
+ const pollForToken = async () => {
155
+ while (attempts < maxAttempts) {
156
+ attempts++;
157
+
158
+ try {
159
+ const tokenResponse = await api.pollDeviceToken(device_code, provider);
160
+
161
+ if (tokenResponse.token) {
162
+ pollSpinner.succeed(chalk.green('Authentication successful!'));
163
+
164
+ // Store credentials
165
+ api.setToken(tokenResponse.token);
166
+
167
+ const userId = tokenResponse.user?._id || tokenResponse.user?.id;
168
+ const userEmail = tokenResponse.user?.email;
169
+ const userName = tokenResponse.user?.name;
170
+
171
+ if (userId) config.set('userId', userId);
172
+ if (userEmail) config.set('email', userEmail);
173
+
174
+ console.log(chalk.gray('\nUser:'), chalk.white(userName || userEmail));
175
+ console.log(chalk.gray('Provider:'), chalk.white(provider));
176
+ console.log(chalk.gray('Token saved to:'), chalk.white(config.path));
177
+ console.log(chalk.green('\nāœ“ You can now use all ARM CLI commands\n'));
178
+
179
+ return;
180
+ }
181
+ } catch (error) {
182
+ if (error.response?.status === 428) {
183
+ // Still pending, continue polling
184
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
185
+ continue;
186
+ }
187
+ throw error;
188
+ }
189
+ }
190
+
191
+ throw new Error('Authentication timeout - please try again');
192
+ };
193
+
194
+ await pollForToken();
195
+
196
+ } catch (error) {
197
+ spinner.stop();
198
+ console.error(chalk.red('\nāœ— Social login failed'));
199
+
200
+ if (error.response?.data?.msg) {
201
+ console.error(chalk.red(`āœ— ${error.response.data.msg}\n`));
202
+ } else {
203
+ console.error(chalk.red(`āœ— ${error.message}\n`));
204
+ }
205
+
206
+ process.exit(1);
207
+ }
208
+ }
209
+
88
210
  module.exports = login;
@@ -0,0 +1,69 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const api = require('../utils/api');
4
+
5
+ // Configure rate limits
6
+ async function configure(tunnelId, options) {
7
+ const spinner = ora('Updating rate limits...').start();
8
+
9
+ try {
10
+ // Get current tunnel
11
+ const tunnel = await api.getTunnel(tunnelId);
12
+ const currentRateLimit = tunnel.tunnel.rateLimit || {};
13
+
14
+ // Build update object
15
+ const updates = {
16
+ rateLimit: {
17
+ enabled: options.disable ? false : (options.enable ? true : currentRateLimit.enabled),
18
+ requestsPerMinute: options.rpm || currentRateLimit.requestsPerMinute,
19
+ requestsPerHour: options.rph || currentRateLimit.requestsPerHour,
20
+ requestsPerDay: options.rpd || currentRateLimit.requestsPerDay
21
+ }
22
+ };
23
+
24
+ await api.updateTunnel(tunnelId, updates);
25
+
26
+ spinner.succeed(chalk.green('Rate limits updated successfully'));
27
+
28
+ console.log(chalk.blue.bold('\n⚔ Rate Limit Configuration\n'));
29
+ console.log(chalk.gray('ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”'));
30
+ console.log(chalk.gray('│') + chalk.white(' Status: ') + (updates.rateLimit.enabled ? chalk.green('Enabled') : chalk.red('Disabled')).padEnd(28) + chalk.gray('│'));
31
+ console.log(chalk.gray('│') + chalk.white(' Per Minute: ') + chalk.cyan(String(updates.rateLimit.requestsPerMinute)).padEnd(20) + chalk.gray('│'));
32
+ console.log(chalk.gray('│') + chalk.white(' Per Hour: ') + chalk.cyan(String(updates.rateLimit.requestsPerHour)).padEnd(20) + chalk.gray('│'));
33
+ console.log(chalk.gray('│') + chalk.white(' Per Day: ') + chalk.cyan(String(updates.rateLimit.requestsPerDay)).padEnd(20) + chalk.gray('│'));
34
+ console.log(chalk.gray('ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜\n'));
35
+ } catch (error) {
36
+ spinner.fail(chalk.red('Failed to update rate limits'));
37
+ console.error(chalk.red(`\nāœ— ${error.response?.data?.msg || error.message}\n`));
38
+ process.exit(1);
39
+ }
40
+ }
41
+
42
+ // Show current rate limits
43
+ async function show(tunnelId) {
44
+ const spinner = ora('Fetching rate limits...').start();
45
+
46
+ try {
47
+ const tunnel = await api.getTunnel(tunnelId);
48
+ const rateLimit = tunnel.tunnel.rateLimit || {};
49
+
50
+ spinner.stop();
51
+
52
+ console.log(chalk.blue.bold('\n⚔ Rate Limit Configuration\n'));
53
+ console.log(chalk.gray('ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”'));
54
+ console.log(chalk.gray('│') + chalk.white(' Status: ') + (rateLimit.enabled ? chalk.green('Enabled') : chalk.red('Disabled')).padEnd(28) + chalk.gray('│'));
55
+ console.log(chalk.gray('│') + chalk.white(' Per Minute: ') + chalk.cyan(String(rateLimit.requestsPerMinute || 60)).padEnd(20) + chalk.gray('│'));
56
+ console.log(chalk.gray('│') + chalk.white(' Per Hour: ') + chalk.cyan(String(rateLimit.requestsPerHour || 1000)).padEnd(20) + chalk.gray('│'));
57
+ console.log(chalk.gray('│') + chalk.white(' Per Day: ') + chalk.cyan(String(rateLimit.requestsPerDay || 10000)).padEnd(20) + chalk.gray('│'));
58
+ console.log(chalk.gray('ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜\n'));
59
+ } catch (error) {
60
+ spinner.fail(chalk.red('Failed to fetch rate limits'));
61
+ console.error(chalk.red(`\nāœ— ${error.response?.data?.msg || error.message}\n`));
62
+ process.exit(1);
63
+ }
64
+ }
65
+
66
+ module.exports = {
67
+ configure,
68
+ show
69
+ };
@@ -21,6 +21,9 @@ async function start(port, options) {
21
21
  name: options.name || `Tunnel-${port}`,
22
22
  subdomain: options.subdomain,
23
23
  localPort: parseInt(port),
24
+ protocol: options.protocol || 'http',
25
+ sslEnabled: options.ssl || options.protocol === 'https',
26
+ customDomain: options.domain,
24
27
  security: {
25
28
  requireAuth: options.auth || false
26
29
  },
@@ -222,9 +225,147 @@ async function logs(tunnelId, options) {
222
225
  }
223
226
  }
224
227
 
228
+ // Set custom domain
229
+ async function setDomain(tunnelId, domain) {
230
+ const spinner = ora('Setting custom domain...').start();
231
+
232
+ try {
233
+ await api.setTunnelDomain(tunnelId, domain);
234
+ spinner.succeed(chalk.green('Custom domain set successfully'));
235
+ console.log(chalk.cyan(`\nāœ“ Tunnel now accessible at: https://${domain}\n`));
236
+ } catch (error) {
237
+ spinner.fail(chalk.red('Failed to set custom domain'));
238
+ console.error(chalk.red(`\nāœ— ${error.response?.data?.msg || error.message}\n`));
239
+ process.exit(1);
240
+ }
241
+ }
242
+
243
+ // Upload SSL certificate
244
+ async function uploadSSL(tunnelId, options) {
245
+ const spinner = ora('Uploading SSL certificate...').start();
246
+
247
+ try {
248
+ const fs = require('fs');
249
+ const cert = fs.readFileSync(options.cert, 'utf8');
250
+ const key = fs.readFileSync(options.key, 'utf8');
251
+ const ca = options.ca ? fs.readFileSync(options.ca, 'utf8') : null;
252
+
253
+ await api.uploadTunnelSSL(tunnelId, { cert, key, ca });
254
+ spinner.succeed(chalk.green('SSL certificate uploaded successfully'));
255
+ console.log();
256
+ } catch (error) {
257
+ spinner.fail(chalk.red('Failed to upload SSL certificate'));
258
+ console.error(chalk.red(`\nāœ— ${error.response?.data?.msg || error.message}\n`));
259
+ process.exit(1);
260
+ }
261
+ }
262
+
263
+ // Configure OAuth
264
+ async function configureOAuth(tunnelId, options) {
265
+ const spinner = ora('Configuring OAuth...').start();
266
+
267
+ try {
268
+ await api.configureTunnelOAuth(tunnelId, {
269
+ provider: options.provider,
270
+ clientId: options.clientId,
271
+ clientSecret: options.clientSecret,
272
+ callbackUrl: options.callbackUrl,
273
+ scope: options.scope ? options.scope.split(',') : ['openid', 'email', 'profile']
274
+ });
275
+ spinner.succeed(chalk.green('OAuth configured successfully'));
276
+ console.log();
277
+ } catch (error) {
278
+ spinner.fail(chalk.red('Failed to configure OAuth'));
279
+ console.error(chalk.red(`\nāœ— ${error.response?.data?.msg || error.message}\n`));
280
+ process.exit(1);
281
+ }
282
+ }
283
+
284
+ // Configure OIDC
285
+ async function configureOIDC(tunnelId, options) {
286
+ const spinner = ora('Configuring OIDC...').start();
287
+
288
+ try {
289
+ await api.configureTunnelOIDC(tunnelId, {
290
+ issuer: options.issuer,
291
+ clientId: options.clientId,
292
+ clientSecret: options.clientSecret,
293
+ callbackUrl: options.callbackUrl
294
+ });
295
+ spinner.succeed(chalk.green('OIDC configured successfully'));
296
+ console.log();
297
+ } catch (error) {
298
+ spinner.fail(chalk.red('Failed to configure OIDC'));
299
+ console.error(chalk.red(`\nāœ— ${error.response?.data?.msg || error.message}\n`));
300
+ process.exit(1);
301
+ }
302
+ }
303
+
304
+ // Configure SAML
305
+ async function configureSAML(tunnelId, options) {
306
+ const spinner = ora('Configuring SAML...').start();
307
+
308
+ try {
309
+ const fs = require('fs');
310
+ const cert = fs.readFileSync(options.cert, 'utf8');
311
+
312
+ await api.configureTunnelSAML(tunnelId, {
313
+ entryPoint: options.entryPoint,
314
+ issuer: options.issuer,
315
+ cert,
316
+ callbackUrl: options.callbackUrl
317
+ });
318
+ spinner.succeed(chalk.green('SAML configured successfully'));
319
+ console.log();
320
+ } catch (error) {
321
+ spinner.fail(chalk.red('Failed to configure SAML'));
322
+ console.error(chalk.red(`\nāœ— ${error.response?.data?.msg || error.message}\n`));
323
+ process.exit(1);
324
+ }
325
+ }
326
+
327
+ // Configure ingress
328
+ async function configureIngress(tunnelId, rules, options) {
329
+ const spinner = ora('Configuring ingress...').start();
330
+
331
+ try {
332
+ // Parse rules: "/api=localhost:3000,/admin=localhost:4000"
333
+ const parsedRules = rules.split(',').map(rule => {
334
+ const [path, backend] = rule.split('=');
335
+ const [host, port] = backend.split(':');
336
+ return {
337
+ path: path.trim(),
338
+ pathType: 'Prefix',
339
+ backend: {
340
+ host: host.trim(),
341
+ port: parseInt(port)
342
+ }
343
+ };
344
+ });
345
+
346
+ await api.configureTunnelIngress(tunnelId, {
347
+ enabled: true,
348
+ rules: parsedRules,
349
+ tls: { enabled: options.tls || false }
350
+ });
351
+ spinner.succeed(chalk.green('Ingress configured successfully'));
352
+ console.log();
353
+ } catch (error) {
354
+ spinner.fail(chalk.red('Failed to configure ingress'));
355
+ console.error(chalk.red(`\nāœ— ${error.response?.data?.msg || error.message}\n`));
356
+ process.exit(1);
357
+ }
358
+ }
359
+
225
360
  module.exports = {
226
361
  start,
227
362
  list,
228
363
  stop,
229
- logs
364
+ logs,
365
+ setDomain,
366
+ uploadSSL,
367
+ configureOAuth,
368
+ configureOIDC,
369
+ configureSAML,
370
+ configureIngress
230
371
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-response-manager",
3
- "version": "1.0.1",
3
+ "version": "2.3.1",
4
4
  "description": "Command-line interface for API Response Manager",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -8,7 +8,17 @@
8
8
  },
9
9
  "scripts": {
10
10
  "test": "jest",
11
- "link": "npm link"
11
+ "link": "npm link",
12
+ "prepublishOnly": "echo 'Publishing api-response-manager CLI...'"
13
+ },
14
+ "files": [
15
+ "bin/",
16
+ "commands/",
17
+ "utils/",
18
+ "README.md"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
12
22
  },
13
23
  "keywords": [
14
24
  "api",
@@ -21,17 +31,18 @@
21
31
  "author": "Vijay Singh Purohit",
22
32
  "license": "MIT",
23
33
  "dependencies": {
24
- "commander": "^11.1.0",
25
34
  "axios": "^1.6.2",
35
+ "boxen": "^5.1.2",
26
36
  "chalk": "^4.1.2",
37
+ "cli-table3": "^0.6.3",
38
+ "commander": "^11.1.0",
39
+ "conf": "^10.2.0",
40
+ "dotenv": "^16.3.1",
27
41
  "inquirer": "^8.2.5",
42
+ "open": "^8.4.0",
28
43
  "ora": "^5.4.1",
29
- "ws": "^8.14.2",
30
- "dotenv": "^16.3.1",
31
- "conf": "^10.2.0",
32
- "boxen": "^5.1.2",
33
- "cli-table3": "^0.6.3",
34
- "update-notifier": "^6.0.2"
44
+ "update-notifier": "^6.0.2",
45
+ "ws": "^8.14.2"
35
46
  },
36
47
  "devDependencies": {
37
48
  "jest": "^29.7.0"
@@ -46,5 +57,6 @@
46
57
  "bugs": {
47
58
  "url": "https://github.com/vijaypurohit322/api-response-manager/issues"
48
59
  },
49
- "homepage": "https://github.com/vijaypurohit322/api-response-manager#readme"
60
+ "homepage": "https://github.com/vijaypurohit322/api-response-manager#readme",
61
+ "preferGlobal": true
50
62
  }
package/utils/api.js CHANGED
@@ -58,6 +58,20 @@ class APIClient {
58
58
  return response.data;
59
59
  }
60
60
 
61
+ // OAuth Device Flow for CLI
62
+ async requestDeviceCode(provider) {
63
+ const response = await this.client.post('/auth/device/code', { provider });
64
+ return response.data;
65
+ }
66
+
67
+ async pollDeviceToken(deviceCode, provider) {
68
+ const response = await this.client.post('/auth/device/token', {
69
+ device_code: deviceCode,
70
+ provider
71
+ });
72
+ return response.data;
73
+ }
74
+
61
75
  // Tunnels
62
76
  async createTunnel(data) {
63
77
  const response = await this.client.post('/tunnels', data);
@@ -140,6 +154,54 @@ class APIClient {
140
154
  const response = await this.client.get(`/projects/${id}/responses`, { params });
141
155
  return response.data;
142
156
  }
157
+
158
+ // Advanced Tunnel Features
159
+ async setTunnelDomain(id, domain) {
160
+ const response = await this.client.post(`/tunnels/${id}/custom-domain`, { domain });
161
+ return response.data;
162
+ }
163
+
164
+ async uploadTunnelSSL(id, data) {
165
+ const response = await this.client.post(`/tunnels/${id}/ssl`, data);
166
+ return response.data;
167
+ }
168
+
169
+ async configureTunnelOAuth(id, data) {
170
+ const response = await this.client.post(`/tunnels/${id}/auth/oauth`, data);
171
+ return response.data;
172
+ }
173
+
174
+ async configureTunnelOIDC(id, data) {
175
+ const response = await this.client.post(`/tunnels/${id}/auth/oidc`, data);
176
+ return response.data;
177
+ }
178
+
179
+ async configureTunnelSAML(id, data) {
180
+ const response = await this.client.post(`/tunnels/${id}/auth/saml`, data);
181
+ return response.data;
182
+ }
183
+
184
+ async configureTunnelIngress(id, data) {
185
+ const response = await this.client.post(`/tunnels/${id}/ingress`, data);
186
+ return response.data;
187
+ }
188
+
189
+ // IP Management
190
+ async updateIPWhitelist(id, ipWhitelist) {
191
+ const response = await this.client.put(`/tunnels/${id}/ip-whitelist`, { ipWhitelist });
192
+ return response.data;
193
+ }
194
+
195
+ async updateIPBlacklist(id, ipBlacklist) {
196
+ const response = await this.client.put(`/tunnels/${id}/ip-blacklist`, { ipBlacklist });
197
+ return response.data;
198
+ }
199
+
200
+ // Update Tunnel
201
+ async updateTunnel(id, updates) {
202
+ const response = await this.client.put(`/tunnels/${id}`, updates);
203
+ return response.data;
204
+ }
143
205
  }
144
206
 
145
207
  module.exports = new APIClient();