nport 2.0.5 → 2.0.7

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/CHANGELOG.md CHANGED
@@ -5,6 +5,131 @@ All notable changes to NPort will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.0.7] - 2026-01-14
9
+
10
+ ### Added
11
+ - 🌐 **Smart Network Warning System**: Intelligent handling of QUIC/network connectivity issues
12
+ - Automatic detection and filtering of QUIC protocol errors
13
+ - User-friendly warning messages instead of scary red error spam
14
+ - Bilingual support (English & Vietnamese)
15
+ - Smart throttling: Shows warning only after 5 errors, max once per 30 seconds
16
+ - Clear explanations of what's happening and how to fix it
17
+ - Automatic reset when connection is restored
18
+ - 🔒 **Protected Subdomain Support**: Enhanced error handling for reserved subdomains
19
+ - User-friendly error message when trying to create protected subdomains (like `api`)
20
+ - Formatted error output matching existing error style
21
+ - Helpful suggestions to use alternative subdomain names
22
+ - Prevents accidental use of backend service subdomains
23
+
24
+ ### Improved
25
+ - 🔇 **Cleaner Terminal Output**: No more error spam from cloudflared
26
+ - QUIC timeout errors are now silently tracked instead of displayed
27
+ - Network warnings filtered: "failed to accept QUIC stream", "timeout: no recent network activity", etc.
28
+ - Only shows meaningful warnings when there's an actual persistent issue
29
+ - Terminal stays clean and readable during normal operation
30
+ - 📡 **Better User Communication**: Context-aware network issue reporting
31
+ - Explains that QUIC failures are usually not critical
32
+ - Tunnel continues working via HTTP/2 fallback
33
+ - Provides actionable troubleshooting steps
34
+ - Reassures users that the tunnel is still functional
35
+ - ✨ **Better Error Messages**: Enhanced user feedback for protected subdomains
36
+ - Catches `SUBDOMAIN_PROTECTED` errors from backend
37
+ - Formats error messages consistently with other error types
38
+ - Shows actionable options when subdomain is reserved
39
+
40
+ ### Technical Details
41
+ - **Network Error Patterns**: Added detection for 7 common QUIC/network error patterns
42
+ - **State Management**: New network issue tracking in application state
43
+ - `networkIssueCount`: Counter for network errors
44
+ - `lastNetworkWarningTime`: Timestamp tracking for cooldown
45
+ - `shouldShowNetworkWarning()`: Smart decision logic
46
+ - **Configuration**: New `NETWORK_CONFIG` with threshold and cooldown settings
47
+ - **Bilingual Messages**: Complete translations for all network warning messages
48
+ - **Protected Subdomain Handling**: Enhanced error handling in `src/api.js`
49
+ - Added check for `SUBDOMAIN_PROTECTED` error type
50
+ - Consistent formatting with existing error messages
51
+ - Clear user guidance for alternative subdomain choices
52
+
53
+ ### User Experience
54
+ **Before:**
55
+ ```
56
+ [Cloudflared] 2026-01-14T04:33:02Z ERR failed to accept QUIC stream...
57
+ [Cloudflared] 2026-01-14T04:33:03Z ERR failed to accept QUIC stream...
58
+ [Cloudflared] 2026-01-14T04:33:04Z ERR failed to accept QUIC stream...
59
+ [Cloudflared] 2026-01-14T04:33:05Z ERR failed to accept QUIC stream...
60
+ ```
61
+
62
+ **After:**
63
+ ```
64
+ ✔ [1/2] Connection established...
65
+ ✔ [2/2] Compression enabled...
66
+
67
+ âš ī¸ NETWORK CONNECTIVITY ISSUE DETECTED
68
+ Cloudflared is having trouble maintaining a stable connection...
69
+ 📡 Your tunnel is still working, but connection quality may be affected.
70
+
71
+ 💡 Possible reasons:
72
+ â€ĸ Unstable internet connection or high packet loss
73
+ â€ĸ Firewall/Router blocking UDP traffic (QUIC protocol)
74
+ â€ĸ ISP throttling or network congestion
75
+
76
+ 🔧 What to try:
77
+ â€ĸ Check your internet connection stability
78
+ â€ĸ Try connecting from a different network
79
+ â€ĸ Disable VPN/Proxy if you're using one
80
+
81
+ â„šī¸ This is usually not critical - your tunnel should continue working normally.
82
+ ```
83
+
84
+ ## [2.0.6] - 2026-01-13
85
+
86
+ ### Added
87
+ - 🔧 **Backend URL Configuration**: Full control over backend server
88
+ - `--backend` / `-b` flag for temporary backend override
89
+ - `--set-backend` command to save backend URL permanently
90
+ - `NPORT_BACKEND_URL` environment variable support
91
+ - Saved backend configuration persists across sessions
92
+ - Priority system: CLI flag > Saved config > Env var > Default
93
+ - đŸ—‚ī¸ **Unified Configuration System**: All settings in one place
94
+ - New centralized `config-manager.js` module
95
+ - All preferences stored in `~/.nport/config.json`
96
+ - Automatic migration from old format (v2.0.5)
97
+ - Easy to read and manually edit JSON format
98
+ - 🌐 **New Default Backend**: Updated to `api.nport.link`
99
+ - Professional domain structure
100
+ - Better branding alignment
101
+ - Shorter and easier to remember
102
+
103
+ ### Changed
104
+ - 📝 **Language Configuration**: Now uses unified config system
105
+ - Language setting moved from `~/.nport/lang` to `~/.nport/config.json`
106
+ - Automatic migration from old file format
107
+ - Consistent configuration approach across all settings
108
+ - 📚 **Documentation Updates**: Complete overhaul
109
+ - Updated `README.md` with backend configuration options
110
+ - New `CLIENT_SETUP.md` focused on npm installation and backend setup
111
+ - Comprehensive backend URL documentation
112
+ - Clear priority order explanation
113
+
114
+ ### Improved
115
+ - đŸŽ¯ **Consistency**: Unified approach to all configuration
116
+ - Backend URL and language now use same storage system
117
+ - Single config file for all preferences
118
+ - Cleaner architecture and code organization
119
+ - 💾 **Configuration File Structure**:
120
+ ```json
121
+ {
122
+ "backendUrl": "https://api.nport.link",
123
+ "language": "en"
124
+ }
125
+ ```
126
+
127
+ ### Migration
128
+ - Automatic migration from v2.0.5 configuration files
129
+ - Old `~/.nport/lang` file automatically migrated to `config.json`
130
+ - No manual steps required
131
+ - Old files removed after successful migration
132
+
8
133
  ## [2.0.5] - 2026-01-13
9
134
 
10
135
  ### Added
@@ -61,6 +186,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
61
186
 
62
187
  ## Version Upgrade Guide
63
188
 
189
+ ### From 2.0.6 to 2.0.7
190
+
191
+ ```bash
192
+ npm install -g nport@latest
193
+ ```
194
+
195
+ **What's New:**
196
+
197
+ 1. **Cleaner Terminal Experience**
198
+ - No more scary red QUIC error spam
199
+ - Smart network warnings when needed
200
+ - Automatic fallback to HTTP/2 when QUIC fails
201
+
202
+ 2. **Better Error Communication**
203
+ - Understand what's happening with your connection
204
+ - Clear explanations in your language (EN/VI)
205
+ - Actionable troubleshooting steps
206
+
207
+ 3. **When You'll See Warnings**
208
+ - Only after multiple network issues (not just one)
209
+ - Maximum once every 30 seconds (no spam)
210
+ - Automatically disappears when connection improves
211
+
212
+ **Breaking Changes:** None - fully backward compatible!
213
+
214
+ ### From 2.0.5 to 2.0.6
215
+
216
+ ```bash
217
+ npm install -g nport@latest
218
+ ```
219
+
220
+ **New Features to Try:**
221
+
222
+ 1. **Set Your Backend Permanently**
223
+ ```bash
224
+ nport --set-backend https://api.nport.link
225
+ ```
226
+
227
+ 2. **Use Custom Backend Temporarily**
228
+ ```bash
229
+ nport 3000 -b https://your-backend.com
230
+ ```
231
+
232
+ 3. **Check Current Configuration**
233
+ ```bash
234
+ cat ~/.nport/config.json
235
+ ```
236
+
237
+ 4. **Use Environment Variable**
238
+ ```bash
239
+ export NPORT_BACKEND_URL=https://your-backend.com
240
+ nport 3000
241
+ ```
242
+
243
+ **Breaking Changes:** None - fully backward compatible!
244
+
245
+ **Migration:** Your language preference from v2.0.5 will be automatically migrated to the new unified config format.
246
+
64
247
  ### From 2.0.4 to 2.0.5
65
248
 
66
249
  ```bash
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nick Pham
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
@@ -35,6 +35,17 @@ Perfect for:
35
35
 
36
36
  ## đŸ“Ļ Installation
37
37
 
38
+ ### Requirements
39
+
40
+ - **Node.js** >= 20.0.0
41
+ - **npm** >= 10.0.0
42
+
43
+ Check your versions:
44
+ ```bash
45
+ node --version
46
+ npm --version
47
+ ```
48
+
38
49
  ### NPM (Recommended)
39
50
 
40
51
  ```bash
@@ -158,6 +169,8 @@ nport <port> [options]
158
169
  |--------|-------|-------------|---------|
159
170
  | `<port>` | - | Local port to tunnel (default: 8080) | `nport 3000` |
160
171
  | `--subdomain` | `-s` | Custom subdomain | `nport 3000 -s myapp` |
172
+ | `--backend` | `-b` | Custom backend URL (temporary) | `nport 3000 -b https://your-backend.com` |
173
+ | `--set-backend` | - | Save backend URL permanently | `nport --set-backend https://your-backend.com` |
161
174
  | `--language` | `-l` | Set language (en/vi) or prompt | `nport 3000 -l vi` |
162
175
  | `--version` | `-v` | Show version information | `nport -v` |
163
176
 
@@ -177,6 +190,61 @@ nport -l # Interactive prompt
177
190
 
178
191
  On first run or when using `--language` without a value, you'll see an interactive language picker. Your choice is automatically saved for future sessions.
179
192
 
193
+ ### Backend URL Options
194
+
195
+ NPort uses a default backend at `https://api.nport.link`, but you can use your own backend server.
196
+
197
+ #### Temporary Backend (One-time Use)
198
+
199
+ Use a custom backend for just the current session:
200
+
201
+ ```bash
202
+ # Use custom backend via CLI flag
203
+ nport 3000 --backend https://your-backend.com
204
+ nport 3000 -b https://your-backend.com
205
+
206
+ # Use custom backend via environment variable
207
+ export NPORT_BACKEND_URL=https://your-backend.com
208
+ nport 3000
209
+
210
+ # Combine with other options
211
+ nport 3000 -s myapp -b https://your-backend.com
212
+ ```
213
+
214
+ #### Persistent Backend (Saved Configuration)
215
+
216
+ Save a backend URL to use automatically in all future sessions:
217
+
218
+ ```bash
219
+ # Save backend URL permanently
220
+ nport --set-backend https://your-backend.com
221
+
222
+ # Now all future commands will use this backend
223
+ nport 3000 # Uses saved backend
224
+ nport 3000 -s myapp # Uses saved backend
225
+
226
+ # Clear saved backend (return to default)
227
+ nport --set-backend
228
+
229
+ # Override saved backend temporarily
230
+ nport 3000 -b https://different-backend.com
231
+ ```
232
+
233
+ **Configuration Priority:**
234
+ 1. CLI flag (`--backend` or `-b`) - Highest priority
235
+ 2. Saved config (`--set-backend`)
236
+ 3. Environment variable (`NPORT_BACKEND_URL`)
237
+ 4. Default (`https://api.nport.link`) - Lowest priority
238
+
239
+ **Configuration Storage:**
240
+ Your backend preference is saved in `~/.nport/config.json`
241
+
242
+ This is useful if you want to:
243
+ - **Self-host**: Run your own NPort backend (see [server/](server/) directory)
244
+ - **Development**: Test against a local backend
245
+ - **Custom domains**: Use your own domain for tunnel URLs
246
+ - **Enterprise**: Use a company-hosted backend server
247
+
180
248
  ### Version Information
181
249
 
182
250
  ```bash
@@ -349,8 +417,12 @@ If you find NPort useful, please consider supporting the project:
349
417
  ## 🙏 Credits
350
418
 
351
419
  - Created by [Nick Pham](https://github.com/tuanngocptn) from Vietnam
352
- - Powered by [Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/)
353
420
  - Inspired by [ngrok](https://ngrok.com) and [localtunnel](https://github.com/localtunnel/localtunnel)
421
+ - Powered by [Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/)
422
+
423
+ <a href="https://www.cloudflare.com" target="_blank">
424
+ <img src="https://nport.link/assets/webp/cloudflare-logo-black.webp" alt="Cloudflare" height="60">
425
+ </a>
354
426
 
355
427
  ## 🔗 Links
356
428
 
package/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import ora from "ora";
4
+ import chalk from "chalk";
4
5
  import { ArgumentParser } from "./src/args.js";
5
6
  import { TunnelOrchestrator } from "./src/tunnel.js";
6
7
  import { VersionManager } from "./src/version.js";
7
8
  import { UI } from "./src/ui.js";
8
9
  import { CONFIG } from "./src/config.js";
9
10
  import { lang } from "./src/lang.js";
11
+ import { configManager } from "./src/config-manager.js";
10
12
 
11
13
  /**
12
14
  * NPort - Free & Open Source ngrok Alternative
@@ -26,6 +28,32 @@ async function displayVersion() {
26
28
  UI.displayVersion(CONFIG.CURRENT_VERSION, updateInfo);
27
29
  }
28
30
 
31
+ /**
32
+ * Handle --set-backend command
33
+ */
34
+ function handleSetBackend(value) {
35
+ if (value === 'clear') {
36
+ // Clear saved backend URL
37
+ configManager.setBackendUrl(null);
38
+ console.log(chalk.green('✔ Backend URL cleared. Using default backend.'));
39
+ console.log(chalk.gray(' Default: https://api.nport.link\n'));
40
+ } else {
41
+ // Save new backend URL
42
+ configManager.setBackendUrl(value);
43
+ console.log(chalk.green('✔ Backend URL saved successfully!'));
44
+ console.log(chalk.cyan(` Backend: ${value}`));
45
+ console.log(chalk.gray('\n This backend will be used for all future sessions.'));
46
+ console.log(chalk.gray(' To clear: nport --set-backend\n'));
47
+ }
48
+
49
+ // Show current configuration
50
+ const savedUrl = configManager.getBackendUrl();
51
+ if (savedUrl) {
52
+ console.log(chalk.white('Current configuration:'));
53
+ console.log(chalk.cyan(` Saved backend: ${savedUrl}`));
54
+ }
55
+ }
56
+
29
57
  /**
30
58
  * Main application entry point
31
59
  */
@@ -45,6 +73,12 @@ async function main() {
45
73
  process.exit(0);
46
74
  }
47
75
 
76
+ // Handle --set-backend command
77
+ if (config.setBackend) {
78
+ handleSetBackend(config.setBackend);
79
+ process.exit(0);
80
+ }
81
+
48
82
  // If only --language flag was used, show success message and exit
49
83
  if (config.language === 'prompt' &&
50
84
  (args.includes('--language') || args.includes('--lang') || args.includes('-l'))) {
@@ -52,6 +86,14 @@ async function main() {
52
86
  process.exit(0);
53
87
  }
54
88
 
89
+ // Load saved backend URL if no CLI backend specified
90
+ if (!config.backendUrl) {
91
+ const savedBackend = configManager.getBackendUrl();
92
+ if (savedBackend) {
93
+ config.backendUrl = savedBackend;
94
+ }
95
+ }
96
+
55
97
  // Start tunnel
56
98
  await TunnelOrchestrator.start(config);
57
99
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nport",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "Free & open source ngrok alternative - Tunnel HTTP/HTTPS connections via Cloudflare Edge with custom subdomains",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -8,8 +8,8 @@
8
8
  "nport": "index.js"
9
9
  },
10
10
  "engines": {
11
- "node": ">=18.0.0",
12
- "npm": ">=8.0.0"
11
+ "node": ">=20.0.0",
12
+ "npm": ">=10.0.0"
13
13
  },
14
14
  "keywords": [
15
15
  "ngrok",
package/src/api.js CHANGED
@@ -8,9 +8,10 @@ import { state } from "./state.js";
8
8
  * Handles communication with the NPort backend service
9
9
  */
10
10
  export class APIClient {
11
- static async createTunnel(subdomain) {
11
+ static async createTunnel(subdomain, backendUrl = null) {
12
+ const url = backendUrl || CONFIG.BACKEND_URL;
12
13
  try {
13
- const { data } = await axios.post(CONFIG.BACKEND_URL, { subdomain });
14
+ const { data } = await axios.post(url, { subdomain });
14
15
 
15
16
  if (!data.success) {
16
17
  throw new Error(data.error || "Unknown error from backend");
@@ -26,8 +27,9 @@ export class APIClient {
26
27
  }
27
28
  }
28
29
 
29
- static async deleteTunnel(subdomain, tunnelId) {
30
- await axios.delete(CONFIG.BACKEND_URL, {
30
+ static async deleteTunnel(subdomain, tunnelId, backendUrl = null) {
31
+ const url = backendUrl || CONFIG.BACKEND_URL;
32
+ await axios.delete(url, {
31
33
  data: { subdomain, tunnelId },
32
34
  });
33
35
  }
@@ -36,6 +38,21 @@ export class APIClient {
36
38
  if (error.response?.data?.error) {
37
39
  const errorMsg = error.response.data.error;
38
40
 
41
+ // Check for protected subdomain (reserved for production services)
42
+ if (errorMsg.includes("SUBDOMAIN_PROTECTED:")) {
43
+ return new Error(
44
+ `Subdomain "${subdomain}" is already taken or in use.\n\n` +
45
+ chalk.yellow(`💡 Try one of these options:\n`) +
46
+ chalk.gray(` 1. Choose a different subdomain: `) +
47
+ chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT} -s ${subdomain}-v2\n`) +
48
+ chalk.gray(` 2. Use a random subdomain: `) +
49
+ chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT}\n`) +
50
+ chalk.gray(
51
+ ` 3. Wait a few minutes and retry if you just stopped a tunnel with this name`
52
+ )
53
+ );
54
+ }
55
+
39
56
  // Check for subdomain in use (active tunnel)
40
57
  if (
41
58
  errorMsg.includes("SUBDOMAIN_IN_USE:") ||
package/src/args.js CHANGED
@@ -2,14 +2,16 @@ import { CONFIG } from "./config.js";
2
2
 
3
3
  /**
4
4
  * Command Line Argument Parser
5
- * Handles parsing of CLI arguments for port, subdomain, and language
5
+ * Handles parsing of CLI arguments for port, subdomain, language, and backend URL
6
6
  */
7
7
  export class ArgumentParser {
8
8
  static parse(argv) {
9
9
  const port = this.parsePort(argv);
10
10
  const subdomain = this.parseSubdomain(argv);
11
11
  const language = this.parseLanguage(argv);
12
- return { port, subdomain, language };
12
+ const backendUrl = this.parseBackendUrl(argv);
13
+ const setBackend = this.parseSetBackend(argv);
14
+ return { port, subdomain, language, backendUrl, setBackend };
13
15
  }
14
16
 
15
17
  static parsePort(argv) {
@@ -66,6 +68,43 @@ export class ArgumentParser {
66
68
  return null; // No language specified
67
69
  }
68
70
 
71
+ static parseBackendUrl(argv) {
72
+ // Try backend URL flag formats: --backend=url, --backend url, -b url
73
+ const formats = [
74
+ () => this.findFlagWithEquals(argv, "--backend="),
75
+ () => this.findFlagWithEquals(argv, "-b="),
76
+ () => this.findFlagWithValue(argv, "--backend"),
77
+ () => this.findFlagWithValue(argv, "-b"),
78
+ ];
79
+
80
+ for (const format of formats) {
81
+ const url = format();
82
+ if (url) return url;
83
+ }
84
+
85
+ return null; // No backend URL specified
86
+ }
87
+
88
+ static parseSetBackend(argv) {
89
+ // Try set-backend flag formats: --set-backend=url, --set-backend url
90
+ const formats = [
91
+ () => this.findFlagWithEquals(argv, "--set-backend="),
92
+ () => this.findFlagWithValue(argv, "--set-backend"),
93
+ ];
94
+
95
+ for (const format of formats) {
96
+ const url = format();
97
+ if (url) return url;
98
+ }
99
+
100
+ // Check if --set-backend flag exists without value (to clear)
101
+ if (argv.includes('--set-backend')) {
102
+ return 'clear';
103
+ }
104
+
105
+ return null; // No set-backend specified
106
+ }
107
+
69
108
  static findFlagWithEquals(argv, flag) {
70
109
  const arg = argv.find((a) => a.startsWith(flag));
71
110
  return arg ? arg.split("=")[1] : null;
package/src/binary.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "child_process";
2
2
  import chalk from "chalk";
3
3
  import fs from "fs";
4
- import { LOG_PATTERNS } from "./config.js";
4
+ import { LOG_PATTERNS, NETWORK_CONFIG } from "./config.js";
5
5
  import { state } from "./state.js";
6
6
  import { UI } from "./ui.js";
7
7
  import { lang } from "./lang.js";
@@ -52,11 +52,19 @@ export class BinaryManager {
52
52
  return;
53
53
  }
54
54
 
55
+ // Check for network warnings (QUIC/connectivity issues)
56
+ if (LOG_PATTERNS.NETWORK_WARNING.some((pattern) => msg.includes(pattern))) {
57
+ this.handleNetworkWarning(msg);
58
+ return; // Don't show the raw error to user
59
+ }
60
+
55
61
  // Show success messages with connection count
56
62
  if (LOG_PATTERNS.SUCCESS.some((pattern) => msg.includes(pattern))) {
57
63
  const count = state.incrementConnection();
58
64
 
65
+ // Reset network issue count when connection succeeds
59
66
  if (count === 1) {
67
+ state.resetNetworkIssues();
60
68
  console.log(chalk.green(lang.t("connection1")));
61
69
  } else if (count === 4) {
62
70
  console.log(chalk.green(lang.t("connection2")));
@@ -72,6 +80,36 @@ export class BinaryManager {
72
80
  }
73
81
  }
74
82
 
83
+ static handleNetworkWarning(msg) {
84
+ const count = state.incrementNetworkIssue();
85
+
86
+ // Show user-friendly warning after threshold is reached
87
+ if (
88
+ state.shouldShowNetworkWarning(
89
+ NETWORK_CONFIG.WARNING_THRESHOLD,
90
+ NETWORK_CONFIG.WARNING_COOLDOWN
91
+ )
92
+ ) {
93
+ this.displayNetworkWarning();
94
+ }
95
+ }
96
+
97
+ static displayNetworkWarning() {
98
+ console.log(chalk.yellow(lang.t("networkIssueTitle")));
99
+ console.log(chalk.gray(lang.t("networkIssueDesc")));
100
+ console.log(chalk.cyan(lang.t("networkIssueTunnel")));
101
+ console.log(chalk.yellow(lang.t("networkIssueReasons")));
102
+ console.log(chalk.gray(lang.t("networkIssueReason1")));
103
+ console.log(chalk.gray(lang.t("networkIssueReason2")));
104
+ console.log(chalk.gray(lang.t("networkIssueReason3")));
105
+ console.log(chalk.yellow(lang.t("networkIssueFix")));
106
+ console.log(chalk.gray(lang.t("networkIssueFix1")));
107
+ console.log(chalk.gray(lang.t("networkIssueFix2")));
108
+ console.log(chalk.gray(lang.t("networkIssueFix3")));
109
+ console.log(chalk.gray(lang.t("networkIssueFix4")));
110
+ console.log(chalk.blue(lang.t("networkIssueIgnore")));
111
+ }
112
+
75
113
  static handleError(err, spinner) {
76
114
  if (spinner) {
77
115
  spinner.fail("Failed to spawn cloudflared process.");
@@ -0,0 +1,139 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+
5
+ /**
6
+ * Configuration Manager
7
+ * Handles persistent storage of user preferences (backend URL, language, etc.)
8
+ */
9
+ class ConfigManager {
10
+ constructor() {
11
+ this.configDir = path.join(os.homedir(), ".nport");
12
+ this.configFile = path.join(this.configDir, "config.json");
13
+ this.oldLangFile = path.join(this.configDir, "lang"); // For migration
14
+ this.config = this.loadConfig();
15
+ this.migrateOldConfig();
16
+ }
17
+
18
+ /**
19
+ * Load configuration from file
20
+ * @returns {object} Configuration object
21
+ */
22
+ loadConfig() {
23
+ try {
24
+ if (fs.existsSync(this.configFile)) {
25
+ const data = fs.readFileSync(this.configFile, "utf8");
26
+ return JSON.parse(data);
27
+ }
28
+ } catch (error) {
29
+ // If config is corrupted or invalid, return default
30
+ console.warn("Warning: Could not load config file, using defaults");
31
+ }
32
+ return {};
33
+ }
34
+
35
+ /**
36
+ * Migrate old configuration files to new unified format from version 2.0.5
37
+ */
38
+ migrateOldConfig() {
39
+ try {
40
+ // Migrate old language file if it exists and no language in config
41
+ if (!this.config.language && fs.existsSync(this.oldLangFile)) {
42
+ const oldLang = fs.readFileSync(this.oldLangFile, "utf8").trim();
43
+ if (oldLang && ["en", "vi"].includes(oldLang)) {
44
+ this.config.language = oldLang;
45
+ this.saveConfig();
46
+ // Optionally delete old file
47
+ try {
48
+ fs.unlinkSync(this.oldLangFile);
49
+ } catch (err) {
50
+ // Ignore if can't delete
51
+ }
52
+ }
53
+ }
54
+ } catch (error) {
55
+ // Ignore migration errors
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Save configuration to file
61
+ */
62
+ saveConfig() {
63
+ try {
64
+ // Ensure .nport directory exists
65
+ if (!fs.existsSync(this.configDir)) {
66
+ fs.mkdirSync(this.configDir, { recursive: true });
67
+ }
68
+ fs.writeFileSync(this.configFile, JSON.stringify(this.config, null, 2), "utf8");
69
+ return true;
70
+ } catch (error) {
71
+ console.warn("Warning: Could not save configuration");
72
+ return false;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Get backend URL from config
78
+ * @returns {string|null} Saved backend URL or null
79
+ */
80
+ getBackendUrl() {
81
+ return this.config.backendUrl || null;
82
+ }
83
+
84
+ /**
85
+ * Set backend URL in config
86
+ * @param {string} url - Backend URL to save
87
+ * @returns {boolean} Success status
88
+ */
89
+ setBackendUrl(url) {
90
+ if (!url) {
91
+ delete this.config.backendUrl;
92
+ } else {
93
+ this.config.backendUrl = url;
94
+ }
95
+ return this.saveConfig();
96
+ }
97
+
98
+ /**
99
+ * Get language from config
100
+ * @returns {string|null} Saved language code or null
101
+ */
102
+ getLanguage() {
103
+ return this.config.language || null;
104
+ }
105
+
106
+ /**
107
+ * Set language in config
108
+ * @param {string} lang - Language code to save (e.g., 'en', 'vi')
109
+ * @returns {boolean} Success status
110
+ */
111
+ setLanguage(lang) {
112
+ if (!lang) {
113
+ delete this.config.language;
114
+ } else {
115
+ this.config.language = lang;
116
+ }
117
+ return this.saveConfig();
118
+ }
119
+
120
+ /**
121
+ * Get all configuration
122
+ * @returns {object} All configuration
123
+ */
124
+ getAll() {
125
+ return { ...this.config };
126
+ }
127
+
128
+ /**
129
+ * Clear all configuration
130
+ * @returns {boolean} Success status
131
+ */
132
+ clear() {
133
+ this.config = {};
134
+ return this.saveConfig();
135
+ }
136
+ }
137
+
138
+ // Export singleton instance
139
+ export const configManager = new ConfigManager();
package/src/config.js CHANGED
@@ -7,11 +7,23 @@ const __dirname = path.dirname(path.dirname(__filename));
7
7
  const require = createRequire(import.meta.url);
8
8
  const packageJson = require("../package.json");
9
9
 
10
+ // Helper function to get backend URL with priority order
11
+ function getBackendUrl() {
12
+ // Priority 1: Environment variable
13
+ if (process.env.NPORT_BACKEND_URL) {
14
+ return process.env.NPORT_BACKEND_URL;
15
+ }
16
+
17
+ // Priority 2: Saved config (will be set by config-manager if available)
18
+ // Priority 3: Default
19
+ return "https://api.nport.link";
20
+ }
21
+
10
22
  // Application constants
11
23
  export const CONFIG = {
12
24
  PACKAGE_NAME: packageJson.name,
13
25
  CURRENT_VERSION: packageJson.version,
14
- BACKEND_URL: "https://nport.tuanngocptn.workers.dev",
26
+ BACKEND_URL: getBackendUrl(),
15
27
  DEFAULT_PORT: 8080,
16
28
  SUBDOMAIN_PREFIX: "user-",
17
29
  TUNNEL_TIMEOUT_HOURS: 4,
@@ -34,6 +46,18 @@ export const PATHS = {
34
46
  export const LOG_PATTERNS = {
35
47
  SUCCESS: ["Registered tunnel connection"],
36
48
  ERROR: ["ERR", "error"],
49
+
50
+ // Network-related warnings (not critical errors)
51
+ NETWORK_WARNING: [
52
+ "failed to accept QUIC stream",
53
+ "failed to dial to edge with quic",
54
+ "failed to accept incoming stream requests",
55
+ "Failed to dial a quic connection",
56
+ "timeout: no recent network activity",
57
+ "failed to dial to edge",
58
+ "quic:",
59
+ ],
60
+
37
61
  IGNORE: [
38
62
  "Cannot determine default origin certificate path",
39
63
  "No file cert.pem",
@@ -53,6 +77,12 @@ export const LOG_PATTERNS = {
53
77
  ],
54
78
  };
55
79
 
80
+ // Network warning configuration
81
+ export const NETWORK_CONFIG = {
82
+ WARNING_THRESHOLD: 5, // Show warning after 5 network errors
83
+ WARNING_COOLDOWN: 30000, // Only show warning every 30 seconds
84
+ };
85
+
56
86
  // Computed constants
57
87
  export const TUNNEL_TIMEOUT_MS = CONFIG.TUNNEL_TIMEOUT_HOURS * 60 * 60 * 1000;
58
88
 
package/src/lang.js CHANGED
@@ -2,6 +2,7 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import os from "os";
4
4
  import readline from "readline";
5
+ import { configManager } from "./config-manager.js";
5
6
 
6
7
  // ============================================================================
7
8
  // Language Translations
@@ -10,7 +11,7 @@ import readline from "readline";
10
11
  const TRANSLATIONS = {
11
12
  en: {
12
13
  // Header
13
- header: "N P O R T âšĄī¸ Free & Open Source from Vietnam",
14
+ header: "N P O R T âšĄī¸ Free & Open Source from Vietnam â¤ī¸",
14
15
 
15
16
  // Spinners
16
17
  creatingTunnel: "Creating tunnel for port {port}...",
@@ -24,7 +25,7 @@ const TRANSLATIONS = {
24
25
 
25
26
  // Footer
26
27
  footerTitle: "đŸ”Ĩ KEEP THE VIBE ALIVE?",
27
- footerSubtitle: "(Made with â¤ī¸ in Vietnam)",
28
+ footerSubtitle: "(Made with â¤ī¸ in Vietnam)",
28
29
  dropStar: "â­ī¸ Drop a Star: ",
29
30
  sendCoffee: "â˜•ī¸ Buy Coffee: ",
30
31
  newVersion: "🚨 NEW VERSION (v{version}) detected!",
@@ -61,6 +62,21 @@ const TRANSLATIONS = {
61
62
  languageVietnamese: "2. Tiáēŋng Viáģ‡t (Vietnamese)",
62
63
  languageInvalid: "Invalid choice. Using English by default.",
63
64
  languageSaved: "✔ Language preference saved!",
65
+
66
+ // Network warnings
67
+ networkIssueTitle: "\nâš ī¸ NETWORK CONNECTIVITY ISSUE DETECTED",
68
+ networkIssueDesc: " Cloudflared is having trouble maintaining a stable connection to Cloudflare's edge servers.",
69
+ networkIssueTunnel: " 📡 Your tunnel is still working, but connection quality may be affected.",
70
+ networkIssueReasons: "\n 💡 Possible reasons:",
71
+ networkIssueReason1: " â€ĸ Unstable internet connection or high packet loss",
72
+ networkIssueReason2: " â€ĸ Firewall/Router blocking UDP traffic (QUIC protocol)",
73
+ networkIssueReason3: " â€ĸ ISP throttling or network congestion",
74
+ networkIssueFix: "\n 🔧 What to try:",
75
+ networkIssueFix1: " â€ĸ Check your internet connection stability",
76
+ networkIssueFix2: " â€ĸ Try connecting from a different network",
77
+ networkIssueFix3: " â€ĸ Disable VPN/Proxy if you're using one",
78
+ networkIssueFix4: " â€ĸ The tunnel will automatically fallback to HTTP/2 if QUIC fails",
79
+ networkIssueIgnore: "\n â„šī¸ This is usually not critical - your tunnel should continue working normally.\n",
64
80
  },
65
81
 
66
82
  vi: {
@@ -78,7 +94,7 @@ const TRANSLATIONS = {
78
94
  timeRemaining: "âąī¸ Tăng táģ‘c tháē§n sáē§u: CÃ˛n {hours}h đáģƒ quáēŠy!",
79
95
 
80
96
  // Footer
81
- footerTitle: "đŸ”Ĩ LƯU DANH SáģŦ SÁCH! KHÔNG QUÊN STAR",
97
+ footerTitle: "đŸ”Ĩ LƯU DANH SáģŦ SÁCH! ĐáģĒNG QUÊN STAR â­ī¸",
82
98
  footerSubtitle: "(Made in Viáģ‡t Nam, chuáēŠn không cáē§n cháģ‰nh! â¤ī¸)",
83
99
  dropStar: "â­ī¸ TháēŖ Star: ",
84
100
  sendCoffee: "â˜•ī¸ Táēˇng Coffee: ",
@@ -97,13 +113,13 @@ const TRANSLATIONS = {
97
113
  goodbyeTitle: "👋 GáēļP Láē I Báē N áģž ÄÆ¯áģœNG BĂNG KHÁC...",
98
114
  goodbyeMessage: "CáēŖm ÆĄn Ä‘ÃŖ quáēŠy NPort! Láē§n sau chÆĄi tiáēŋp nha 😘",
99
115
  website: "🌐 SÃĸn chÆĄi chính: ",
100
- author: "👤 Nhà tài tráģŖ chÆ°ÆĄng trÃŦnh: ",
116
+ author: "👤 Nhà tài tráģŖ: ",
101
117
  changeLanguage: "🌍 Đáģ•i ngôn ngáģ¯: ",
102
118
  changeLanguageHint: "nport --language",
103
119
 
104
120
  // Version
105
121
  versionTitle: "NPort v{version}",
106
- versionSubtitle: "HÆĄn cáēŖ Ngrok - Ma-de in Ziáģ‡t Nam",
122
+ versionSubtitle: "HÆĄn cáēŖ Ngrok - Ma-de in Viáģ‡t Nam",
107
123
  versionLatest: "🎉 ChÃēc máģĢng! Đang cÚng server váģ›i báēŖn máģ›i nháēĨt!",
108
124
  versionAvailable: "🌟 Vèo vèo: CÃŗ báēŖn máģ›i v{version} váģĢa cáē­p báēŋn!",
109
125
  versionUpdate: "Update kháēŠn trÆ°ÆĄng láēš làng: ",
@@ -116,6 +132,21 @@ const TRANSLATIONS = {
116
132
  languageVietnamese: "2. Tiáēŋng Viáģ‡t (Đáģ‰nh cáģ§a chÃŗp)",
117
133
  languageInvalid: "Æ  hÆĄ, cháģn sai ráģ“i! Máēˇc đáģ‹nh Tiáēŋng Viáģ‡t luôn cho nÃŗng.",
118
134
  languageSaved: "đŸŽ¯ Xong ráģ“i! Lưu ngôn ngáģ¯ thành công!",
135
+
136
+ // Network warnings
137
+ networkIssueTitle: "\nâš ī¸ PHÁT HIáģ†N Váē¤N Đáģ€ Máē NG",
138
+ networkIssueDesc: " Cloudflared đang gáēˇp khÃŗ khăn khi giáģ¯ káēŋt náģ‘i áģ•n đáģ‹nh táģ›i Cloudflare edge servers.",
139
+ networkIssueTunnel: " 📡 Tunnel cáģ§a báēĄn váēĢn hoáēĄt đáģ™ng, nhưng cháēĨt lưáģŖng káēŋt náģ‘i cÃŗ tháģƒ báģ‹ áēŖnh hưáģŸng.",
140
+ networkIssueReasons: "\n 💡 CÃŗ tháģƒ do:",
141
+ networkIssueReason1: " â€ĸ MáēĄng internet không áģ•n đáģ‹nh hoáēˇc máēĨt gÃŗi tin",
142
+ networkIssueReason2: " â€ĸ Firewall/Router cháēˇn UDP traffic (giao tháģŠc QUIC)",
143
+ networkIssueReason3: " â€ĸ Nhà máēĄng throttle hoáēˇc táē¯c ngháēŊn máēĄng",
144
+ networkIssueFix: "\n 🔧 Tháģ­ cÃĄc cÃĄch sau:",
145
+ networkIssueFix1: " â€ĸ Kiáģƒm tra káēŋt náģ‘i internet cáģ§a báēĄn",
146
+ networkIssueFix2: " â€ĸ Tháģ­ Ä‘áģ•i sang máēĄng khÃĄc (ví dáģĨ: 4G/5G)",
147
+ networkIssueFix3: " â€ĸ Táē¯t VPN/Proxy náēŋu đang báē­t",
148
+ networkIssueFix4: " â€ĸ Tunnel sáēŊ táģą Ä‘áģ™ng chuyáģƒn sang HTTP/2 náēŋu QUIC fail",
149
+ networkIssueIgnore: "\n â„šī¸ Láģ—i này thưáģng không nghiÃĒm tráģng - tunnel váēĢn hoáēĄt đáģ™ng bÃŦnh thưáģng.\n",
119
150
  }
120
151
  };
121
152
 
@@ -126,8 +157,6 @@ const TRANSLATIONS = {
126
157
  class LanguageManager {
127
158
  constructor() {
128
159
  this.currentLanguage = "en";
129
- this.configDir = path.join(os.homedir(), ".nport");
130
- this.configFile = path.join(this.configDir, "lang");
131
160
  this.availableLanguages = ["en", "vi"];
132
161
  }
133
162
 
@@ -154,15 +183,9 @@ class LanguageManager {
154
183
  * @returns {string|null} Saved language code or null
155
184
  */
156
185
  loadLanguagePreference() {
157
- try {
158
- if (fs.existsSync(this.configFile)) {
159
- const lang = fs.readFileSync(this.configFile, "utf8").trim();
160
- if (this.availableLanguages.includes(lang)) {
161
- return lang;
162
- }
163
- }
164
- } catch (error) {
165
- // Ignore errors, will prompt user
186
+ const lang = configManager.getLanguage();
187
+ if (lang && this.availableLanguages.includes(lang)) {
188
+ return lang;
166
189
  }
167
190
  return null;
168
191
  }
@@ -172,16 +195,7 @@ class LanguageManager {
172
195
  * @param {string} lang - Language code to save
173
196
  */
174
197
  saveLanguagePreference(lang) {
175
- try {
176
- // Ensure .nport directory exists
177
- if (!fs.existsSync(this.configDir)) {
178
- fs.mkdirSync(this.configDir, { recursive: true });
179
- }
180
- fs.writeFileSync(this.configFile, lang, "utf8");
181
- } catch (error) {
182
- // Silently fail if can't save
183
- console.warn("Warning: Could not save language preference");
184
- }
198
+ configManager.setLanguage(lang);
185
199
  }
186
200
 
187
201
  /**
package/src/state.js CHANGED
@@ -7,17 +7,24 @@ class TunnelState {
7
7
  this.tunnelId = null;
8
8
  this.subdomain = null;
9
9
  this.port = null;
10
+ this.backendUrl = null;
10
11
  this.tunnelProcess = null;
11
12
  this.timeoutId = null;
12
13
  this.connectionCount = 0;
13
14
  this.startTime = null;
14
15
  this.updateInfo = null;
16
+
17
+ // Network issue tracking
18
+ this.networkIssueCount = 0;
19
+ this.lastNetworkWarningTime = 0;
20
+ this.networkWarningShown = false;
15
21
  }
16
22
 
17
- setTunnel(tunnelId, subdomain, port) {
23
+ setTunnel(tunnelId, subdomain, port, backendUrl = null) {
18
24
  this.tunnelId = tunnelId;
19
25
  this.subdomain = subdomain;
20
26
  this.port = port;
27
+ this.backendUrl = backendUrl;
21
28
  if (!this.startTime) {
22
29
  this.startTime = Date.now();
23
30
  }
@@ -60,15 +67,47 @@ class TunnelState {
60
67
  return (Date.now() - this.startTime) / 1000;
61
68
  }
62
69
 
70
+ // Network issue tracking methods
71
+ incrementNetworkIssue() {
72
+ this.networkIssueCount++;
73
+ return this.networkIssueCount;
74
+ }
75
+
76
+ resetNetworkIssues() {
77
+ this.networkIssueCount = 0;
78
+ }
79
+
80
+ shouldShowNetworkWarning(threshold, cooldown) {
81
+ const now = Date.now();
82
+ if (
83
+ this.networkIssueCount >= threshold &&
84
+ now - this.lastNetworkWarningTime > cooldown
85
+ ) {
86
+ this.lastNetworkWarningTime = now;
87
+ return true;
88
+ }
89
+ return false;
90
+ }
91
+
92
+ setNetworkWarningShown(value) {
93
+ this.networkWarningShown = value;
94
+ }
95
+
63
96
  reset() {
64
97
  this.clearTimeout();
65
98
  this.tunnelId = null;
66
99
  this.subdomain = null;
67
100
  this.port = null;
101
+ this.backendUrl = null;
68
102
  this.tunnelProcess = null;
69
103
  this.connectionCount = 0;
70
104
  this.startTime = null;
71
105
  this.updateInfo = null;
106
+
107
+ // Reset network tracking
108
+ this.networkIssueCount = 0;
109
+ this.lastNetworkWarningTime = 0;
110
+ this.networkWarningShown = false;
72
111
  }
73
112
  }
74
113
 
package/src/tunnel.js CHANGED
@@ -15,7 +15,7 @@ import { lang } from "./lang.js";
15
15
  */
16
16
  export class TunnelOrchestrator {
17
17
  static async start(config) {
18
- state.setTunnel(null, config.subdomain, config.port);
18
+ state.setTunnel(null, config.subdomain, config.port, config.backendUrl);
19
19
 
20
20
  // Initialize analytics
21
21
  await analytics.initialize();
@@ -42,8 +42,8 @@ export class TunnelOrchestrator {
42
42
 
43
43
  try {
44
44
  // Create tunnel
45
- const tunnel = await APIClient.createTunnel(config.subdomain);
46
- state.setTunnel(tunnel.tunnelId, config.subdomain, config.port);
45
+ const tunnel = await APIClient.createTunnel(config.subdomain, config.backendUrl);
46
+ state.setTunnel(tunnel.tunnelId, config.subdomain, config.port, config.backendUrl);
47
47
 
48
48
  // Track successful tunnel creation
49
49
  analytics.trackTunnelCreated(config.subdomain, config.port);
@@ -101,7 +101,7 @@ export class TunnelOrchestrator {
101
101
  }
102
102
 
103
103
  // Delete tunnel
104
- await APIClient.deleteTunnel(state.subdomain, state.tunnelId);
104
+ await APIClient.deleteTunnel(state.subdomain, state.tunnelId, state.backendUrl);
105
105
  UI.displayCleanupSuccess();
106
106
  } catch (err) {
107
107
  UI.displayCleanupError();
package/src/ui.js CHANGED
@@ -9,8 +9,13 @@ import { lang } from "./lang.js";
9
9
  export class UI {
10
10
  static displayProjectInfo() {
11
11
  const line = "─".repeat(56);
12
+ const headerText = lang.t("header");
13
+ // Calculate proper padding (accounting for emojis which take visual space)
14
+ const visualLength = 59; // Target visual width
15
+ const padding = " ".repeat(Math.max(0, visualLength - headerText.length - 4));
16
+
12
17
  console.log(chalk.gray(`\n ╭${line}╮`));
13
- console.log(chalk.cyan.bold(` │ ${lang.t("header")}`) + " ".repeat(56 - lang.t("header").length - 4) + chalk.gray("│"));
18
+ console.log(chalk.cyan.bold(` │ ${headerText}`) + padding + chalk.gray("│"));
14
19
  console.log(chalk.gray(` ╰${line}╯\n`));
15
20
  }
16
21
 
@@ -95,4 +100,4 @@ export class UI {
95
100
 
96
101
  console.log(chalk.gray(lang.t("learnMore")) + chalk.cyan("https://nport.link\n"));
97
102
  }
98
- }
103
+ }