nport 2.1.0 → 2.1.2

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.
@@ -0,0 +1 @@
1
+ var C=Object.defineProperty;var o=(r,n)=>C(r,"name",{value:n,configurable:!0});import s from"fs";import u from"path";import S from"https";import w from"os";import{execSync as _}from"child_process";import{fileURLToPath as E}from"url";var $=E(import.meta.url),v=u.dirname(u.dirname($)),m=u.join(v,"bin"),g="cloudflared",A=".tgz",R="cloudflared.tgz",f=w.platform(),p=w.arch(),h=f==="win32",N=h?`${g}.exe`:g,i=u.join(m,N),P="https://github.com/cloudflare/cloudflared/releases/latest/download",T=[301,302],D=200,M="755",U={darwin:{x64:"cloudflared-darwin-amd64.tgz",arm64:"cloudflared-darwin-arm64.tgz"},win32:{x64:"cloudflared-windows-amd64.exe",ia32:"cloudflared-windows-386.exe"},linux:{x64:"cloudflared-linux-amd64",arm64:"cloudflared-linux-arm64",arm:"cloudflared-linux-arm"}};function b(r){return{x64:"x64",amd64:"amd64",arm64:"arm64",ia32:"ia32",arm:"arm"}[r]||r}o(b,"normalizeArch");function B(){let r=b(p),n=U[f];if(!n)throw new Error(`Unsupported platform: ${f}. Supported platforms: darwin, win32, linux`);let t=n[r];if(!t)throw new Error(`Unsupported architecture: ${p} for platform ${f}. Supported architectures: ${Object.keys(n).join(", ")}`);return`${P}/${t}`}o(B,"getDownloadUrl");function L(r){return r.endsWith(A)}o(L,"isCompressedArchive");function O(r){s.existsSync(r)||s.mkdirSync(r,{recursive:!0})}o(O,"ensureDirectory");function c(r){try{s.existsSync(r)&&s.unlinkSync(r)}catch{}}o(c,"safeUnlink");function x(r,n=M){h||s.chmodSync(r,n)}o(x,"setExecutablePermissions");function z(r,n){if(!s.existsSync(r))throw new Error(n||`File not found: ${r}`)}o(z,"validateFileExists");async function y(r,n){return new Promise((t,d)=>{let e=s.createWriteStream(n);S.get(r,a=>{if(T.includes(a.statusCode)){e.close(),c(n),y(a.headers.location,n).then(t).catch(d);return}if(a.statusCode!==D){e.close(),c(n),d(new Error(`Download failed with status code ${a.statusCode} from ${r}`));return}a.pipe(e),e.on("finish",()=>{e.close(()=>t(n))}),e.on("error",I=>{e.close(),c(n),d(I)})}).on("error",a=>{e.close(),c(n),d(new Error(`Network error: ${a.message}`))})})}o(y,"downloadFile");function F(r,n){try{_(`tar -xzf "${r}" -C "${n}"`,{stdio:"pipe"})}catch(t){throw new Error(`Extraction failed: ${t.message}`)}}o(F,"extractTarGz");var l={info:o(r=>console.log(`\u2139\uFE0F ${r}`),"info"),success:o(r=>console.log(`\u2705 ${r}`),"success"),warn:o(r=>console.warn(`\u26A0\uFE0F ${r}`),"warn"),error:o(r=>console.error(`\u274C ${r}`),"error"),progress:o(r=>console.log(`\u{1F6A7} ${r}`),"progress"),extract:o(r=>console.log(`\u{1F4E6} ${r}`),"extract")};async function k(){l.progress("Cloudflared binary not found. Downloading... (This happens only once)");let r=B(),n=L(r),t=n?u.join(m,R):i;try{return await y(r,t),n&&(l.extract("Extracting binary..."),F(t,m),c(t),z(i,"Extraction failed: Binary not found after extraction")),x(i),l.success("Download complete."),i}catch(d){throw c(t),c(i),d}}o(k,"installBinary");async function G(){if(O(m),s.existsSync(i))return x(i),i;try{return await k()}catch(r){l.error(`Installation failed: ${r.message}`),process.exit(1)}}o(G,"ensureCloudflared");function H(){return!!(process.env.CI||process.env.GITHUB_ACTIONS||process.env.GITLAB_CI||process.env.CIRCLECI||process.env.TRAVIS||process.env.JENKINS_URL||process.env.BUILDKITE)}o(H,"isCI");async function W(){if(H()){l.info("Running in CI environment - skipping binary download");return}try{let r=await G();l.success(`Cloudflared binary is ready at: ${r}`)}catch(r){l.error(r.message),process.exit(1)}}o(W,"main");var j=E(import.meta.url);(process.argv[1]===j||process.argv[1]?.endsWith("bin-manager.js"))&&W();export{G as ensureCloudflared};
package/dist/index.js ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ var he=Object.defineProperty;var i=(o,e)=>he(o,"name",{value:e,configurable:!0});import on from"ora";import b from"chalk";import P from"path";import{fileURLToPath as Ie}from"url";import{createRequire as we}from"module";var Q="https://api.nport.link";var X="user-";var $={THRESHOLD:5,COOLDOWN:3e4},Z={SUCCESS:["Registered tunnel connection"],ERROR:["ERR","error"],NETWORK_WARNING:["failed to accept QUIC stream","failed to dial to edge with quic","failed to accept incoming stream requests","Failed to dial a quic connection","timeout: no recent network activity","failed to dial to edge","quic:"],IGNORE:["Cannot determine default origin certificate path","No file cert.pem","origincert option","TUNNEL_ORIGIN_CERT","context canceled","failed to run the datagram handler","failed to serve tunnel connection","Connection terminated","no more connections active and exiting","Serve tunnel error","accept stream listener encountered a failure","Retrying connection","icmp router terminated","use of closed network connection","Application error 0x0"]},ee=["en","vi"],ne="https://github.com/tuanngocptn/nport",B="https://nport.link",te="https://buymeacoffee.com/tuanngocptn";var Ee=Ie(import.meta.url),oe=P.dirname(P.dirname(Ee)),be=we(import.meta.url),re=be("../package.json");function Ce(){return process.env.NPORT_BACKEND_URL?process.env.NPORT_BACKEND_URL:Q}i(Ce,"getBackendUrl");var u={PACKAGE_NAME:re.name,CURRENT_VERSION:re.version,BACKEND_URL:Ce(),DEFAULT_PORT:8080,SUBDOMAIN_PREFIX:X,TUNNEL_TIMEOUT_HOURS:4,UPDATE_CHECK_TIMEOUT:3e3},ke={IS_WINDOWS:process.platform==="win32",BIN_NAME:process.platform==="win32"?"cloudflared.exe":"cloudflared"},x={BIN_DIR:P.join(oe,"bin"),BIN_PATH:P.join(oe,"bin",ke.BIN_NAME)},U=Z,W={WARNING_THRESHOLD:$.THRESHOLD,WARNING_COOLDOWN:$.COOLDOWN},se=u.TUNNEL_TIMEOUT_HOURS*60*60*1e3;var O=class{static{i(this,"ArgumentParser")}static parse(e){let n=this.parsePort(e),t=this.parseSubdomain(e),r=this.parseLanguage(e),c=this.parseBackendUrl(e),g=this.parseSetBackend(e);return{port:n,subdomain:t,language:r,backendUrl:c,setBackend:g}}static parsePort(e){return parseInt(e[0],10)||u.DEFAULT_PORT}static parseSubdomain(e){let n=[()=>this.findFlagWithEquals(e,"--subdomain="),()=>this.findFlagWithEquals(e,"-s="),()=>this.findFlagWithValue(e,"--subdomain"),()=>this.findFlagWithValue(e,"-s")];for(let t of n){let r=t();if(r)return r}return this.generateRandomSubdomain()}static parseLanguage(e){if(e.includes("--language")||e.includes("--lang")||e.includes("-l")){let t=e.indexOf("--language")!==-1?e.indexOf("--language"):e.indexOf("--lang")!==-1?e.indexOf("--lang"):e.indexOf("-l"),r=e[t+1];if(!r||r.startsWith("-"))return"prompt"}let n=[()=>this.findFlagWithEquals(e,"--language="),()=>this.findFlagWithEquals(e,"--lang="),()=>this.findFlagWithEquals(e,"-l="),()=>this.findFlagWithValue(e,"--language"),()=>this.findFlagWithValue(e,"--lang"),()=>this.findFlagWithValue(e,"-l")];for(let t of n){let r=t();if(r)return r}return null}static parseBackendUrl(e){let n=[()=>this.findFlagWithEquals(e,"--backend="),()=>this.findFlagWithEquals(e,"-b="),()=>this.findFlagWithValue(e,"--backend"),()=>this.findFlagWithValue(e,"-b")];for(let t of n){let r=t();if(r)return r}return null}static parseSetBackend(e){let n=[()=>this.findFlagWithEquals(e,"--set-backend="),()=>this.findFlagWithValue(e,"--set-backend")];for(let t of n){let r=t();if(r)return r}return e.includes("--set-backend")?"clear":null}static findFlagWithEquals(e,n){let t=e.find(r=>r.startsWith(n));return t?t.split("=")[1]:null}static findFlagWithValue(e,n){let t=e.indexOf(n);return t!==-1&&e[t+1]?e[t+1]:null}static generateRandomSubdomain(){return`${u.SUBDOMAIN_PREFIX}${Math.floor(Math.random()*1e4)}`}};import nn from"ora";import J from"chalk";import tn from"fs";var M=class{static{i(this,"TunnelState")}tunnelId=null;subdomain=null;port=null;backendUrl=null;tunnelProcess=null;timeoutId=null;connectionCount=0;startTime=null;updateInfo=null;networkIssueCount=0;lastNetworkWarningTime=0;networkWarningShown=!1;setTunnel(e,n,t,r=null){this.tunnelId=e,this.subdomain=n,this.port=t,this.backendUrl=r,this.startTime||(this.startTime=Date.now())}setUpdateInfo(e){this.updateInfo=e}setProcess(e){this.tunnelProcess=e}setTimeout(e){this.timeoutId=e}clearTimeout(){this.timeoutId&&(clearTimeout(this.timeoutId),this.timeoutId=null)}incrementConnection(){return this.connectionCount++,this.connectionCount}hasTunnel(){return this.tunnelId!==null}hasProcess(){return this.tunnelProcess!==null&&!this.tunnelProcess.killed}getDurationSeconds(){return this.startTime?(Date.now()-this.startTime)/1e3:0}incrementNetworkIssue(){return this.networkIssueCount++,this.networkIssueCount}resetNetworkIssues(){this.networkIssueCount=0}shouldShowNetworkWarning(e,n){let t=Date.now();return this.networkIssueCount>=e&&t-this.lastNetworkWarningTime>n?(this.lastNetworkWarningTime=t,!0):!1}setNetworkWarningShown(e){this.networkWarningShown=e}reset(){this.clearTimeout(),this.tunnelId=null,this.subdomain=null,this.port=null,this.backendUrl=null,this.tunnelProcess=null,this.connectionCount=0,this.startTime=null,this.updateInfo=null,this.networkIssueCount=0,this.lastNetworkWarningTime=0,this.networkWarningShown=!1}},l=new M;import{spawn as Se}from"child_process";import p from"chalk";import Ue from"fs";import a from"chalk";import Ne from"readline";import T from"fs";import G from"path";import ve from"os";var V=class{static{i(this,"ConfigManager")}configDir;configFile;oldLangFile;config;constructor(){this.configDir=G.join(ve.homedir(),".nport"),this.configFile=G.join(this.configDir,"config.json"),this.oldLangFile=G.join(this.configDir,"lang"),this.config=this.loadConfig(),this.migrateOldConfig()}loadConfig(){try{if(T.existsSync(this.configFile)){let e=T.readFileSync(this.configFile,"utf8");return JSON.parse(e)}}catch{console.warn("Warning: Could not load config file, using defaults")}return{}}migrateOldConfig(){try{if(!this.config.language&&T.existsSync(this.oldLangFile)){let e=T.readFileSync(this.oldLangFile,"utf8").trim();if(e&&["en","vi"].includes(e)){this.config.language=e,this.saveConfig();try{T.unlinkSync(this.oldLangFile)}catch{}}}}catch{}}saveConfig(){try{return T.existsSync(this.configDir)||T.mkdirSync(this.configDir,{recursive:!0}),T.writeFileSync(this.configFile,JSON.stringify(this.config,null,2),"utf8"),!0}catch{return console.warn("Warning: Could not save configuration"),!1}}getBackendUrl(){return this.config.backendUrl??null}setBackendUrl(e){return e?this.config.backendUrl=e:delete this.config.backendUrl,this.saveConfig()}getLanguage(){return this.config.language??null}setLanguage(e){return e?this.config.language=e:delete this.config.language,this.saveConfig()}getAll(){return{...this.config}}clear(){return this.config={},this.saveConfig()}},I=new V;var H={en:{header:"N P O R T \u26A1\uFE0F Free & Open Source from Vietnam \u2764\uFE0F",creatingTunnel:"Creating tunnel for port {port}...",checkingUpdates:"Checking for updates...",tunnelLive:"\u{1F680} WE LIVE BABY!",connection1:" \u2714 [1/2] Connection established...",connection2:" \u2714 [2/2] Compression enabled...",timeRemaining:"\u23F1\uFE0F Time: {hours}h remaining",footerTitle:"\u{1F525} KEEP THE VIBE ALIVE?",footerSubtitle:"(Made with \u2764\uFE0F in Vietnam)",dropStar:"\u2B50\uFE0F Drop a Star: ",sendCoffee:"\u2615\uFE0F Buy Coffee: ",newVersion:"\u{1F6A8} NEW VERSION (v{version}) detected!",updateCommand:"> npm install -g nport@latest",tunnelShutdown:"\u{1F6D1} TUNNEL SHUTDOWN.",cleaningUp:"Cleaning up... ",cleanupDone:"Done.",cleanupFailed:"Failed.",subdomainReleased:"Subdomain... Released. \u{1F5D1}\uFE0F",serverBusy:"(Server might be down or busy)",goodbyeTitle:"\u{1F44B} BEFORE YOU GO...",goodbyeMessage:"Thanks for using NPort!",website:"\u{1F310} Website: ",author:"\u{1F464} Author: ",changeLanguage:"\u{1F30D} Language: ",changeLanguageHint:"nport --language",versionTitle:"NPort v{version}",versionSubtitle:"Free & open source ngrok alternative",versionLatest:"\u2714 You're running the latest version!",versionAvailable:"\u{1F6A8} New version available: v{version}",versionUpdate:"Update now: ",learnMore:"Learn more: ",languagePrompt:`
3
+ \u{1F30D} Language Selection / Ch\u1ECDn ng\xF4n ng\u1EEF
4
+ `,languageQuestion:"Choose your language (1-2): ",languageEnglish:"1. English",languageVietnamese:"2. Ti\u1EBFng Vi\u1EC7t (Vietnamese)",languageInvalid:"Invalid choice. Using English by default.",languageSaved:"\u2714 Language preference saved!",networkIssueTitle:`
5
+ \u26A0\uFE0F NETWORK CONNECTIVITY ISSUE DETECTED`,networkIssueDesc:" Cloudflared is having trouble maintaining a stable connection to Cloudflare's edge servers.",networkIssueTunnel:" \u{1F4E1} Your tunnel is still working, but connection quality may be affected.",networkIssueReasons:`
6
+ \u{1F4A1} Possible reasons:`,networkIssueReason1:" \u2022 Unstable internet connection or high packet loss",networkIssueReason2:" \u2022 Firewall/Router blocking UDP traffic (QUIC protocol)",networkIssueReason3:" \u2022 ISP throttling or network congestion",networkIssueFix:`
7
+ \u{1F527} What to try:`,networkIssueFix1:" \u2022 Check your internet connection stability",networkIssueFix2:" \u2022 Try connecting from a different network",networkIssueFix3:" \u2022 Disable VPN/Proxy if you're using one",networkIssueFix4:" \u2022 The tunnel will automatically fallback to HTTP/2 if QUIC fails",networkIssueIgnore:`
8
+ \u2139\uFE0F This is usually not critical - your tunnel should continue working normally.
9
+ `},vi:{header:"N P O R T \u26A1\uFE0F Vi\u1EC7t Nam M\xE3i \u0110\u1EC9nh \u2764\uFE0F",creatingTunnel:"\u{1F6E0}\uFE0F \u0110ang kh\u1EDFi \u0111\u1ED9ng c\u1ED5ng {port}... Chu\u1EA9n b\u1ECB bay n\xE0o!",checkingUpdates:"\u{1F50D} \u0110ang d\xF2 la b\u1EA3n c\u1EADp nh\u1EADt m\u1EDBi... \u0110\u1EE3i t\xED s\u1EAFp c\xF3 qu\xE0!",tunnelLive:"\u{1F680} B\u1EACT MODE T\u1ED0C H\xC0NH! \u0110ANG BAY R\u1ED2I N\xC8!",connection1:" \u2714 [1/2] \u0110ang c\u1EAFm d\xE2y m\u1EA1ng v\u0169 tr\u1EE5...",connection2:" \u2714 [2/2] \u0110ang b\u01A1m si\xEAu n\xE9n kh\xED t\u1ED1c \u0111\u1ED9 \xE1nh s\xE1ng...",timeRemaining:"\u23F1\uFE0F T\u0103ng t\u1ED1c th\u1EA7n s\u1EA7u: C\xF2n {hours}h \u0111\u1EC3 qu\u1EA9y!",footerTitle:"\u{1F525} L\u01AFU DANH S\u1EEC S\xC1CH! \u0110\u1EEANG QU\xCAN STAR \u2B50\uFE0F",footerSubtitle:"(Made in Vi\u1EC7t Nam, chu\u1EA9n kh\xF4ng c\u1EA7n ch\u1EC9nh! \u2764\uFE0F)",dropStar:"\u2B50\uFE0F Th\u1EA3 Star: ",sendCoffee:"\u2615\uFE0F T\u1EB7ng Coffee: ",newVersion:"\u{1F680} B\u1EA2N M\u1EDAI (v{version}) v\u1EEBa h\u1EA1 c\xE1nh!",updateCommand:"\u{1F4A1} G\xF5 li\u1EC1n: npm install -g nport@latest",tunnelShutdown:"\u{1F6D1} \u0110\xE3 t\u1EDBi gi\u1EDD 'ch\u1ED1t' deal r\u1ED3i c\u1EA3 nh\xE0 \u01A1i...",cleaningUp:"\u0110ang d\u1ECDn d\u1EB9p chi\u1EBFn tr\u01B0\u1EDDng... \u{1F9F9}",cleanupDone:"X\u1ECBn x\xF2! \u0110\xE3 d\u1ECDn xong r\u1ED3i n\xE8.",cleanupFailed:"O\u1EB1n tr\u1EDDi, d\u1ECDn kh\xF4ng n\u1ED5i!",subdomainReleased:"Subdomain... X\xED xo\xE1! T\u1EA1m bi\u1EC7t nh\xE9 \u{1F5D1}\uFE0F\u2728",serverBusy:"(C\xF3 th\u1EC3 server \u0111ang b\u1EADn order tr\xE0 s\u1EEFa)",goodbyeTitle:"\u{1F44B} G\u1EB6P L\u1EA0I B\u1EA0N \u1EDE \u0110\u01AF\u1EDCNG B\u0102NG KH\xC1C...",goodbyeMessage:"C\u1EA3m \u01A1n \u0111\xE3 qu\u1EA9y NPort! L\u1EA7n sau ch\u01A1i ti\u1EBFp nha \u{1F618}",website:"\u{1F310} S\xE2n ch\u01A1i ch\xEDnh: ",author:"\u{1F464} Nh\xE0 t\xE0i tr\u1EE3: ",changeLanguage:"\u{1F30D} \u0110\u1ED5i ng\xF4n ng\u1EEF: ",changeLanguageHint:"nport --language",versionTitle:"NPort v{version}",versionSubtitle:"H\u01A1n c\u1EA3 Ngrok - Ma-de in Vi\u1EC7t Nam",versionLatest:"\u{1F389} Ch\xFAc m\u1EEBng! \u0110ang c\xF9ng server v\u1EDBi b\u1EA3n m\u1EDBi nh\u1EA5t!",versionAvailable:"\u{1F31F} V\xE8o v\xE8o: C\xF3 b\u1EA3n m\u1EDBi v{version} v\u1EEBa c\u1EADp b\u1EBFn!",versionUpdate:"Update kh\u1EA9n tr\u01B0\u01A1ng l\u1EB9 l\xE0ng: ",learnMore:"Kh\xE1m ph\xE1 th\xEAm cho n\xF3ng: ",languagePrompt:`
10
+ \u{1F30D} Ch\u1ECDn l\u1EF1a ng\xF4n ng\u1EEF ngay b\xEAn d\u01B0\u1EDBi n\xE0o!
11
+ `,languageQuestion:"Ch\u1EDBp l\u1EA5y m\u1ED9t l\u1EF1a ch\u1ECDn nha (1-2): ",languageEnglish:"1. English (Chu\u1EA9n qu\u1ED1c t\u1EBF!)",languageVietnamese:"2. Ti\u1EBFng Vi\u1EC7t (\u0110\u1EC9nh c\u1EE7a ch\xF3p)",languageInvalid:"\u01A0 h\u01A1, ch\u1ECDn sai r\u1ED3i! M\u1EB7c \u0111\u1ECBnh Ti\u1EBFng Vi\u1EC7t lu\xF4n cho n\xF3ng.",languageSaved:"\u{1F3AF} Xong r\u1ED3i! L\u01B0u ng\xF4n ng\u1EEF th\xE0nh c\xF4ng!",networkIssueTitle:`
12
+ \u26A0\uFE0F PH\xC1T HI\u1EC6N V\u1EA4N \u0110\u1EC0 M\u1EA0NG`,networkIssueDesc:" Cloudflared \u0111ang g\u1EB7p kh\xF3 kh\u0103n khi gi\u1EEF k\u1EBFt n\u1ED1i \u1ED5n \u0111\u1ECBnh t\u1EDBi Cloudflare edge servers.",networkIssueTunnel:" \u{1F4E1} Tunnel c\u1EE7a b\u1EA1n v\u1EABn ho\u1EA1t \u0111\u1ED9ng, nh\u01B0ng ch\u1EA5t l\u01B0\u1EE3ng k\u1EBFt n\u1ED1i c\xF3 th\u1EC3 b\u1ECB \u1EA3nh h\u01B0\u1EDFng.",networkIssueReasons:`
13
+ \u{1F4A1} C\xF3 th\u1EC3 do:`,networkIssueReason1:" \u2022 M\u1EA1ng internet kh\xF4ng \u1ED5n \u0111\u1ECBnh ho\u1EB7c m\u1EA5t g\xF3i tin",networkIssueReason2:" \u2022 Firewall/Router ch\u1EB7n UDP traffic (giao th\u1EE9c QUIC)",networkIssueReason3:" \u2022 Nh\xE0 m\u1EA1ng throttle ho\u1EB7c t\u1EAFc ngh\u1EBDn m\u1EA1ng",networkIssueFix:`
14
+ \u{1F527} Th\u1EED c\xE1c c\xE1ch sau:`,networkIssueFix1:" \u2022 Ki\u1EC3m tra k\u1EBFt n\u1ED1i internet c\u1EE7a b\u1EA1n",networkIssueFix2:" \u2022 Th\u1EED \u0111\u1ED5i sang m\u1EA1ng kh\xE1c (v\xED d\u1EE5: 4G/5G)",networkIssueFix3:" \u2022 T\u1EAFt VPN/Proxy n\u1EBFu \u0111ang b\u1EADt",networkIssueFix4:" \u2022 Tunnel s\u1EBD t\u1EF1 \u0111\u1ED9ng chuy\u1EC3n sang HTTP/2 n\u1EBFu QUIC fail",networkIssueIgnore:`
15
+ \u2139\uFE0F L\u1ED7i n\xE0y th\u01B0\u1EDDng kh\xF4ng nghi\xEAm tr\u1ECDng - tunnel v\u1EABn ho\u1EA1t \u0111\u1ED9ng b\xECnh th\u01B0\u1EDDng.
16
+ `}},j=class{static{i(this,"LanguageManager")}currentLanguage="en";availableLanguages=ee;t(e,n={}){let r=(H[this.currentLanguage]||H.en)[e]||H.en[e]||e;return Object.keys(n).forEach(c=>{let g=n[c];g!==void 0&&(r=r.replace(`{${c}}`,String(g)))}),r}loadLanguagePreference(){let e=I.getLanguage();return e&&this.availableLanguages.includes(e)?e:null}saveLanguagePreference(e){I.setLanguage(e)}setLanguage(e){return this.availableLanguages.includes(e)?(this.currentLanguage=e,!0):!1}getLanguage(){return this.currentLanguage}async promptLanguageSelection(){return new Promise(e=>{let n=Ne.createInterface({input:process.stdin,output:process.stdout});console.log(this.t("languagePrompt")),console.log(` ${this.t("languageEnglish")}`),console.log(` ${this.t("languageVietnamese")}
17
+ `),n.question(`${this.t("languageQuestion")}`,t=>{n.close();let r=t.trim(),c="en";r==="1"?c="en":r==="2"?c="vi":console.log(`
18
+ ${this.t("languageInvalid")}
19
+ `),this.setLanguage(c),this.saveLanguagePreference(c),console.log(`${this.t("languageSaved")}
20
+ `),e(c)})})}async initialize(e=null){if(e&&e!=="prompt"&&this.setLanguage(e))return this.saveLanguagePreference(e),e;if(e==="prompt")return await this.promptLanguageSelection();let n=this.loadLanguagePreference();return n?(this.setLanguage(n),n):await this.promptLanguageSelection()}},s=new j;var m=class{static{i(this,"UI")}static displayProjectInfo(){let e="\u2500".repeat(56),n=s.t("header"),r=" ".repeat(Math.max(0,59-n.length-4));console.log(a.gray(`
21
+ \u256D${e}\u256E`)),console.log(a.cyan.bold(` \u2502 ${n}`)+r+a.gray("\u2502")),console.log(a.gray(` \u2570${e}\u256F
22
+ `))}static displayStartupBanner(e){this.displayProjectInfo()}static displayTunnelSuccess(e,n,t){console.log(),console.log(a.cyan.bold(` \u{1F449} ${e} \u{1F448}
23
+ `)),console.log(a.gray(" "+"\u2500".repeat(54)+`
24
+ `)),console.log(a.gray(` ${s.t("timeRemaining",{hours:u.TUNNEL_TIMEOUT_HOURS})}
25
+ `))}static displayFooter(e){console.log(a.gray(" "+"\u2500".repeat(54)+`
26
+ `)),console.log(a.yellow.bold(` ${s.t("footerTitle")}
27
+ `)),console.log(a.gray(` ${s.t("footerSubtitle")}
28
+ `)),console.log(a.cyan(` ${s.t("dropStar")}`)+a.white(ne)),console.log(a.yellow(` ${s.t("sendCoffee")}`)+a.white(te)),e?.shouldUpdate&&(console.log(a.red.bold(`
29
+ ${s.t("newVersion",{version:e.latest})}`)),console.log(a.gray(" ")+a.cyan(s.t("updateCommand")))),console.log()}static displayTimeoutWarning(){console.log(a.yellow(`
30
+ \u23F0 Tunnel has been running for ${u.TUNNEL_TIMEOUT_HOURS} hours.`)),console.log(a.yellow(" Automatically shutting down..."))}static displayError(e,n=null){n&&n.fail("Failed to connect to server."),console.error(a.red(e.message))}static displayCleanupStart(){console.log(a.red.bold(`
31
+
32
+ ${s.t("tunnelShutdown")}
33
+ `)),process.stdout.write(a.gray(` ${s.t("cleaningUp")}`))}static displayCleanupSuccess(){console.log(a.green(s.t("cleanupDone"))),console.log(a.gray(` ${s.t("subdomainReleased")}
34
+ `)),this.displayGoodbye()}static displayCleanupError(){console.log(a.red(s.t("cleanupFailed"))),console.log(a.gray(` ${s.t("serverBusy")}
35
+ `)),this.displayGoodbye()}static displayGoodbye(){console.log(a.gray(" "+"\u2500".repeat(54)+`
36
+ `)),console.log(a.cyan.bold(` ${s.t("goodbyeTitle")}
37
+ `)),console.log(a.gray(` ${s.t("goodbyeMessage")}
38
+ `)),console.log(a.cyan(` ${s.t("website")}`)+a.white(B)),console.log(a.cyan(` ${s.t("author")}`)+a.white("Nick Pham (https://github.com/tuanngocptn)")),console.log(a.cyan(` ${s.t("changeLanguage")}`)+a.yellow(s.t("changeLanguageHint"))),console.log()}static displayVersion(e,n){console.log(a.cyan.bold(`
39
+ ${s.t("versionTitle",{version:e})}`)),console.log(a.gray(`${s.t("versionSubtitle")}
40
+ `)),n?.shouldUpdate?(console.log(a.yellow(s.t("versionAvailable",{version:n.latest}))),console.log(a.cyan(s.t("versionUpdate"))+a.white(`npm install -g nport@latest
41
+ `))):console.log(a.green(`${s.t("versionLatest")}
42
+ `)),console.log(a.gray(s.t("learnMore"))+a.cyan(`${B}
43
+ `))}};var v=class{static{i(this,"BinaryManager")}static validate(e){return Ue.existsSync(e)?!0:(console.error(p.red(`
44
+ \u274C Error: Cloudflared binary not found at: ${e}`)),console.error(p.yellow(`\u{1F449} Please run 'npm install' again to download the binary.
45
+ `)),!1)}static spawn(e,n,t){return Se(e,["tunnel","run","--token",n,"--url",`http://localhost:${t}`])}static attachHandlers(e,n=null){e.stderr?.on("data",t=>this.handleStderr(t)),e.on("error",t=>this.handleError(t,n)),e.on("close",t=>this.handleClose(t))}static handleStderr(e){let n=e.toString();if(!U.IGNORE.some(t=>n.includes(t))){if(U.NETWORK_WARNING.some(t=>n.includes(t))){this.handleNetworkWarning();return}if(U.SUCCESS.some(t=>n.includes(t))){let t=l.incrementConnection();t===1?(l.resetNetworkIssues(),console.log(p.green(s.t("connection1")))):t===4&&(console.log(p.green(s.t("connection2"))),m.displayFooter(l.updateInfo));return}U.ERROR.some(t=>n.includes(t))&&console.error(p.red(`[Cloudflared] ${n.trim()}`))}}static handleNetworkWarning(){l.incrementNetworkIssue(),l.shouldShowNetworkWarning(W.WARNING_THRESHOLD,W.WARNING_COOLDOWN)&&this.displayNetworkWarning()}static displayNetworkWarning(){console.log(p.yellow(s.t("networkIssueTitle"))),console.log(p.gray(s.t("networkIssueDesc"))),console.log(p.cyan(s.t("networkIssueTunnel"))),console.log(p.yellow(s.t("networkIssueReasons"))),console.log(p.gray(s.t("networkIssueReason1"))),console.log(p.gray(s.t("networkIssueReason2"))),console.log(p.gray(s.t("networkIssueReason3"))),console.log(p.yellow(s.t("networkIssueFix"))),console.log(p.gray(s.t("networkIssueFix1"))),console.log(p.gray(s.t("networkIssueFix2"))),console.log(p.gray(s.t("networkIssueFix3"))),console.log(p.gray(s.t("networkIssueFix4"))),console.log(p.blue(s.t("networkIssueIgnore")))}static handleError(e,n){n&&n.fail("Failed to spawn cloudflared process."),console.error(p.red(`Process Error: ${e.message}`))}static handleClose(e){e!==0&&e!==null&&console.log(p.red(`Tunnel process exited with code ${e}`))}};import E from"fs";import R from"path";import Re from"https";import le from"os";import{execSync as _e}from"child_process";import{fileURLToPath as ce}from"url";var Le=ce(import.meta.url),Ae=R.dirname(R.dirname(Le)),D=R.join(Ae,"bin"),ie="cloudflared",Pe=".tgz",xe="cloudflared.tgz",F=le.platform(),ae=le.arch(),ue=F==="win32",Oe=ue?`${ie}.exe`:ie,w=R.join(D,Oe),Fe="https://github.com/cloudflare/cloudflared/releases/latest/download",De=[301,302],$e=200,Be="755",We={darwin:{x64:"cloudflared-darwin-amd64.tgz",arm64:"cloudflared-darwin-arm64.tgz"},win32:{x64:"cloudflared-windows-amd64.exe",ia32:"cloudflared-windows-386.exe"},linux:{x64:"cloudflared-linux-amd64",arm64:"cloudflared-linux-arm64",arm:"cloudflared-linux-arm"}};function Me(o){return{x64:"x64",amd64:"amd64",arm64:"arm64",ia32:"ia32",arm:"arm"}[o]||o}i(Me,"normalizeArch");function Ge(){let o=Me(ae),e=We[F];if(!e)throw new Error(`Unsupported platform: ${F}. Supported platforms: darwin, win32, linux`);let n=e[o];if(!n)throw new Error(`Unsupported architecture: ${ae} for platform ${F}. Supported architectures: ${Object.keys(e).join(", ")}`);return`${Fe}/${n}`}i(Ge,"getDownloadUrl");function Ve(o){return o.endsWith(Pe)}i(Ve,"isCompressedArchive");function He(o){E.existsSync(o)||E.mkdirSync(o,{recursive:!0})}i(He,"ensureDirectory");function C(o){try{E.existsSync(o)&&E.unlinkSync(o)}catch{}}i(C,"safeUnlink");function ge(o,e=Be){ue||E.chmodSync(o,e)}i(ge,"setExecutablePermissions");function je(o,e){if(!E.existsSync(o))throw new Error(e||`File not found: ${o}`)}i(je,"validateFileExists");async function de(o,e){return new Promise((n,t)=>{let r=E.createWriteStream(e);Re.get(o,c=>{if(De.includes(c.statusCode)){r.close(),C(e),de(c.headers.location,e).then(n).catch(t);return}if(c.statusCode!==$e){r.close(),C(e),t(new Error(`Download failed with status code ${c.statusCode} from ${o}`));return}c.pipe(r),r.on("finish",()=>{r.close(()=>n(e))}),r.on("error",g=>{r.close(),C(e),t(g)})}).on("error",c=>{r.close(),C(e),t(new Error(`Network error: ${c.message}`))})})}i(de,"downloadFile");function Ke(o,e){try{_e(`tar -xzf "${o}" -C "${e}"`,{stdio:"pipe"})}catch(n){throw new Error(`Extraction failed: ${n.message}`)}}i(Ke,"extractTarGz");var k={info:i(o=>console.log(`\u2139\uFE0F ${o}`),"info"),success:i(o=>console.log(`\u2705 ${o}`),"success"),warn:i(o=>console.warn(`\u26A0\uFE0F ${o}`),"warn"),error:i(o=>console.error(`\u274C ${o}`),"error"),progress:i(o=>console.log(`\u{1F6A7} ${o}`),"progress"),extract:i(o=>console.log(`\u{1F4E6} ${o}`),"extract")};async function qe(){k.progress("Cloudflared binary not found. Downloading... (This happens only once)");let o=Ge(),e=Ve(o),n=e?R.join(D,xe):w;try{return await de(o,n),e&&(k.extract("Extracting binary..."),Ke(n,D),C(n),je(w,"Extraction failed: Binary not found after extraction")),ge(w),k.success("Download complete."),w}catch(t){throw C(n),C(w),t}}i(qe,"installBinary");async function K(){if(He(D),E.existsSync(w))return ge(w),w;try{return await qe()}catch(o){k.error(`Installation failed: ${o.message}`),process.exit(1)}}i(K,"ensureCloudflared");function ze(){return!!(process.env.CI||process.env.GITHUB_ACTIONS||process.env.GITLAB_CI||process.env.CIRCLECI||process.env.TRAVIS||process.env.JENKINS_URL||process.env.BUILDKITE)}i(ze,"isCI");async function Je(){if(ze()){k.info("Running in CI environment - skipping binary download");return}try{let o=await K();k.success(`Cloudflared binary is ready at: ${o}`)}catch(o){k.error(o.message),process.exit(1)}}i(Je,"main");var Ye=ce(import.meta.url);(process.argv[1]===Ye||process.argv[1]?.endsWith("bin-manager.js"))&&Je();import pe from"axios";import d from"chalk";var _=class{static{i(this,"APIClient")}static async createTunnel(e,n=null){let t=n||u.BACKEND_URL;try{let{data:r}=await pe.post(t,{subdomain:e});if(!r.success)throw new Error(r.error||"Unknown error from backend");return{tunnelId:r.tunnelId,tunnelToken:r.tunnelToken,url:r.url}}catch(r){throw this.handleError(r,e)}}static async deleteTunnel(e,n,t=null){let r=t||u.BACKEND_URL;await pe.delete(r,{data:{subdomain:e,tunnelId:n}})}static handleError(e,n){let t=e.response?.data?.error;return t?t.includes("SUBDOMAIN_PROTECTED:")?new Error(`Subdomain "${n}" is already taken or in use.
46
+
47
+ `+d.yellow(`\u{1F4A1} Try one of these options:
48
+ `)+d.gray(" 1. Choose a different subdomain: ")+d.cyan(`nport ${l.port||u.DEFAULT_PORT} -s ${n}-v2
49
+ `)+d.gray(" 2. Use a random subdomain: ")+d.cyan(`nport ${l.port||u.DEFAULT_PORT}
50
+ `)+d.gray(" 3. Wait a few minutes and retry if you just stopped a tunnel with this name")):t.includes("SUBDOMAIN_IN_USE:")||t.includes("currently in use")||t.includes("already exists and is currently active")?new Error(d.red(`\u2717 Subdomain "${n}" is already in use!
51
+
52
+ `)+d.yellow(`\u{1F4A1} This subdomain is currently being used by another active tunnel.
53
+
54
+ `)+d.white(`Choose a different subdomain:
55
+ `)+d.gray(" 1. Add a suffix: ")+d.cyan(`nport ${l.port||u.DEFAULT_PORT} -s ${n}-2
56
+ `)+d.gray(" 2. Try a variation: ")+d.cyan(`nport ${l.port||u.DEFAULT_PORT} -s my-${n}
57
+ `)+d.gray(" 3. Use random name: ")+d.cyan(`nport ${l.port||u.DEFAULT_PORT}
58
+ `)):t.includes("already have a tunnel")||t.includes("[1013]")?new Error(`Subdomain "${n}" is already taken or in use.
59
+
60
+ `+d.yellow(`\u{1F4A1} Try one of these options:
61
+ `)+d.gray(" 1. Choose a different subdomain: ")+d.cyan(`nport ${l.port||u.DEFAULT_PORT} -s ${n}-v2
62
+ `)+d.gray(" 2. Use a random subdomain: ")+d.cyan(`nport ${l.port||u.DEFAULT_PORT}
63
+ `)+d.gray(" 3. Wait a few minutes and retry if you just stopped a tunnel with this name")):new Error(`Backend Error: ${t}`):e.response?new Error(`Backend Error: ${JSON.stringify(e.response.data,null,2)}`):e}};import en from"axios";import Xe from"axios";import{createHash as Ze}from"crypto";import y from"os";import L from"fs";import me from"path";var q={measurementId:"G-JJHG4DP1K9",apiSecret:"NjNID8jtRJe9s8uSBz2jfw"},h={enabled:!0,debug:process.env.NPORT_DEBUG==="true",timeout:2e3,userIdFile:me.join(y.homedir(),".nport","analytics-id")},z=class{static{i(this,"AnalyticsManager")}userId=null;sessionId=null;sessionStartTime=null;disabled=!1;constructor(){process.env.NPORT_ANALYTICS==="false"&&(this.disabled=!0)}async initialize(){if(!this.disabled){if(!q.apiSecret){h.debug&&console.warn("[Analytics] API secret not configured. Analytics disabled."),this.disabled=!0;return}try{this.userId=await this.getUserId(),this.sessionId=this.generateSessionId(),this.sessionStartTime=Date.now(),h.debug&&(console.log("[Analytics] Initialized successfully"),console.log(`[Analytics] User ID: ${this.userId}`),console.log(`[Analytics] Session ID: ${this.sessionId}`))}catch(e){h.debug&&console.warn("[Analytics] Failed to initialize:",e),this.disabled=!0}}}async getUserId(){try{let e=me.join(y.homedir(),".nport");if(L.existsSync(e)||L.mkdirSync(e,{recursive:!0}),L.existsSync(h.userIdFile)){let t=L.readFileSync(h.userIdFile,"utf8").trim();if(t)return t}let n=this.generateAnonymousId();return L.writeFileSync(h.userIdFile,n,"utf8"),n}catch{return this.generateAnonymousId()}}generateAnonymousId(){let e=[y.hostname(),y.platform(),y.arch(),y.homedir()].join("-");return Ze("sha256").update(e).digest("hex").substring(0,32)}generateSessionId(){return Math.floor(Date.now()/1e3)}async trackEvent(e,n={}){if(!(this.disabled||!h.enabled||!this.userId))try{let t=this.buildPayload(e,n),c=`https://www.google-analytics.com/mp/collect?measurement_id=${q.measurementId}&api_secret=${q.apiSecret}`;h.debug&&(console.log(`[Analytics] Sending event: ${e}`),console.log("[Analytics] Payload:",JSON.stringify(t,null,2))),Xe.post(c,t,{timeout:h.timeout,headers:{"Content-Type":"application/json"}}).then(g=>{h.debug&&(console.log(`[Analytics] Response status: ${g.status}`),g.data&&console.log("[Analytics] Response:",JSON.stringify(g.data,null,2)))}).catch(g=>{h.debug&&console.warn("[Analytics] Request failed:",g.message)})}catch(t){h.debug&&console.warn("[Analytics] Error tracking event:",t)}}getEngagementTime(){return this.sessionStartTime?Math.max(100,Date.now()-this.sessionStartTime):100}buildPayload(e,n){return{client_id:this.userId,timestamp_micros:Date.now()*1e3,events:[{name:e,params:{session_id:String(this.sessionId),engagement_time_msec:this.getEngagementTime(),...this.getSystemInfo(),...n}}]}}getSystemInfo(){return{os_platform:y.platform(),os_version:y.release(),os_arch:y.arch(),node_version:process.version}}trackCliStart(e,n,t){this.trackEvent("cli_start",{port:String(e),has_custom_subdomain:n&&!n.startsWith("user-"),cli_version:t})}trackTunnelCreated(e,n){this.trackEvent("tunnel_created",{subdomain_type:e.startsWith("user-")?"random":"custom",port:String(n)})}trackTunnelError(e,n){this.trackEvent("tunnel_error",{error_type:e,error_message:n.substring(0,100)})}trackTunnelShutdown(e,n){this.trackEvent("tunnel_shutdown",{shutdown_reason:e,duration_seconds:String(Math.floor(n))})}trackUpdateAvailable(e,n){this.trackEvent("update_available",{current_version:e,latest_version:n})}},f=new z;var N=class{static{i(this,"VersionManager")}static async checkForUpdates(){try{let n=(await en.get(`https://registry.npmjs.org/${u.PACKAGE_NAME}/latest`,{timeout:u.UPDATE_CHECK_TIMEOUT})).data.version,t=this.compareVersions(n,u.CURRENT_VERSION)>0;return t&&f.trackUpdateAvailable(u.CURRENT_VERSION,n),{current:u.CURRENT_VERSION,latest:n,shouldUpdate:t}}catch{return null}}static compareVersions(e,n){let t=e.split(".").map(Number),r=n.split(".").map(Number),c=Math.max(t.length,r.length);for(let g=0;g<c;g++){let A=t[g]||0,Y=r[g]||0;if(A>Y)return 1;if(A<Y)return-1}return 0}};var S=class{static{i(this,"TunnelOrchestrator")}static async start(e){l.setTunnel(null,e.subdomain,e.port,e.backendUrl),await f.initialize(),f.trackCliStart(e.port,e.subdomain,u.CURRENT_VERSION),m.displayStartupBanner(e.port);let n=await N.checkForUpdates();if(l.setUpdateInfo(n),!tn.existsSync(x.BIN_PATH)){console.log(J.yellow(`
64
+ \u{1F4E6} Cloudflared binary not found. Downloading...
65
+ `));try{await K()}catch(r){f.trackTunnelError("binary_download_failed",r.message),console.error(J.red(`
66
+ \u274C Failed to download cloudflared: ${r.message}`)),process.exit(1)}}v.validate(x.BIN_PATH)||(f.trackTunnelError("binary_missing","Cloudflared binary not found"),await new Promise(r=>setTimeout(r,100)),process.exit(1));let t=nn(s.t("creatingTunnel",{port:e.port})).start();try{let r=await _.createTunnel(e.subdomain,e.backendUrl);l.setTunnel(r.tunnelId,e.subdomain,e.port,e.backendUrl),f.trackTunnelCreated(e.subdomain,e.port),t.stop(),console.log(J.green(` ${s.t("tunnelLive")}`)),m.displayTunnelSuccess(r.url,e.port,n);let c=v.spawn(x.BIN_PATH,r.tunnelToken,e.port);l.setProcess(c),v.attachHandlers(c,t);let g=setTimeout(()=>{m.displayTimeoutWarning(),this.cleanup("timeout")},se);l.setTimeout(g)}catch(r){let c=r,g=c.message.includes("already taken")?"subdomain_taken":"tunnel_creation_failed";f.trackTunnelError(g,c.message),m.displayError(c,t),await new Promise(A=>setTimeout(A,100)),process.exit(1)}}static async cleanup(e="manual"){l.clearTimeout(),l.hasTunnel()||process.exit(0),m.displayCleanupStart();let n=l.getDurationSeconds();f.trackTunnelShutdown(e,n);try{l.hasProcess()&&l.tunnelProcess&&l.tunnelProcess.kill(),l.subdomain&&l.tunnelId&&await _.deleteTunnel(l.subdomain,l.tunnelId,l.backendUrl),m.displayCleanupSuccess()}catch{m.displayCleanupError()}await new Promise(t=>setTimeout(t,100)),process.exit(0)}};async function rn(){let o=on(s.t("checkingUpdates")).start(),e=await N.checkForUpdates();o.stop(),m.displayVersion(u.CURRENT_VERSION,e)}i(rn,"displayVersion");function sn(o){o==="clear"?(I.setBackendUrl(null),console.log(b.green("\u2714 Backend URL cleared. Using default backend.")),console.log(b.gray(` Default: https://api.nport.link
67
+ `))):(I.setBackendUrl(o),console.log(b.green("\u2714 Backend URL saved successfully!")),console.log(b.cyan(` Backend: ${o}`)),console.log(b.gray(`
68
+ This backend will be used for all future sessions.`)),console.log(b.gray(` To clear: nport --set-backend
69
+ `)));let e=I.getBackendUrl();e&&(console.log(b.white("Current configuration:")),console.log(b.cyan(` Saved backend: ${e}`)))}i(sn,"handleSetBackend");async function an(){try{let o=process.argv.slice(2),e=O.parse(o);await s.initialize(e.language),(o.includes("-v")||o.includes("--version"))&&(await rn(),process.exit(0)),e.setBackend&&(sn(e.setBackend),process.exit(0)),e.language==="prompt"&&(o.includes("--language")||o.includes("--lang")||o.includes("-l"))&&process.exit(0);let n=e.backendUrl;if(!n){let r=I.getBackendUrl();r&&(n=r)}let t={port:e.port,subdomain:e.subdomain,backendUrl:n,language:e.language};await S.start(t)}catch(o){console.error(`Fatal Error: ${o.message}`),process.exit(1)}}i(an,"main");process.on("SIGINT",()=>S.cleanup());process.on("SIGTERM",()=>S.cleanup());an();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nport",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
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": "dist/index.js",
@@ -63,13 +63,14 @@
63
63
  }
64
64
  ],
65
65
  "scripts": {
66
- "build": "tsc",
67
- "dev": "tsc --watch",
66
+ "build": "node scripts/build.js",
67
+ "dev": "node scripts/build.js --watch",
68
68
  "start": "node dist/index.js",
69
69
  "postinstall": "node scripts/postinstall.js",
70
70
  "test": "vitest run",
71
71
  "test:watch": "vitest",
72
- "lint": "tsc --noEmit"
72
+ "lint": "tsc --noEmit",
73
+ "typecheck": "tsc --noEmit"
73
74
  },
74
75
  "dependencies": {
75
76
  "axios": "^1.13.2",
@@ -78,15 +79,14 @@
78
79
  },
79
80
  "devDependencies": {
80
81
  "@types/node": "^22.0.0",
82
+ "esbuild": "^0.24.0",
81
83
  "typescript": "^5.7.0",
82
84
  "vitest": "~3.2.0"
83
85
  },
84
86
  "files": [
85
87
  "dist/",
86
- "bin/",
87
- "scripts/",
88
+ "scripts/postinstall.js",
88
89
  "README.md",
89
- "CHANGELOG.md",
90
90
  "LICENSE"
91
91
  ],
92
92
  "os": [
package/CHANGELOG.md DELETED
@@ -1,432 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to NPort will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [2.1.0] - 2026-01-27
9
-
10
- ### Added
11
- - 🔷 **Full TypeScript Migration**: Complete rewrite of CLI and Server in TypeScript
12
- - Strict type checking enabled across the entire codebase
13
- - All modules converted from JavaScript to TypeScript
14
- - Type-safe interfaces for configuration, tunnels, analytics, and i18n
15
- - Better IDE support with IntelliSense and autocompletion
16
- - 📚 **Comprehensive Documentation**: New docs folder with detailed guides
17
- - `docs/ARCHITECTURE.md`: System overview, component diagrams, and data flow
18
- - `docs/API.md`: Complete API reference with endpoints and examples
19
- - `docs/CONTRIBUTING.md`: Contribution guidelines and development setup
20
- - 🤖 **AI Context Support**: Added `.ai/context.md` for AI-assisted development
21
- - Project structure and key patterns documented
22
- - Coding conventions and architecture decisions
23
- - Makes AI pair programming more effective
24
- - 🧪 **Unit Testing with Vitest**: Comprehensive test suite for CLI
25
- - Tests for argument parsing (`tests/args.test.ts`)
26
- - Tests for version comparison (`tests/version.test.ts`)
27
- - Tests for state management (`tests/state.test.ts`)
28
- - Easy to run with `npm test`
29
- - 📋 **Code Ownership**: Added `.github/CODEOWNERS` file
30
- - Clear ownership for code review assignments
31
- - Faster PR reviews with automatic reviewer assignment
32
- - 📝 **Cursor Rules**: Added `.cursorrules` for consistent AI assistance
33
- - Project-specific coding conventions
34
- - TypeScript and naming guidelines
35
- - Common patterns and anti-patterns
36
- - 🔄 **Auto-download Cloudflared**: Binary downloads automatically on first run
37
- - No need to run separate install commands
38
- - Seamless first-time user experience
39
- - Progress indicator during download
40
- - 🔒 **Protected Subdomain Support**: Enhanced error handling for reserved subdomains
41
- - User-friendly error message when trying to create protected subdomains (like `api`)
42
- - Formatted error output matching existing error style
43
- - Helpful suggestions to use alternative subdomain names
44
- - Prevents accidental use of backend service subdomains
45
- - 📋 **TODO Management**: Added `TODO.md` for tracking planned features
46
- - Move time limit enforcement to backend
47
- - Update README powered-by section
48
- - Track terminal link clicks
49
-
50
- ### Changed
51
- - 🏗️ **Project Structure**: Reorganized for better maintainability
52
- - All source code in `src/` directory
53
- - Type definitions in `src/types/`
54
- - Shared constants in `src/constants.ts`
55
- - Tests in `tests/` directory
56
- - 📦 **Build System**: Updated to TypeScript compilation
57
- - Uses `tsc` for compilation
58
- - Output to `dist/` directory
59
- - Source maps for debugging
60
- - 🔧 **Development Workflow**: Improved developer experience
61
- - `npm run dev` for watch mode
62
- - `npm run build` for production
63
- - `npm test` for running tests
64
- - `npm run lint` for type checking
65
- - ⚙️ **System Requirements**: Updated to Node.js 20+
66
- - Minimum Node.js version: 20.0.0
67
- - Minimum npm version: 10.0.0
68
- - Better security and performance with latest LTS
69
-
70
- ### Improved
71
- - ✨ **Better Error Messages**: Enhanced user feedback for protected subdomains
72
- - Catches `SUBDOMAIN_PROTECTED` errors from backend
73
- - Formats error messages consistently with other error types
74
- - Shows actionable options when subdomain is reserved
75
-
76
- ### Fixed
77
- - 🐛 **Intel Mac Binary Download**: Fixed cloudflared binary download on Intel Macs
78
- - Node.js reports architecture as `x64`, not `amd64` - now correctly mapped
79
- - Fixed ARM64 Macs to download the correct `cloudflared-darwin-arm64.tgz` binary
80
- - Previously ARM64 Macs were incorrectly downloading the AMD64 binary
81
-
82
- ### Server (v1.1.0)
83
- - 🔷 **TypeScript Migration**: Server fully converted to TypeScript
84
- - Type-safe Cloudflare Worker handlers
85
- - Properly typed API responses
86
- - Full type coverage for Cloudflare API interactions
87
- - 🧪 **Server Tests**: Updated test suite for TypeScript
88
- - Vitest with Cloudflare Workers pool
89
- - Tests for all API endpoints
90
- - Scheduled task testing
91
- - 🔒 **Protected Subdomains**: Infrastructure to protect critical subdomains from deletion or overwriting
92
- - New `PROTECTED_SUBDOMAINS` constant array for easy management of reserved subdomains
93
- - Default protected subdomain: `api` (reserved for NPort backend service)
94
- - Easy to add more protected subdomains by updating the array
95
- - Protected subdomains are blocked at both creation and cleanup stages
96
- - 🛡️ **Backend Service Protection**: Prevents accidental deletion or overwriting of production services
97
- - `api` subdomain is now protected from user creation attempts
98
- - Scheduled cleanup job skips protected subdomains
99
- - Returns clear error message when users try to use protected names
100
- - 🔧 **Manual Cleanup Endpoint**: Added `GET /scheduled` endpoint to manually trigger cleanup
101
- - Useful for testing and on-demand cleanup
102
- - Respects protected subdomains configuration
103
- - Returns JSON response with cleanup results
104
-
105
- ### Technical Details
106
- - **Type System**:
107
- - `TunnelConfig`, `TunnelState`, `ConnectionInfo` interfaces
108
- - `AnalyticsParams`, `VersionInfo`, `I18nStrings` types
109
- - `LogPatterns` with readonly arrays for constants
110
- - **ESM Compliance**: All imports use `.js` extensions as required
111
- - **Constants**: Centralized in `src/constants.ts` with `as const` assertions
112
- - **Error Handling**: Type-safe error boundaries throughout
113
-
114
- ### Migration
115
- - Automatic migration - no manual steps required
116
- - Existing configuration in `~/.nport/config.json` remains compatible
117
- - All CLI flags and options unchanged
118
-
119
- ### Breaking Changes
120
- - None - fully backward compatible!
121
-
122
- ## [2.0.7] - 2026-01-14
123
-
124
- ### Added
125
- - 🌐 **Smart Network Warning System**: Intelligent handling of QUIC/network connectivity issues
126
- - Automatic detection and filtering of QUIC protocol errors
127
- - User-friendly warning messages instead of scary red error spam
128
- - Bilingual support (English & Vietnamese)
129
- - Smart throttling: Shows warning only after 5 errors, max once per 30 seconds
130
- - Clear explanations of what's happening and how to fix it
131
- - Automatic reset when connection is restored
132
- - 🔒 **Protected Subdomain Support**: Enhanced error handling for reserved subdomains
133
- - User-friendly error message when trying to create protected subdomains (like `api`)
134
- - Formatted error output matching existing error style
135
- - Helpful suggestions to use alternative subdomain names
136
- - Prevents accidental use of backend service subdomains
137
-
138
- ### Improved
139
- - 🔇 **Cleaner Terminal Output**: No more error spam from cloudflared
140
- - QUIC timeout errors are now silently tracked instead of displayed
141
- - Network warnings filtered: "failed to accept QUIC stream", "timeout: no recent network activity", etc.
142
- - Only shows meaningful warnings when there's an actual persistent issue
143
- - Terminal stays clean and readable during normal operation
144
- - 📡 **Better User Communication**: Context-aware network issue reporting
145
- - Explains that QUIC failures are usually not critical
146
- - Tunnel continues working via HTTP/2 fallback
147
- - Provides actionable troubleshooting steps
148
- - Reassures users that the tunnel is still functional
149
- - ✨ **Better Error Messages**: Enhanced user feedback for protected subdomains
150
- - Catches `SUBDOMAIN_PROTECTED` errors from backend
151
- - Formats error messages consistently with other error types
152
- - Shows actionable options when subdomain is reserved
153
-
154
- ### Technical Details
155
- - **Network Error Patterns**: Added detection for 7 common QUIC/network error patterns
156
- - **State Management**: New network issue tracking in application state
157
- - `networkIssueCount`: Counter for network errors
158
- - `lastNetworkWarningTime`: Timestamp tracking for cooldown
159
- - `shouldShowNetworkWarning()`: Smart decision logic
160
- - **Configuration**: New `NETWORK_CONFIG` with threshold and cooldown settings
161
- - **Bilingual Messages**: Complete translations for all network warning messages
162
- - **Protected Subdomain Handling**: Enhanced error handling in `src/api.js`
163
- - Added check for `SUBDOMAIN_PROTECTED` error type
164
- - Consistent formatting with existing error messages
165
- - Clear user guidance for alternative subdomain choices
166
-
167
- ### User Experience
168
- **Before:**
169
- ```
170
- [Cloudflared] 2026-01-14T04:33:02Z ERR failed to accept QUIC stream...
171
- [Cloudflared] 2026-01-14T04:33:03Z ERR failed to accept QUIC stream...
172
- [Cloudflared] 2026-01-14T04:33:04Z ERR failed to accept QUIC stream...
173
- [Cloudflared] 2026-01-14T04:33:05Z ERR failed to accept QUIC stream...
174
- ```
175
-
176
- **After:**
177
- ```
178
- ✔ [1/2] Connection established...
179
- ✔ [2/2] Compression enabled...
180
-
181
- ⚠️ NETWORK CONNECTIVITY ISSUE DETECTED
182
- Cloudflared is having trouble maintaining a stable connection...
183
- 📡 Your tunnel is still working, but connection quality may be affected.
184
-
185
- 💡 Possible reasons:
186
- • Unstable internet connection or high packet loss
187
- • Firewall/Router blocking UDP traffic (QUIC protocol)
188
- • ISP throttling or network congestion
189
-
190
- 🔧 What to try:
191
- • Check your internet connection stability
192
- • Try connecting from a different network
193
- • Disable VPN/Proxy if you're using one
194
-
195
- ℹ️ This is usually not critical - your tunnel should continue working normally.
196
- ```
197
-
198
- ## [2.0.6] - 2026-01-13
199
-
200
- ### Added
201
- - 🔧 **Backend URL Configuration**: Full control over backend server
202
- - `--backend` / `-b` flag for temporary backend override
203
- - `--set-backend` command to save backend URL permanently
204
- - `NPORT_BACKEND_URL` environment variable support
205
- - Saved backend configuration persists across sessions
206
- - Priority system: CLI flag > Saved config > Env var > Default
207
- - 🗂️ **Unified Configuration System**: All settings in one place
208
- - New centralized `config-manager.js` module
209
- - All preferences stored in `~/.nport/config.json`
210
- - Automatic migration from old format (v2.0.5)
211
- - Easy to read and manually edit JSON format
212
- - 🌐 **New Default Backend**: Updated to `api.nport.link`
213
- - Professional domain structure
214
- - Better branding alignment
215
- - Shorter and easier to remember
216
-
217
- ### Changed
218
- - 📝 **Language Configuration**: Now uses unified config system
219
- - Language setting moved from `~/.nport/lang` to `~/.nport/config.json`
220
- - Automatic migration from old file format
221
- - Consistent configuration approach across all settings
222
- - 📚 **Documentation Updates**: Complete overhaul
223
- - Updated `README.md` with backend configuration options
224
- - New `CLIENT_SETUP.md` focused on npm installation and backend setup
225
- - Comprehensive backend URL documentation
226
- - Clear priority order explanation
227
-
228
- ### Improved
229
- - 🎯 **Consistency**: Unified approach to all configuration
230
- - Backend URL and language now use same storage system
231
- - Single config file for all preferences
232
- - Cleaner architecture and code organization
233
- - 💾 **Configuration File Structure**:
234
- ```json
235
- {
236
- "backendUrl": "https://api.nport.link",
237
- "language": "en"
238
- }
239
- ```
240
-
241
- ### Migration
242
- - Automatic migration from v2.0.5 configuration files
243
- - Old `~/.nport/lang` file automatically migrated to `config.json`
244
- - No manual steps required
245
- - Old files removed after successful migration
246
-
247
- ## [2.0.5] - 2026-01-13
248
-
249
- ### Added
250
- - 🌍 **Multilingual Support**: Full internationalization with English and Vietnamese
251
- - Interactive language selection on first run
252
- - `--language` / `-l` flag to change language anytime
253
- - Language preference saved to `~/.nport/lang`
254
- - 🎨 **Enhanced UI**: Complete redesign with better visual hierarchy
255
- - New header design with Vietnamese pride
256
- - Improved connection status messages
257
- - Better formatted output with consistent spacing
258
- - Language change hint in goodbye message
259
- - 📂 **Modular Architecture**: Refactored codebase into organized modules
260
- - Split code into `/src` directory with clear separation of concerns
261
- - Better maintainability and testability
262
- - Modules: config, state, args, binary, api, version, ui, tunnel, lang, analytics
263
- - ✅ **Version Command**: Added `-v` / `--version` flag
264
- - Shows current version
265
- - Checks for updates automatically
266
- - Displays update instructions if available
267
- - 📝 **Configuration Directory**: Organized config files under `~/.nport/`
268
- - Language preference: `~/.nport/lang`
269
- - Analytics ID: `~/.nport/analytics-id`
270
-
271
- ### Changed
272
- - 🎯 **Improved Console Output**: Better formatting and spacing throughout
273
- - Connection messages now properly indented
274
- - Time remaining display updated
275
- - Footer redesigned with clearer calls-to-action
276
- - 🔧 **Better Argument Parsing**: Enhanced CLI argument handling
277
- - Support for multiple language flag formats
278
- - Interactive prompt when using `--language` without value
279
- - 📚 **Updated Documentation**: Complete README overhaul
280
- - Added multilingual feature documentation
281
- - New CLI options table
282
- - Project structure documentation
283
- - Cleaner examples and better organization
284
-
285
- ### Fixed
286
- - 🐛 **Terminal Compatibility**: Removed problematic emoji flags
287
- - Vietnamese flag emoji replaced with text
288
- - Better compatibility across different terminals
289
-
290
- ## [2.0.4] - Previous Release
291
-
292
- ### Features
293
- - Basic tunnel functionality
294
- - Custom subdomain support
295
- - Cloudflare integration
296
- - Auto-cleanup on exit
297
- - 4-hour tunnel timeout
298
-
299
- ---
300
-
301
- ## Version Upgrade Guide
302
-
303
- ### From 2.0.7 to 2.1.0
304
-
305
- ```bash
306
- npm install -g nport@latest
307
- ```
308
-
309
- **What's New:**
310
-
311
- 1. **Full TypeScript Rewrite**
312
- - Both CLI and Server now fully typed
313
- - Better IDE support and autocompletion
314
- - Catches errors at compile time
315
-
316
- 2. **Comprehensive Documentation**
317
- - Architecture guide in `docs/ARCHITECTURE.md`
318
- - API reference in `docs/API.md`
319
- - Contributing guide in `docs/CONTRIBUTING.md`
320
-
321
- 3. **Unit Testing**
322
- - Run tests with `npm test`
323
- - Covers argument parsing, version checks, state management
324
-
325
- 4. **Auto-download Cloudflared**
326
- - Binary downloads automatically on first `nport` run
327
- - No separate install step needed
328
-
329
- 5. **AI-Friendly Codebase**
330
- - `.ai/context.md` for AI assistants
331
- - `.cursorrules` for consistent AI suggestions
332
- - JSDoc comments throughout
333
-
334
- **For Contributors:**
335
- ```bash
336
- git clone https://github.com/tuanngocptn/nport
337
- cd nport
338
- npm install
339
- npm run build
340
- npm run dev # Watch mode
341
- ```
342
-
343
- **Breaking Changes:** None - fully backward compatible!
344
-
345
- ### From 2.0.6 to 2.0.7
346
-
347
- ```bash
348
- npm install -g nport@latest
349
- ```
350
-
351
- **What's New:**
352
-
353
- 1. **Cleaner Terminal Experience**
354
- - No more scary red QUIC error spam
355
- - Smart network warnings when needed
356
- - Automatic fallback to HTTP/2 when QUIC fails
357
-
358
- 2. **Better Error Communication**
359
- - Understand what's happening with your connection
360
- - Clear explanations in your language (EN/VI)
361
- - Actionable troubleshooting steps
362
-
363
- 3. **When You'll See Warnings**
364
- - Only after multiple network issues (not just one)
365
- - Maximum once every 30 seconds (no spam)
366
- - Automatically disappears when connection improves
367
-
368
- **Breaking Changes:** None - fully backward compatible!
369
-
370
- ### From 2.0.5 to 2.0.6
371
-
372
- ```bash
373
- npm install -g nport@latest
374
- ```
375
-
376
- **New Features to Try:**
377
-
378
- 1. **Set Your Backend Permanently**
379
- ```bash
380
- nport --set-backend https://api.nport.link
381
- ```
382
-
383
- 2. **Use Custom Backend Temporarily**
384
- ```bash
385
- nport 3000 -b https://your-backend.com
386
- ```
387
-
388
- 3. **Check Current Configuration**
389
- ```bash
390
- cat ~/.nport/config.json
391
- ```
392
-
393
- 4. **Use Environment Variable**
394
- ```bash
395
- export NPORT_BACKEND_URL=https://your-backend.com
396
- nport 3000
397
- ```
398
-
399
- **Breaking Changes:** None - fully backward compatible!
400
-
401
- **Migration:** Your language preference from v2.0.5 will be automatically migrated to the new unified config format.
402
-
403
- ### From 2.0.4 to 2.0.5
404
-
405
- ```bash
406
- npm install -g nport@latest
407
- ```
408
-
409
- **New Features to Try:**
410
-
411
- 1. **Change Language**
412
- ```bash
413
- nport --language
414
- ```
415
-
416
- 2. **Check Version**
417
- ```bash
418
- nport -v
419
- ```
420
-
421
- 3. **Direct Language Selection**
422
- ```bash
423
- nport 3000 -l vi # Vietnamese
424
- nport 3000 -l en # English
425
- ```
426
-
427
- **Breaking Changes:** None - fully backward compatible!
428
-
429
- ---
430
-
431
- Made with ❤️ in Vietnam by [Nick Pham](https://github.com/tuanngocptn)
432
-