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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nport",
3
- "version": "2.0.4",
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
- "analytics.js",
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-analytics"),
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
+