nport 2.0.7 → 2.1.0

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.
@@ -1,379 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import https from "https";
4
- import os from "os";
5
- import { execSync } from "child_process";
6
- import { fileURLToPath } from "url";
7
-
8
- // ============================================================================
9
- // Configuration & Constants
10
- // ============================================================================
11
-
12
- // Fix for __dirname in ES modules
13
- const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = path.dirname(path.dirname(__filename));
15
-
16
- // Binary configuration
17
- const BIN_DIR = path.join(__dirname, "bin");
18
- const BINARY_NAME = "cloudflared";
19
- const COMPRESSED_SUFFIX = ".tgz";
20
- const TEMP_ARCHIVE_NAME = "cloudflared.tgz";
21
-
22
- // Platform detection
23
- const PLATFORM = os.platform();
24
- const ARCH = os.arch();
25
- const IS_WINDOWS = PLATFORM === "win32";
26
- const IS_MACOS = PLATFORM === "darwin";
27
- const IS_LINUX = PLATFORM === "linux";
28
-
29
- // Binary paths
30
- const BIN_NAME = IS_WINDOWS ? `${BINARY_NAME}.exe` : BINARY_NAME;
31
- const BIN_PATH = path.join(BIN_DIR, BIN_NAME);
32
-
33
- // Download configuration
34
- const GITHUB_BASE_URL = "https://github.com/cloudflare/cloudflared/releases/latest/download";
35
- const REDIRECT_CODES = [301, 302];
36
- const SUCCESS_CODE = 200;
37
- const UNIX_EXECUTABLE_MODE = "755";
38
-
39
- // ============================================================================
40
- // Platform Detection
41
- // ============================================================================
42
-
43
- /**
44
- * Platform and architecture mapping for cloudflared releases
45
- */
46
- const PLATFORM_MAPPINGS = {
47
- darwin: {
48
- amd64: "cloudflared-darwin-amd64.tgz",
49
- arm64: "cloudflared-darwin-amd64.tgz", // macOS uses universal binary
50
- },
51
- win32: {
52
- x64: "cloudflared-windows-amd64.exe",
53
- ia32: "cloudflared-windows-386.exe",
54
- },
55
- linux: {
56
- x64: "cloudflared-linux-amd64",
57
- arm64: "cloudflared-linux-arm64",
58
- arm: "cloudflared-linux-arm",
59
- },
60
- };
61
-
62
- /**
63
- * Normalizes architecture name for mapping lookup
64
- * @param {string} arch - Raw architecture from os.arch()
65
- * @returns {string} Normalized architecture name
66
- */
67
- function normalizeArch(arch) {
68
- const archMap = {
69
- x64: "x64",
70
- amd64: "amd64",
71
- arm64: "arm64",
72
- ia32: "ia32",
73
- arm: "arm",
74
- };
75
- return archMap[arch] || arch;
76
- }
77
-
78
- /**
79
- * Determines the download URL based on current platform and architecture
80
- * @returns {string} Download URL for cloudflared binary
81
- * @throws {Error} If platform/architecture combination is not supported
82
- */
83
- function getDownloadUrl() {
84
- const normalizedArch = normalizeArch(ARCH);
85
- const platformMapping = PLATFORM_MAPPINGS[PLATFORM];
86
-
87
- if (!platformMapping) {
88
- throw new Error(
89
- `Unsupported platform: ${PLATFORM}. Supported platforms: darwin, win32, linux`
90
- );
91
- }
92
-
93
- const binaryName = platformMapping[normalizedArch];
94
-
95
- if (!binaryName) {
96
- throw new Error(
97
- `Unsupported architecture: ${ARCH} for platform ${PLATFORM}. ` +
98
- `Supported architectures: ${Object.keys(platformMapping).join(", ")}`
99
- );
100
- }
101
-
102
- return `${GITHUB_BASE_URL}/${binaryName}`;
103
- }
104
-
105
- /**
106
- * Checks if the download URL points to a compressed archive
107
- * @param {string} url - Download URL
108
- * @returns {boolean} True if URL is for a compressed file
109
- */
110
- function isCompressedArchive(url) {
111
- return url.endsWith(COMPRESSED_SUFFIX);
112
- }
113
-
114
- // ============================================================================
115
- // File System Utilities
116
- // ============================================================================
117
-
118
- /**
119
- * Ensures directory exists, creates it if it doesn't
120
- * @param {string} dirPath - Directory path to ensure
121
- */
122
- function ensureDirectory(dirPath) {
123
- if (!fs.existsSync(dirPath)) {
124
- fs.mkdirSync(dirPath, { recursive: true });
125
- }
126
- }
127
-
128
- /**
129
- * Safely removes a file if it exists
130
- * @param {string} filePath - Path to file to remove
131
- */
132
- function safeUnlink(filePath) {
133
- try {
134
- if (fs.existsSync(filePath)) {
135
- fs.unlinkSync(filePath);
136
- }
137
- } catch (err) {
138
- // Ignore errors during cleanup
139
- console.warn(`Warning: Could not remove ${filePath}:`, err.message);
140
- }
141
- }
142
-
143
- /**
144
- * Sets executable permissions on Unix-like systems
145
- * @param {string} filePath - Path to file
146
- * @param {string} mode - Permission mode (e.g., "755")
147
- */
148
- function setExecutablePermissions(filePath, mode = UNIX_EXECUTABLE_MODE) {
149
- if (!IS_WINDOWS) {
150
- fs.chmodSync(filePath, mode);
151
- }
152
- }
153
-
154
- /**
155
- * Validates that a file exists at the given path
156
- * @param {string} filePath - Path to validate
157
- * @param {string} errorMessage - Error message if file doesn't exist
158
- * @throws {Error} If file doesn't exist
159
- */
160
- function validateFileExists(filePath, errorMessage) {
161
- if (!fs.existsSync(filePath)) {
162
- throw new Error(errorMessage || `File not found: ${filePath}`);
163
- }
164
- }
165
-
166
- // ============================================================================
167
- // Download Utilities
168
- // ============================================================================
169
-
170
- /**
171
- * Downloads a file from a URL with automatic redirect handling
172
- * @param {string} url - URL to download from
173
- * @param {string} dest - Destination file path
174
- * @returns {Promise<string>} Resolves with destination path on success
175
- */
176
- async function downloadFile(url, dest) {
177
- return new Promise((resolve, reject) => {
178
- const file = fs.createWriteStream(dest);
179
-
180
- https
181
- .get(url, (response) => {
182
- // Handle redirects
183
- if (REDIRECT_CODES.includes(response.statusCode)) {
184
- file.close();
185
- safeUnlink(dest);
186
- downloadFile(response.headers.location, dest)
187
- .then(resolve)
188
- .catch(reject);
189
- return;
190
- }
191
-
192
- // Handle non-success status codes
193
- if (response.statusCode !== SUCCESS_CODE) {
194
- file.close();
195
- safeUnlink(dest);
196
- reject(
197
- new Error(
198
- `Download failed with status code ${response.statusCode} from ${url}`
199
- )
200
- );
201
- return;
202
- }
203
-
204
- // Stream response to file
205
- response.pipe(file);
206
-
207
- file.on("finish", () => {
208
- file.close(() => resolve(dest));
209
- });
210
-
211
- file.on("error", (err) => {
212
- file.close();
213
- safeUnlink(dest);
214
- reject(err);
215
- });
216
- })
217
- .on("error", (err) => {
218
- file.close();
219
- safeUnlink(dest);
220
- reject(new Error(`Network error: ${err.message}`));
221
- });
222
- });
223
- }
224
-
225
- // ============================================================================
226
- // Archive Extraction
227
- // ============================================================================
228
-
229
- /**
230
- * Extracts a .tgz archive to a directory
231
- * @param {string} archivePath - Path to .tgz file
232
- * @param {string} targetDir - Directory to extract to
233
- * @throws {Error} If extraction fails
234
- */
235
- function extractTarGz(archivePath, targetDir) {
236
- try {
237
- execSync(`tar -xzf "${archivePath}" -C "${targetDir}"`, {
238
- stdio: "pipe", // Suppress output
239
- });
240
- } catch (err) {
241
- throw new Error(`Extraction failed: ${err.message}`);
242
- }
243
- }
244
-
245
- // ============================================================================
246
- // Logging Utilities
247
- // ============================================================================
248
-
249
- /**
250
- * Console logging with consistent formatting
251
- */
252
- const logger = {
253
- info: (msg) => console.log(`ℹ️ ${msg}`),
254
- success: (msg) => console.log(`✅ ${msg}`),
255
- warn: (msg) => console.warn(`⚠️ ${msg}`),
256
- error: (msg) => console.error(`❌ ${msg}`),
257
- progress: (msg) => console.log(`🚧 ${msg}`),
258
- extract: (msg) => console.log(`📦 ${msg}`),
259
- };
260
-
261
- // ============================================================================
262
- // Core Binary Management
263
- // ============================================================================
264
-
265
- /**
266
- * Downloads and installs the cloudflared binary
267
- * @returns {Promise<string>} Path to installed binary
268
- */
269
- async function installBinary() {
270
- logger.progress(
271
- "Cloudflared binary not found. Downloading... (This happens only once)"
272
- );
273
-
274
- const url = getDownloadUrl();
275
- const isArchive = isCompressedArchive(url);
276
- const downloadDest = isArchive
277
- ? path.join(BIN_DIR, TEMP_ARCHIVE_NAME)
278
- : BIN_PATH;
279
-
280
- try {
281
- // Download binary or archive
282
- await downloadFile(url, downloadDest);
283
-
284
- // Extract if it's an archive (macOS)
285
- if (isArchive) {
286
- logger.extract("Extracting binary...");
287
- extractTarGz(downloadDest, BIN_DIR);
288
-
289
- // Clean up archive
290
- safeUnlink(downloadDest);
291
-
292
- // Validate extraction succeeded
293
- validateFileExists(
294
- BIN_PATH,
295
- "Extraction failed: Binary not found after extraction"
296
- );
297
- }
298
-
299
- // Set executable permissions (Unix-like systems)
300
- setExecutablePermissions(BIN_PATH);
301
-
302
- logger.success("Download complete.");
303
- return BIN_PATH;
304
- } catch (error) {
305
- // Clean up any partial downloads
306
- safeUnlink(downloadDest);
307
- safeUnlink(BIN_PATH);
308
- throw error;
309
- }
310
- }
311
-
312
- /**
313
- * Ensures cloudflared binary is available, downloading if necessary
314
- * @returns {Promise<string>} Path to cloudflared binary
315
- */
316
- export async function ensureCloudflared() {
317
- // Ensure bin directory exists
318
- ensureDirectory(BIN_DIR);
319
-
320
- // Check if binary already exists
321
- if (fs.existsSync(BIN_PATH)) {
322
- // Always ensure permissions are set correctly (in case they were lost)
323
- setExecutablePermissions(BIN_PATH);
324
- return BIN_PATH;
325
- }
326
-
327
- // Download and install
328
- try {
329
- return await installBinary();
330
- } catch (error) {
331
- logger.error(`Installation failed: ${error.message}`);
332
- process.exit(1);
333
- }
334
- }
335
-
336
- // ============================================================================
337
- // CLI Entry Point
338
- // ============================================================================
339
-
340
- /**
341
- * Checks if we're running in a CI environment
342
- * @returns {boolean} True if in CI environment
343
- */
344
- function isCI() {
345
- return !!(
346
- process.env.CI || // Generic CI flag
347
- process.env.GITHUB_ACTIONS || // GitHub Actions
348
- process.env.GITLAB_CI || // GitLab CI
349
- process.env.CIRCLECI || // CircleCI
350
- process.env.TRAVIS || // Travis CI
351
- process.env.JENKINS_URL || // Jenkins
352
- process.env.BUILDKITE // Buildkite
353
- );
354
- }
355
-
356
- /**
357
- * Main function when run directly from command line
358
- */
359
- async function main() {
360
- // Skip binary download in CI environments to keep package lightweight
361
- if (isCI()) {
362
- logger.info("Running in CI environment - skipping binary download");
363
- return;
364
- }
365
-
366
- try {
367
- const binaryPath = await ensureCloudflared();
368
- logger.success(`Cloudflared binary is ready at: ${binaryPath}`);
369
- } catch (error) {
370
- logger.error(error.message);
371
- process.exit(1);
372
- }
373
- }
374
-
375
- // Run if executed directly
376
- if (process.argv[1] === __filename) {
377
- main();
378
- }
379
-
package/src/binary.js DELETED
@@ -1,126 +0,0 @@
1
- import { spawn } from "child_process";
2
- import chalk from "chalk";
3
- import fs from "fs";
4
- import { LOG_PATTERNS, NETWORK_CONFIG } 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
- // 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
-
61
- // Show success messages with connection count
62
- if (LOG_PATTERNS.SUCCESS.some((pattern) => msg.includes(pattern))) {
63
- const count = state.incrementConnection();
64
-
65
- // Reset network issue count when connection succeeds
66
- if (count === 1) {
67
- state.resetNetworkIssues();
68
- console.log(chalk.green(lang.t("connection1")));
69
- } else if (count === 4) {
70
- console.log(chalk.green(lang.t("connection2")));
71
- // Display footer after tunnel is fully active
72
- UI.displayFooter(state.updateInfo);
73
- }
74
- return;
75
- }
76
-
77
- // Show critical errors only
78
- if (LOG_PATTERNS.ERROR.some((pattern) => msg.includes(pattern))) {
79
- console.error(chalk.red(`[Cloudflared] ${msg.trim()}`));
80
- }
81
- }
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
-
113
- static handleError(err, spinner) {
114
- if (spinner) {
115
- spinner.fail("Failed to spawn cloudflared process.");
116
- }
117
- console.error(chalk.red(`Process Error: ${err.message}`));
118
- }
119
-
120
- static handleClose(code) {
121
- if (code !== 0 && code !== null) {
122
- console.log(chalk.red(`Tunnel process exited with code ${code}`));
123
- }
124
- }
125
- }
126
-
@@ -1,139 +0,0 @@
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();