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 +183 -0
- package/LICENSE +21 -0
- package/README.md +73 -1
- package/index.js +42 -0
- package/package.json +3 -3
- package/src/api.js +21 -4
- package/src/args.js +41 -2
- package/src/binary.js +39 -1
- package/src/config-manager.js +139 -0
- package/src/config.js +31 -1
- package/src/lang.js +40 -26
- package/src/state.js +40 -1
- package/src/tunnel.js +4 -4
- package/src/ui.js +7 -2
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.
|
|
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": ">=
|
|
12
|
-
"npm": ">=
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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 â¤ī¸
|
|
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!
|
|
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
|
|
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
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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(` â ${
|
|
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
|
+
}
|