nport 2.0.4 → 2.0.6
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 +175 -0
- package/README.md +174 -16
- package/index.js +84 -639
- package/package.json +4 -4
- package/{analytics.js → src/analytics.js} +8 -2
- package/src/api.js +89 -0
- package/src/args.js +122 -0
- package/{bin-manager.js → src/bin-manager.js} +2 -1
- package/src/binary.js +88 -0
- package/src/config-manager.js +139 -0
- package/src/config.js +70 -0
- package/src/lang.js +263 -0
- package/src/state.js +79 -0
- package/src/tunnel.js +116 -0
- package/src/ui.js +103 -0
- package/src/version.js +56 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nport",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
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",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
}
|
|
64
64
|
],
|
|
65
65
|
"scripts": {
|
|
66
|
-
"postinstall": "node bin-manager.js",
|
|
66
|
+
"postinstall": "node src/bin-manager.js",
|
|
67
67
|
"start": "node index.js"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
@@ -73,9 +73,9 @@
|
|
|
73
73
|
},
|
|
74
74
|
"files": [
|
|
75
75
|
"index.js",
|
|
76
|
-
"
|
|
77
|
-
"bin-manager.js",
|
|
76
|
+
"src/",
|
|
78
77
|
"README.md",
|
|
78
|
+
"CHANGELOG.md",
|
|
79
79
|
"LICENSE"
|
|
80
80
|
],
|
|
81
81
|
"os": [
|
|
@@ -11,7 +11,7 @@ import path from "path";
|
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
12
12
|
|
|
13
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
-
const __dirname = path.dirname(__filename);
|
|
14
|
+
const __dirname = path.dirname(path.dirname(__filename));
|
|
15
15
|
|
|
16
16
|
// Firebase/GA4 Configuration (from website/home.html)
|
|
17
17
|
// Full Firebase config for reference (if needed for future features)
|
|
@@ -36,7 +36,7 @@ const ANALYTICS_CONFIG = {
|
|
|
36
36
|
enabled: true, // Can be disabled by environment variable
|
|
37
37
|
debug: process.env.NPORT_DEBUG === "true",
|
|
38
38
|
timeout: 2000, // Don't block CLI for too long
|
|
39
|
-
userIdFile: path.join(os.homedir(), ".nport-
|
|
39
|
+
userIdFile: path.join(os.homedir(), ".nport", "analytics-id"),
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
// ============================================================================
|
|
@@ -92,6 +92,12 @@ class AnalyticsManager {
|
|
|
92
92
|
*/
|
|
93
93
|
async getUserId() {
|
|
94
94
|
try {
|
|
95
|
+
// Ensure .nport directory exists
|
|
96
|
+
const configDir = path.join(os.homedir(), ".nport");
|
|
97
|
+
if (!fs.existsSync(configDir)) {
|
|
98
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
|
|
95
101
|
// Try to read existing user ID
|
|
96
102
|
if (fs.existsSync(ANALYTICS_CONFIG.userIdFile)) {
|
|
97
103
|
const userId = fs.readFileSync(ANALYTICS_CONFIG.userIdFile, "utf8").trim();
|
package/src/api.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { CONFIG } from "./config.js";
|
|
4
|
+
import { state } from "./state.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* API Client
|
|
8
|
+
* Handles communication with the NPort backend service
|
|
9
|
+
*/
|
|
10
|
+
export class APIClient {
|
|
11
|
+
static async createTunnel(subdomain, backendUrl = null) {
|
|
12
|
+
const url = backendUrl || CONFIG.BACKEND_URL;
|
|
13
|
+
try {
|
|
14
|
+
const { data } = await axios.post(url, { subdomain });
|
|
15
|
+
|
|
16
|
+
if (!data.success) {
|
|
17
|
+
throw new Error(data.error || "Unknown error from backend");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
tunnelId: data.tunnelId,
|
|
22
|
+
tunnelToken: data.tunnelToken,
|
|
23
|
+
url: data.url,
|
|
24
|
+
};
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw this.handleError(error, subdomain);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static async deleteTunnel(subdomain, tunnelId, backendUrl = null) {
|
|
31
|
+
const url = backendUrl || CONFIG.BACKEND_URL;
|
|
32
|
+
await axios.delete(url, {
|
|
33
|
+
data: { subdomain, tunnelId },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static handleError(error, subdomain) {
|
|
38
|
+
if (error.response?.data?.error) {
|
|
39
|
+
const errorMsg = error.response.data.error;
|
|
40
|
+
|
|
41
|
+
// Check for subdomain in use (active tunnel)
|
|
42
|
+
if (
|
|
43
|
+
errorMsg.includes("SUBDOMAIN_IN_USE:") ||
|
|
44
|
+
errorMsg.includes("currently in use") ||
|
|
45
|
+
errorMsg.includes("already exists and is currently active")
|
|
46
|
+
) {
|
|
47
|
+
return new Error(
|
|
48
|
+
chalk.red(`✗ Subdomain "${subdomain}" is already in use!\n\n`) +
|
|
49
|
+
chalk.yellow(`💡 This subdomain is currently being used by another active tunnel.\n\n`) +
|
|
50
|
+
chalk.white(`Choose a different subdomain:\n`) +
|
|
51
|
+
chalk.gray(` 1. Add a suffix: `) +
|
|
52
|
+
chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT} -s ${subdomain}-2\n`) +
|
|
53
|
+
chalk.gray(` 2. Try a variation: `) +
|
|
54
|
+
chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT} -s my-${subdomain}\n`) +
|
|
55
|
+
chalk.gray(` 3. Use random name: `) +
|
|
56
|
+
chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT}\n`)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check for duplicate tunnel error (other Cloudflare errors)
|
|
61
|
+
if (
|
|
62
|
+
errorMsg.includes("already have a tunnel") ||
|
|
63
|
+
errorMsg.includes("[1013]")
|
|
64
|
+
) {
|
|
65
|
+
return new Error(
|
|
66
|
+
`Subdomain "${subdomain}" is already taken or in use.\n\n` +
|
|
67
|
+
chalk.yellow(`💡 Try one of these options:\n`) +
|
|
68
|
+
chalk.gray(` 1. Choose a different subdomain: `) +
|
|
69
|
+
chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT} -s ${subdomain}-v2\n`) +
|
|
70
|
+
chalk.gray(` 2. Use a random subdomain: `) +
|
|
71
|
+
chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT}\n`) +
|
|
72
|
+
chalk.gray(
|
|
73
|
+
` 3. Wait a few minutes and retry if you just stopped a tunnel with this name`
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return new Error(`Backend Error: ${errorMsg}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (error.response) {
|
|
82
|
+
const errorMsg = JSON.stringify(error.response.data, null, 2);
|
|
83
|
+
return new Error(`Backend Error: ${errorMsg}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
package/src/args.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { CONFIG } from "./config.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Command Line Argument Parser
|
|
5
|
+
* Handles parsing of CLI arguments for port, subdomain, language, and backend URL
|
|
6
|
+
*/
|
|
7
|
+
export class ArgumentParser {
|
|
8
|
+
static parse(argv) {
|
|
9
|
+
const port = this.parsePort(argv);
|
|
10
|
+
const subdomain = this.parseSubdomain(argv);
|
|
11
|
+
const language = this.parseLanguage(argv);
|
|
12
|
+
const backendUrl = this.parseBackendUrl(argv);
|
|
13
|
+
const setBackend = this.parseSetBackend(argv);
|
|
14
|
+
return { port, subdomain, language, backendUrl, setBackend };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static parsePort(argv) {
|
|
18
|
+
const portArg = parseInt(argv[0]);
|
|
19
|
+
return portArg || CONFIG.DEFAULT_PORT;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static parseSubdomain(argv) {
|
|
23
|
+
// Try all subdomain formats
|
|
24
|
+
const formats = [
|
|
25
|
+
() => this.findFlagWithEquals(argv, "--subdomain="),
|
|
26
|
+
() => this.findFlagWithEquals(argv, "-s="),
|
|
27
|
+
() => this.findFlagWithValue(argv, "--subdomain"),
|
|
28
|
+
() => this.findFlagWithValue(argv, "-s"),
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
for (const format of formats) {
|
|
32
|
+
const subdomain = format();
|
|
33
|
+
if (subdomain) return subdomain;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return this.generateRandomSubdomain();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static parseLanguage(argv) {
|
|
40
|
+
// Check if --language flag exists without value (to trigger prompt)
|
|
41
|
+
if (argv.includes('--language') || argv.includes('--lang') || argv.includes('-l')) {
|
|
42
|
+
const langIndex = argv.indexOf('--language') !== -1 ? argv.indexOf('--language') :
|
|
43
|
+
argv.indexOf('--lang') !== -1 ? argv.indexOf('--lang') :
|
|
44
|
+
argv.indexOf('-l');
|
|
45
|
+
|
|
46
|
+
// If flag is present but next arg is another flag or doesn't exist, return 'prompt'
|
|
47
|
+
const nextArg = argv[langIndex + 1];
|
|
48
|
+
if (!nextArg || nextArg.startsWith('-')) {
|
|
49
|
+
return 'prompt';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Try language flag formats: --language=en, --lang=en, -l en
|
|
54
|
+
const formats = [
|
|
55
|
+
() => this.findFlagWithEquals(argv, "--language="),
|
|
56
|
+
() => this.findFlagWithEquals(argv, "--lang="),
|
|
57
|
+
() => this.findFlagWithEquals(argv, "-l="),
|
|
58
|
+
() => this.findFlagWithValue(argv, "--language"),
|
|
59
|
+
() => this.findFlagWithValue(argv, "--lang"),
|
|
60
|
+
() => this.findFlagWithValue(argv, "-l"),
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
for (const format of formats) {
|
|
64
|
+
const language = format();
|
|
65
|
+
if (language) return language;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null; // No language specified
|
|
69
|
+
}
|
|
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
|
+
|
|
108
|
+
static findFlagWithEquals(argv, flag) {
|
|
109
|
+
const arg = argv.find((a) => a.startsWith(flag));
|
|
110
|
+
return arg ? arg.split("=")[1] : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static findFlagWithValue(argv, flag) {
|
|
114
|
+
const index = argv.indexOf(flag);
|
|
115
|
+
return index !== -1 && argv[index + 1] ? argv[index + 1] : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static generateRandomSubdomain() {
|
|
119
|
+
return `${CONFIG.SUBDOMAIN_PREFIX}${Math.floor(Math.random() * 10000)}`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
@@ -11,7 +11,7 @@ import { fileURLToPath } from "url";
|
|
|
11
11
|
|
|
12
12
|
// Fix for __dirname in ES modules
|
|
13
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
-
const __dirname = path.dirname(__filename);
|
|
14
|
+
const __dirname = path.dirname(path.dirname(__filename));
|
|
15
15
|
|
|
16
16
|
// Binary configuration
|
|
17
17
|
const BIN_DIR = path.join(__dirname, "bin");
|
|
@@ -376,3 +376,4 @@ async function main() {
|
|
|
376
376
|
if (process.argv[1] === __filename) {
|
|
377
377
|
main();
|
|
378
378
|
}
|
|
379
|
+
|
package/src/binary.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { LOG_PATTERNS } from "./config.js";
|
|
5
|
+
import { state } from "./state.js";
|
|
6
|
+
import { UI } from "./ui.js";
|
|
7
|
+
import { lang } from "./lang.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Binary Manager
|
|
11
|
+
* Handles cloudflared binary validation, spawning, and process management
|
|
12
|
+
*/
|
|
13
|
+
export class BinaryManager {
|
|
14
|
+
static validate(binaryPath) {
|
|
15
|
+
if (fs.existsSync(binaryPath)) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.error(
|
|
20
|
+
chalk.red(`\n❌ Error: Cloudflared binary not found at: ${binaryPath}`)
|
|
21
|
+
);
|
|
22
|
+
console.error(
|
|
23
|
+
chalk.yellow(
|
|
24
|
+
"👉 Please run 'npm install' again to download the binary.\n"
|
|
25
|
+
)
|
|
26
|
+
);
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static spawn(binaryPath, token, port) {
|
|
31
|
+
return spawn(binaryPath, [
|
|
32
|
+
"tunnel",
|
|
33
|
+
"run",
|
|
34
|
+
"--token",
|
|
35
|
+
token,
|
|
36
|
+
"--url",
|
|
37
|
+
`http://localhost:${port}`,
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static attachHandlers(process, spinner = null) {
|
|
42
|
+
process.stderr.on("data", (chunk) => this.handleStderr(chunk));
|
|
43
|
+
process.on("error", (err) => this.handleError(err, spinner));
|
|
44
|
+
process.on("close", (code) => this.handleClose(code));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static handleStderr(chunk) {
|
|
48
|
+
const msg = chunk.toString();
|
|
49
|
+
|
|
50
|
+
// Skip harmless warnings
|
|
51
|
+
if (LOG_PATTERNS.IGNORE.some((pattern) => msg.includes(pattern))) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Show success messages with connection count
|
|
56
|
+
if (LOG_PATTERNS.SUCCESS.some((pattern) => msg.includes(pattern))) {
|
|
57
|
+
const count = state.incrementConnection();
|
|
58
|
+
|
|
59
|
+
if (count === 1) {
|
|
60
|
+
console.log(chalk.green(lang.t("connection1")));
|
|
61
|
+
} else if (count === 4) {
|
|
62
|
+
console.log(chalk.green(lang.t("connection2")));
|
|
63
|
+
// Display footer after tunnel is fully active
|
|
64
|
+
UI.displayFooter(state.updateInfo);
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Show critical errors only
|
|
70
|
+
if (LOG_PATTERNS.ERROR.some((pattern) => msg.includes(pattern))) {
|
|
71
|
+
console.error(chalk.red(`[Cloudflared] ${msg.trim()}`));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static handleError(err, spinner) {
|
|
76
|
+
if (spinner) {
|
|
77
|
+
spinner.fail("Failed to spawn cloudflared process.");
|
|
78
|
+
}
|
|
79
|
+
console.error(chalk.red(`Process Error: ${err.message}`));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static handleClose(code) {
|
|
83
|
+
if (code !== 0 && code !== null) {
|
|
84
|
+
console.log(chalk.red(`Tunnel process exited with code ${code}`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
@@ -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
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { createRequire } from "module";
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(path.dirname(__filename));
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const packageJson = require("../package.json");
|
|
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
|
+
|
|
22
|
+
// Application constants
|
|
23
|
+
export const CONFIG = {
|
|
24
|
+
PACKAGE_NAME: packageJson.name,
|
|
25
|
+
CURRENT_VERSION: packageJson.version,
|
|
26
|
+
BACKEND_URL: getBackendUrl(),
|
|
27
|
+
DEFAULT_PORT: 8080,
|
|
28
|
+
SUBDOMAIN_PREFIX: "user-",
|
|
29
|
+
TUNNEL_TIMEOUT_HOURS: 4,
|
|
30
|
+
UPDATE_CHECK_TIMEOUT: 3000,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Platform-specific configuration
|
|
34
|
+
export const PLATFORM = {
|
|
35
|
+
IS_WINDOWS: process.platform === "win32",
|
|
36
|
+
BIN_NAME: process.platform === "win32" ? "cloudflared.exe" : "cloudflared",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Paths
|
|
40
|
+
export const PATHS = {
|
|
41
|
+
BIN_DIR: path.join(__dirname, "bin"),
|
|
42
|
+
BIN_PATH: path.join(__dirname, "bin", PLATFORM.BIN_NAME),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Log patterns for filtering cloudflared output
|
|
46
|
+
export const LOG_PATTERNS = {
|
|
47
|
+
SUCCESS: ["Registered tunnel connection"],
|
|
48
|
+
ERROR: ["ERR", "error"],
|
|
49
|
+
IGNORE: [
|
|
50
|
+
"Cannot determine default origin certificate path",
|
|
51
|
+
"No file cert.pem",
|
|
52
|
+
"origincert option",
|
|
53
|
+
"TUNNEL_ORIGIN_CERT",
|
|
54
|
+
"context canceled",
|
|
55
|
+
"failed to run the datagram handler",
|
|
56
|
+
"failed to serve tunnel connection",
|
|
57
|
+
"Connection terminated",
|
|
58
|
+
"no more connections active and exiting",
|
|
59
|
+
"Serve tunnel error",
|
|
60
|
+
"accept stream listener encountered a failure",
|
|
61
|
+
"Retrying connection",
|
|
62
|
+
"icmp router terminated",
|
|
63
|
+
"use of closed network connection",
|
|
64
|
+
"Application error 0x0",
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Computed constants
|
|
69
|
+
export const TUNNEL_TIMEOUT_MS = CONFIG.TUNNEL_TIMEOUT_HOURS * 60 * 60 * 1000;
|
|
70
|
+
|