nport 2.0.6 → 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.
package/src/analytics.js DELETED
@@ -1,265 +0,0 @@
1
- // ============================================================================
2
- // Firebase Analytics for CLI
3
- // Using Google Analytics 4 Measurement Protocol
4
- // ============================================================================
5
-
6
- import axios from "axios";
7
- import { createHash, randomUUID } from "crypto";
8
- import os from "os";
9
- import fs from "fs";
10
- import path from "path";
11
- import { fileURLToPath } from "url";
12
-
13
- const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = path.dirname(path.dirname(__filename));
15
-
16
- // Firebase/GA4 Configuration (from website/home.html)
17
- // Full Firebase config for reference (if needed for future features)
18
- const FIREBASE_WEB_CONFIG = {
19
- apiKey: "AIzaSyArRxHZJUt4o2RxiLqX1yDSkuUd6ZFy45I",
20
- authDomain: "nport-link.firebaseapp.com",
21
- projectId: "nport-link",
22
- storageBucket: "nport-link.firebasestorage.app",
23
- messagingSenderId: "515584605320",
24
- appId: "1:515584605320:web:88daabc8d77146c6e7f33d",
25
- measurementId: "G-8MYXZL6PGD"
26
- };
27
-
28
- // Analytics-specific config (for GA4 Measurement Protocol)
29
- const FIREBASE_CONFIG = {
30
- measurementId: FIREBASE_WEB_CONFIG.measurementId,
31
- apiSecret: process.env.NPORT_ANALYTICS_SECRET || "YOUR_API_SECRET_HERE", // Get from Firebase Console
32
- };
33
-
34
- // Analytics Configuration
35
- const ANALYTICS_CONFIG = {
36
- enabled: true, // Can be disabled by environment variable
37
- debug: process.env.NPORT_DEBUG === "true",
38
- timeout: 2000, // Don't block CLI for too long
39
- userIdFile: path.join(os.homedir(), ".nport", "analytics-id"),
40
- };
41
-
42
- // ============================================================================
43
- // Analytics Manager
44
- // ============================================================================
45
-
46
- class AnalyticsManager {
47
- constructor() {
48
- this.userId = null;
49
- this.sessionId = null;
50
- this.disabled = false;
51
-
52
- // Disable analytics if environment variable is set
53
- if (process.env.NPORT_ANALYTICS === "false") {
54
- this.disabled = true;
55
- }
56
- }
57
-
58
- /**
59
- * Initialize analytics - must be called before tracking
60
- */
61
- async initialize() {
62
- if (this.disabled) return;
63
-
64
- // Check if API secret is configured
65
- if (!FIREBASE_CONFIG.apiSecret || FIREBASE_CONFIG.apiSecret === "YOUR_API_SECRET_HERE") {
66
- if (ANALYTICS_CONFIG.debug) {
67
- console.warn("[Analytics] API secret not configured. Analytics disabled.");
68
- console.warn("[Analytics] Set NPORT_ANALYTICS_SECRET environment variable.");
69
- }
70
- this.disabled = true;
71
- return;
72
- }
73
-
74
- try {
75
- this.userId = await this.getUserId();
76
- this.sessionId = this.generateSessionId();
77
-
78
- if (ANALYTICS_CONFIG.debug) {
79
- console.log("[Analytics] Initialized successfully");
80
- console.log("[Analytics] User ID:", this.userId.substring(0, 8) + "...");
81
- }
82
- } catch (error) {
83
- if (ANALYTICS_CONFIG.debug) {
84
- console.error("[Analytics] Initialization failed:", error.message);
85
- }
86
- this.disabled = true;
87
- }
88
- }
89
-
90
- /**
91
- * Get or create a persistent user ID
92
- */
93
- async getUserId() {
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
-
101
- // Try to read existing user ID
102
- if (fs.existsSync(ANALYTICS_CONFIG.userIdFile)) {
103
- const userId = fs.readFileSync(ANALYTICS_CONFIG.userIdFile, "utf8").trim();
104
- if (userId) return userId;
105
- }
106
-
107
- // Generate new anonymous user ID
108
- const userId = this.generateAnonymousId();
109
-
110
- // Save for future use
111
- fs.writeFileSync(ANALYTICS_CONFIG.userIdFile, userId, "utf8");
112
-
113
- return userId;
114
- } catch (error) {
115
- // If file operations fail, use session-based ID
116
- return this.generateAnonymousId();
117
- }
118
- }
119
-
120
- /**
121
- * Generate anonymous user ID based on machine characteristics
122
- */
123
- generateAnonymousId() {
124
- const machineId = [
125
- os.hostname(),
126
- os.platform(),
127
- os.arch(),
128
- os.homedir(),
129
- ].join("-");
130
-
131
- return createHash("sha256").update(machineId).digest("hex").substring(0, 32);
132
- }
133
-
134
- /**
135
- * Generate session ID
136
- */
137
- generateSessionId() {
138
- return randomUUID();
139
- }
140
-
141
- /**
142
- * Track an event
143
- */
144
- async trackEvent(eventName, params = {}) {
145
- if (this.disabled || !ANALYTICS_CONFIG.enabled) return;
146
-
147
- try {
148
- const payload = this.buildPayload(eventName, params);
149
-
150
- // Send to GA4 Measurement Protocol (non-blocking)
151
- axios.post(
152
- `https://www.google-analytics.com/mp/collect?measurement_id=${FIREBASE_CONFIG.measurementId}&api_secret=${FIREBASE_CONFIG.apiSecret}`,
153
- payload,
154
- {
155
- timeout: ANALYTICS_CONFIG.timeout,
156
- headers: { "Content-Type": "application/json" },
157
- }
158
- ).catch((error) => {
159
- // Silently fail - don't interrupt CLI operations
160
- if (ANALYTICS_CONFIG.debug) {
161
- console.error("[Analytics] Failed to send event:", error.message);
162
- }
163
- });
164
-
165
- if (ANALYTICS_CONFIG.debug) {
166
- console.log("[Analytics] Event tracked:", eventName, params);
167
- }
168
- } catch (error) {
169
- // Silently fail
170
- if (ANALYTICS_CONFIG.debug) {
171
- console.error("[Analytics] Error tracking event:", error.message);
172
- }
173
- }
174
- }
175
-
176
- /**
177
- * Build GA4 Measurement Protocol payload
178
- */
179
- buildPayload(eventName, params) {
180
- return {
181
- client_id: this.userId,
182
- events: [
183
- {
184
- name: eventName,
185
- params: {
186
- session_id: this.sessionId,
187
- engagement_time_msec: "100",
188
- ...this.getSystemInfo(),
189
- ...params,
190
- },
191
- },
192
- ],
193
- };
194
- }
195
-
196
- /**
197
- * Get system information for context
198
- */
199
- getSystemInfo() {
200
- return {
201
- os_platform: os.platform(),
202
- os_version: os.release(),
203
- os_arch: os.arch(),
204
- node_version: process.version,
205
- };
206
- }
207
-
208
- /**
209
- * Track CLI start
210
- */
211
- async trackCliStart(port, subdomain, version) {
212
- await this.trackEvent("cli_start", {
213
- port: String(port),
214
- has_custom_subdomain: subdomain && !subdomain.startsWith("user-"),
215
- cli_version: version,
216
- });
217
- }
218
-
219
- /**
220
- * Track tunnel creation
221
- */
222
- async trackTunnelCreated(subdomain, port) {
223
- await this.trackEvent("tunnel_created", {
224
- subdomain_type: subdomain.startsWith("user-") ? "random" : "custom",
225
- port: String(port),
226
- });
227
- }
228
-
229
- /**
230
- * Track tunnel error
231
- */
232
- async trackTunnelError(errorType, errorMessage) {
233
- await this.trackEvent("tunnel_error", {
234
- error_type: errorType,
235
- error_message: errorMessage.substring(0, 100), // Limit length
236
- });
237
- }
238
-
239
- /**
240
- * Track tunnel shutdown
241
- */
242
- async trackTunnelShutdown(reason, durationSeconds) {
243
- await this.trackEvent("tunnel_shutdown", {
244
- shutdown_reason: reason, // "manual", "timeout", "error"
245
- duration_seconds: String(Math.floor(durationSeconds)),
246
- });
247
- }
248
-
249
- /**
250
- * Track CLI update notification shown
251
- */
252
- async trackUpdateAvailable(currentVersion, latestVersion) {
253
- await this.trackEvent("update_available", {
254
- current_version: currentVersion,
255
- latest_version: latestVersion,
256
- });
257
- }
258
- }
259
-
260
- // ============================================================================
261
- // Export singleton instance
262
- // ============================================================================
263
-
264
- export const analytics = new AnalyticsManager();
265
-
package/src/api.js DELETED
@@ -1,89 +0,0 @@
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 DELETED
@@ -1,122 +0,0 @@
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
-