@frp-bridge/core 0.0.2 → 0.0.4
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/dist/index.d.ts +1606 -449
- package/dist/index.js +258 -0
- package/package.json +8 -11
- package/dist/index.d.mts +0 -591
- package/dist/index.mjs +0 -3
package/dist/index.mjs
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import{join as l}from"pathe";import{createWriteStream as ie,existsSync as m,mkdirSync as U,chmodSync as ae,readFileSync as v,writeFileSync as N,unlinkSync as ce}from"node:fs";import{writeFile as S,readFile as C,readdir as de,unlink as le}from"node:fs/promises";import{exec as ue,spawn as he}from"node:child_process";import{get as pe}from"node:http";import{get as V}from"node:https";import y from"node:process";import{promisify as me}from"node:util";import{cpus as H,totalmem as q,release as fe,platform as ge,hostname as ye,homedir as B}from"node:os";import{consola as I}from"consola";import{randomUUID as j}from"node:crypto";import{EventEmitter as z}from"node:events";import{WebSocket as E,WebSocketServer as we}from"ws";import{Buffer as T}from"node:buffer";const ve="Unknown command",Re="Unknown query";class W{constructor(e,t={}){this.context=e,this.storage=t.storage,Object.entries(t.commands??{}).forEach(([r,n])=>{this.commandHandlers.set(r,n)}),Object.entries(t.queries??{}).forEach(([r,n])=>{this.queryHandlers.set(r,n)})}storage;commandHandlers=new Map;queryHandlers=new Map;eventBuffer=[];commandQueue=Promise.resolve();state={status:"idle",version:0};registerCommand(e,t){this.commandHandlers.set(e,t)}registerQuery(e,t){this.queryHandlers.set(e,t)}execute(e){const t=this.commandQueue.then(()=>this.runCommand(e));return this.commandQueue=t.then(()=>{},()=>{}),t}async query(e){const t=this.queryHandlers.get(e.name);if(!t)throw this.buildError("VALIDATION_ERROR",`${Re}: ${e.name}`);return t(e,this.context)}snapshot(){return{...this.state}}drainEvents(){const e=this.eventBuffer;return this.eventBuffer=[],e}async runCommand(e){const t=this.commandHandlers.get(e.name);if(!t)return{status:"failed",error:this.buildError("VALIDATION_ERROR",`${ve}: ${e.name}`)};const r={...this.state},n={bumped:!1},o={context:this.context,state:r,emit:i=>this.pushEvents(i),requestVersionBump:()=>this.bumpVersion(e.metadata?.author,n)};this.state.status="running";try{const i=await t(e,o);return i.events&&this.pushEvents(i.events),i.snapshot&&await this.persistSnapshot(i.snapshot,e.metadata?.author),i.error?(this.state.lastError=i.error,this.state.status="error"):i.status==="success"&&(this.state.lastError=void 0,this.state.status="running"),{...i,version:i.version??this.state.version}}catch(i){const c=this.normalizeError(i);return this.state.lastError=c,this.state.status="error",{status:"failed",error:c,version:this.state.version}}}pushEvents(e){const t=this.now();e.forEach(r=>{this.eventBuffer.push({...r,timestamp:r.timestamp??t,version:r.version??this.state.version})})}bumpVersion(e,t){return t.bumped?this.state.version:(t.bumped=!0,this.state.version+=1,this.state.lastAppliedAt=this.now(),e&&this.pushEvents([{type:"config:version-bumped",timestamp:this.now(),version:this.state.version,payload:{author:e}}]),this.state.version)}async persistSnapshot(e,t){this.storage&&e&&await this.storage.save({...e,version:e.version??this.state.version,appliedAt:e.appliedAt??this.now(),author:e.author??t})}normalizeError(e){return e&&typeof e=="object"&&"code"in e&&"message"in e?e:{code:"SYSTEM_ERROR",message:e instanceof Error?e.message:"Unknown error"}}buildError(e,t,r){return{code:e,message:t,details:r}}now(){return this.context.clock?this.context.clock():Date.now()}}const _="fatedier",F="frp",R={client:y.platform==="win32"?"frpc.exe":"frpc",server:y.platform==="win32"?"frps.exe":"frps"},G={x64:"amd64",arm64:"arm64",arm:"arm",ia32:"386"},J={win32:"windows",darwin:"darwin",linux:"linux",freebsd:"freebsd"},M=me(ue);async function A(){const s=`https://api.github.com/repos/${_}/${F}/releases/latest`;return new Promise((e,t)=>{V(s,{headers:{"User-Agent":"frp-bridge"}},r=>{if(r.statusCode!==200){t(new Error(`Failed to fetch latest version: ${r.statusCode}`));return}let n="";r.on("data",o=>n+=o),r.on("end",()=>{try{const o=JSON.parse(n).tag_name?.replace(/^v/,"")||"0.65.0";e(o)}catch(o){t(o)}})}).on("error",t)})}function L(){const s=J[y.platform],e=G[y.arch];if(!s||!e)throw new Error(`Unsupported platform: ${y.platform}-${y.arch}`);return`${s}_${e}`}function k(s,e){const t=e.startsWith("windows_")?"zip":"tar.gz";return`https://github.com/${_}/${F}/releases/download/v${s}/frp_${s}_${e}.${t}`}async function D(s,e){return new Promise((t,r)=>{const n=ie(e);(s.startsWith("https")?V:pe)(s,o=>{if(o.statusCode===302||o.statusCode===301){const i=o.headers.location;if(i){n.close(),D(i,e).then(t).catch(r);return}}if(o.statusCode!==200){r(new Error(`Failed to download: ${o.statusCode}`));return}o.pipe(n),n.on("finish",()=>{n.close(),t()})}).on("error",o=>{n.close(),r(o)})})}async function x(s){return M(s)}async function P(s){try{return y.platform==="win32"?await M(`where ${s}`):await M(`which ${s}`),!0}catch{return!1}}function f(s){m(s)||U(s,{recursive:!0})}function w(s){const e=s.split(`
|
|
2
|
-
`),t={};let r="",n=null;for(const o of e){const i=o.trim();if(!i||i.startsWith("#"))continue;if(i.startsWith("[[")&&i.endsWith("]]")){r=i.slice(2,-2).trim(),Array.isArray(t[r])||(t[r]=[]),n={},t[r].push(n);continue}if(i.startsWith("[")&&i.endsWith("]")){r=i.slice(1,-1).trim(),n=null,t[r]||(t[r]={});continue}const c=i.indexOf("=");if(c>0){const p=i.slice(0,c).trim();let a=i.slice(c+1).trim();(a.startsWith('"')&&a.endsWith('"')||a.startsWith("'")&&a.endsWith("'"))&&(a=a.slice(1,-1)),a==="true"?a=!0:a==="false"?a=!1:Number.isNaN(Number(a))||(a=Number(a)),r?n?n[p]=a:t[r][p]=a:t[p]=a}}return t}function O(s){const e=[];for(const[t,r]of Object.entries(s))(typeof r!="object"||r===null)&&e.push($(t,r));for(const[t,r]of Object.entries(s))if(Array.isArray(r)&&r.length>0&&typeof r[0]=="object"&&r[0]!==null){e.push("");for(const n of r){e.push(`[[${t}]]`);for(const[o,i]of Object.entries(n))e.push($(o,i));e.push("")}}for(const[t,r]of Object.entries(s))if(typeof r=="object"&&r!==null&&!Array.isArray(r)){e.push(""),e.push(`[${t}]`);for(const[n,o]of Object.entries(r))e.push($(n,o))}return e.join(`
|
|
3
|
-
`).trim()}function $(s,e){return typeof e=="string"?`${s} = "${e}"`:typeof e=="boolean"||typeof e=="number"?`${s} = ${e}`:Array.isArray(e)?`${s} = [${e.map(t=>typeof t=="string"?`"${t}"`:t).join(", ")}]`:`${s} = "${String(e)}"`}const Ie={__proto__:null,commandExists:P,downloadFile:D,ensureDir:f,executeCommand:x,getDownloadUrl:k,getLatestVersion:A,getPlatform:L,parseToml:w,toToml:O};function Q(s){return!!s&&typeof s=="object"&&"version"in s}class Y{constructor(e){this.directory=e,f(e)}async save(e){if(typeof e.version!="number")throw new TypeError("Snapshot version must be a number when using FileSnapshotStorage");f(this.directory);const t=JSON.stringify(e,null,2);await S(this.buildPath(e.version),t,"utf-8")}async load(e){const t=this.buildPath(e);if(!m(t))return;const r=await C(t,"utf-8"),n=JSON.parse(r);if(!Q(n))throw new TypeError(`Invalid snapshot schema at version ${e}`);return n}async list(){f(this.directory);const e=await de(this.directory),t=[];for(const r of e){if(!r.endsWith(".json"))continue;const n=await C(l(this.directory,r),"utf-8"),o=JSON.parse(n);Q(o)&&t.push(o)}return t.sort((r,n)=>r.version-n.version)}buildPath(e){return l(this.directory,`${e}.json`)}}function X(s){return["tcp","udp","stcp","xtcp","sudp","tcpmux"].includes(s)}async function K(s,e,t,r){await e();const n=t??!0;let o;return n&&(s.isRunning()&&await s.stop(),await s.start(),o=[{type:"process:started",timestamp:Date.now()}]),r.requestVersionBump(),{status:"success",events:o}}function Te(s){return async(e,t)=>e.payload?.config?K(s.process,async()=>{s.process.updateConfig(e.payload.config)},e.payload.restart,t):{status:"failed",error:{code:"VALIDATION_ERROR",message:"config.apply requires payload.config"}}}function Oe(s){return async(e,t)=>{const r=e.payload?.content;if(!r?.trim())return{status:"failed",error:{code:"VALIDATION_ERROR",message:"config.applyRaw requires payload.content"}};try{const{parseToml:n}=await Promise.resolve().then(function(){return Ie});n(r)}catch(n){return{status:"failed",error:{code:"VALIDATION_ERROR",message:"config.applyRaw received invalid TOML content",details:n instanceof Error?{message:n.message}:void 0}}}return K(s.process,async()=>{s.process.updateConfigRaw(r)},e.payload?.restart,t)}}function be(s){return async()=>s.process.isRunning()?(await s.process.stop(),{status:"success",events:[{type:"process:stopped",timestamp:Date.now()}]}):{status:"success"}}function Ne(s){return async e=>{if(!s.nodeManager)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"node.register is only available in server mode"}};const t=e.payload;if(!t||!t.hostname||!t.serverAddr||!t.serverPort)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"node.register requires hostname, serverAddr, and serverPort"}};try{return{status:"success",result:await s.nodeManager.registerNode(t)}}catch(r){return{status:"failed",error:{code:"VALIDATION_ERROR",message:r instanceof Error?r.message:"Failed to register node"}}}}}function Ce(s){return async e=>{if(!s.nodeManager)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"node.heartbeat is only available in server mode"}};const t=e.payload;if(!t||!t.nodeId)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"node.heartbeat requires nodeId"}};try{return await s.nodeManager.updateHeartbeat(t),{status:"success"}}catch(r){return{status:"failed",error:{code:"VALIDATION_ERROR",message:r instanceof Error?r.message:"Failed to process heartbeat"}}}}}function Pe(s){return async e=>{if(!s.nodeManager)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"node.unregister is only available in server mode"}};const t=e.payload?.nodeId;if(!t)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"node.unregister requires nodeId"}};try{return s.nodeManager.unregisterNode(t),{status:"success"}}catch(r){return{status:"failed",error:{code:"VALIDATION_ERROR",message:r instanceof Error?r.message:"Failed to unregister node"}}}}}function Ee(s){return async e=>{const t=e.payload;if(!t||!t.proxy)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"proxy.add requires payload.proxy"}};if(s.mode==="server"){if(!t.nodeId)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"proxy.add requires payload.nodeId in server mode"}};const r=t.proxy.remotePort;if(r&&X(t.proxy.type)){const n=s.nodeManager?.isRemotePortInUse(r,t.nodeId);if(n?.inUse)return{status:"failed",error:{code:"PORT_CONFLICT",message:`Remote port ${r} is already in use by tunnel "${n.tunnelName}" on node ${n.nodeId}`}}}if(!s.rpcServer)return{status:"failed",error:{code:"RPC_NOT_AVAILABLE",message:"RPC server not available"}};try{return{status:"success",result:await s.rpcServer.rpcCall(t.nodeId,"proxy.add",{proxy:t.proxy})}}catch(n){return{status:"failed",error:{code:"RPC_ERROR",message:n instanceof Error?n.message:"Failed to add tunnel on node"}}}}try{return s.process.addTunnel(t.proxy),{status:"success",result:t.proxy}}catch(r){return{status:"failed",error:{code:"RUNTIME_ERROR",message:r instanceof Error?r.message:"Failed to add tunnel"}}}}}function Ae(s){return async e=>{const t=e.payload;if(!t||!t.name)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"proxy.update requires payload.name"}};if(s.mode==="server"){if(!t.nodeId)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"proxy.update requires payload.nodeId in server mode"}};const r=t.proxy?.remotePort;if(r&&X(t.proxy?.type)){const n=s.nodeManager?.isRemotePortInUse(r,t.nodeId);if(n?.inUse)return{status:"failed",error:{code:"PORT_CONFLICT",message:`Remote port ${r} is already in use by tunnel "${n.tunnelName}" on node ${n.nodeId}`}}}if(!s.rpcServer)return{status:"failed",error:{code:"RPC_NOT_AVAILABLE",message:"RPC server not available"}};try{return{status:"success",result:await s.rpcServer.rpcCall(t.nodeId,"proxy.update",{name:t.name,proxy:t.proxy})}}catch(n){return{status:"failed",error:{code:"RPC_ERROR",message:n instanceof Error?n.message:"Failed to update tunnel on node"}}}}try{return s.process.updateTunnel(t.name,t.proxy),{status:"success",result:{name:t.name,...t.proxy}}}catch(r){return{status:"failed",error:{code:"RUNTIME_ERROR",message:r instanceof Error?r.message:"Failed to update tunnel"}}}}}function De(s){return async e=>{const t=e.payload;if(!t||!t.name)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"proxy.remove requires payload.name"}};if(s.mode==="server"){if(!t.nodeId)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"proxy.remove requires payload.nodeId in server mode"}};if(!s.rpcServer)return{status:"failed",error:{code:"RPC_NOT_AVAILABLE",message:"RPC server not available"}};try{return{status:"success",result:await s.rpcServer.rpcCall(t.nodeId,"proxy.remove",{name:t.name})}}catch(r){return{status:"failed",error:{code:"RPC_ERROR",message:r instanceof Error?r.message:"Failed to remove tunnel on node"}}}}try{return s.process.removeTunnel(t.name),{status:"success",result:{name:t.name}}}catch(r){return{status:"failed",error:{code:"RUNTIME_ERROR",message:r instanceof Error?r.message:"Failed to remove tunnel"}}}}}function xe(s){return{"config.apply":Te(s),"config.applyRaw":Oe(s),"process.stop":be(s),"node.register":Ne(s),"node.heartbeat":Ce(s),"node.unregister":Pe(s),"proxy.add":Ee(s),"proxy.update":Ae(s),"proxy.remove":De(s)}}function Se(s){return async()=>{const e=s.runtime.snapshot();return{result:{running:s.process.isRunning(),config:s.process.getConfig()},version:e.version}}}function _e(s){return async()=>{const e=s.runtime.snapshot();return{result:e,version:e.version}}}function Fe(s){return async()=>{if(!s.nodeManager)return{result:{items:[],total:0,page:1,pageSize:100,hasMore:!1},version:s.runtime.snapshot().version};const e={page:1,pageSize:100};return{result:s.nodeManager.listNodes(e),version:s.runtime.snapshot().version}}}function Me(s){return async e=>{if(!s.nodeManager)return{result:null,version:s.runtime.snapshot().version};const t=e.payload?.nodeId;return t?{result:s.nodeManager.getNode(t)??null,version:s.runtime.snapshot().version}:{result:null,version:s.runtime.snapshot().version}}}function Le(s){return async()=>s.nodeManager?{result:s.nodeManager.getStatistics(),version:s.runtime.snapshot().version}:{result:{total:0,online:0,offline:0,connecting:0,error:0},version:s.runtime.snapshot().version}}function ke(s){return async()=>{if(s.mode!=="client")return{result:[],version:s.runtime.snapshot().version};try{return{result:s.process.listTunnels(),version:s.runtime.snapshot().version}}catch{return{result:[],version:s.runtime.snapshot().version}}}}function $e(s){return async e=>{if(s.mode!=="client")return{result:null,version:s.runtime.snapshot().version};const t=e.payload?.name;if(!t)return{result:null,version:s.runtime.snapshot().version};try{return{result:s.process.getTunnel(t)??null,version:s.runtime.snapshot().version}}catch{return{result:null,version:s.runtime.snapshot().version}}}}function Ue(s){return{"process.status":Se(s),"runtime.snapshot":_e(s),"node.list":Fe(s),"node.get":Me(s),"node.statistics":Le(s),"proxy.list":ke(s),"proxy.get":$e(s)}}class Z{nodeId;heartbeatInterval;logger;heartbeatTimer;constructor(e={}){this.nodeId=e.nodeId,this.heartbeatInterval=e.heartbeatInterval??3e4,this.logger=e.logger}setNodeId(e){this.nodeId=e}collectNodeInfo(){const e=H(),t=q();return{hostname:ye(),osType:ge(),osRelease:fe(),cpuCores:e.length,memTotal:t,protocol:"tcp",serverAddr:"",serverPort:0}}collectHeartbeat(){if(!this.nodeId)throw new Error("Node ID not set. Call setNodeId() first or wait for registration.");const e=H(),t=q();return{nodeId:this.nodeId,status:"online",lastHeartbeat:Date.now(),cpuCores:e.length,memTotal:t}}startHeartbeat(e,t){if(this.heartbeatTimer){this.logger?.debug?.("Heartbeat already running");return}const r=t??this.heartbeatInterval;try{const n=this.collectHeartbeat();e(n)}catch(n){this.logger?.error?.("Failed to collect initial heartbeat",n)}this.heartbeatTimer=setInterval(()=>{try{const n=this.collectHeartbeat();e(n)}catch(n){this.logger?.error?.("Failed to collect heartbeat",n)}},r),this.logger?.info?.(`Heartbeat started with interval ${r}ms`)}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=void 0,this.logger?.info?.("Heartbeat stopped"))}isHeartbeatRunning(){return!!this.heartbeatTimer}}class ee{constructor(e){this.storagePath=e,this.nodeDir=e,this.indexPath=l(e,"nodes.json"),m(this.nodeDir)||U(this.nodeDir,{recursive:!0})}indexPath;nodeDir;async save(e){const t=l(this.nodeDir,`node-${e.id}.json`);await S(t,JSON.stringify(e,null,2),"utf-8"),await this.updateIndex(e.id,!0)}async delete(e){const t=l(this.nodeDir,`node-${e}.json`);try{await le(t)}catch{}await this.updateIndex(e,!1)}async load(e){const t=l(this.nodeDir,`node-${e}.json`);try{const r=await C(t,"utf-8");return JSON.parse(r)}catch{return}}async list(){try{const e=await C(this.indexPath,"utf-8"),t=JSON.parse(e),r=[];for(const n of t)try{const o=await this.load(n);o&&r.push(o)}catch{}return r}catch{return[]}}async updateIndex(e,t){let r=[];try{const n=await C(this.indexPath,"utf-8");r=JSON.parse(n)}catch{}t?r.includes(e)||r.push(e):r=r.filter(n=>n!==e),await S(this.indexPath,JSON.stringify(r,null,2),"utf-8")}}class te extends z{constructor(e,t={},r){super(),this.context=e,this.heartbeatTimeout=t.heartbeatTimeout??9e4,this.logger=t.logger,this.storage=r}nodes=new Map;heartbeatTimers=new Map;tunnelRegistry=new Map;storage;heartbeatTimeout;logger;async initialize(){if(this.storage)try{const e=await this.storage.list();for(const t of e)this.nodes.set(t.id,t),this.setupHeartbeatTimer(t.id);this.logger?.info?.(`Loaded ${e.length} nodes from storage`)}catch(e){this.logger?.error?.("Failed to load nodes from storage",{error:e})}}async registerNode(e){const t=Date.now(),r=j(),n={id:r,ip:e.ip,port:e.port,protocol:e.protocol,serverAddr:e.serverAddr,serverPort:e.serverPort,hostname:e.hostname,osType:e.osType,osRelease:e.osRelease,platform:e.platform,cpuCores:e.cpuCores,memTotal:e.memTotal,frpVersion:e.frpVersion,bridgeVersion:e.bridgeVersion,token:e.token,status:"online",connectedAt:t,lastHeartbeat:t,createdAt:t,updatedAt:t};if(this.nodes.set(r,n),this.setupHeartbeatTimer(r),this.storage)try{await this.storage.save(n)}catch(o){this.logger?.error?.("Failed to save node",{nodeId:r,error:o})}return this.emit("node:registered",{type:"node:registered",timestamp:t,payload:{nodeId:r,nodeInfo:n}}),n}async updateHeartbeat(e){const t=this.nodes.get(e.nodeId);if(!t)return;const r=t.status,n=Date.now();if(t.status=e.status,t.lastHeartbeat=n,t.updatedAt=n,e.cpuCores!==void 0&&(t.cpuCores=e.cpuCores),e.memTotal!==void 0&&(t.memTotal=e.memTotal),this.setupHeartbeatTimer(e.nodeId),this.storage)try{await this.storage.save(t)}catch(o){this.logger?.error?.("Failed to save node heartbeat",{nodeId:e.nodeId,error:o})}this.emit("node:heartbeat",{type:"node:heartbeat",timestamp:n,payload:{nodeId:e.nodeId}}),r!==e.status&&this.emit("node:statusChanged",{type:"node:statusChanged",timestamp:n,payload:{nodeId:e.nodeId,oldStatus:r,newStatus:e.status}})}async unregisterNode(e){if(!this.nodes.get(e))return;const t=Date.now();if(this.nodes.delete(e),this.clearHeartbeatTimer(e),this.clearNodeTunnels(e),this.storage)try{await this.storage.delete(e)}catch(r){this.logger?.error?.("Failed to delete node",{nodeId:e,error:r})}this.emit("node:unregistered",{type:"node:unregistered",timestamp:t,payload:{nodeId:e}})}async getNode(e){return this.nodes.get(e)}async listNodes(e){const t=e?.page??1,r=e?.pageSize??20,n=e?.status,o=e?.search?.toLowerCase();let i=Array.from(this.nodes.values());n&&(i=i.filter(d=>d.status===n)),e?.labels&&(i=i.filter(d=>d.labels?Object.entries(e.labels).every(([g,b])=>d.labels?.[g]===b):!1)),o&&(i=i.filter(d=>d.hostname?.toLowerCase().includes(o)||d.ip.toLowerCase().includes(o)||d.id.toLowerCase().includes(o)));const c=i.length,p=(t-1)*r,a=p+r;return{items:i.slice(p,a),total:c,page:t,pageSize:r,hasMore:a<c}}async getStatistics(){const e=Array.from(this.nodes.values());return{total:e.length,online:e.filter(t=>t.status==="online").length,offline:e.filter(t=>t.status==="offline").length,connecting:e.filter(t=>t.status==="connecting").length,error:e.filter(t=>t.status==="error").length}}hasNode(e){return this.nodes.has(e)}getOnlineNodes(){return Array.from(this.nodes.values()).filter(e=>e.status==="online")}getOfflineNodes(){return Array.from(this.nodes.values()).filter(e=>e.status==="offline")}getNodesByStatus(e){return Array.from(this.nodes.values()).filter(t=>t.status===e)}setupHeartbeatTimer(e){this.clearHeartbeatTimer(e);const t=setTimeout(()=>{this.handleHeartbeatTimeout(e)},this.heartbeatTimeout);this.heartbeatTimers.set(e,t)}clearHeartbeatTimer(e){const t=this.heartbeatTimers.get(e);t&&(clearTimeout(t),this.heartbeatTimers.delete(e))}async handleHeartbeatTimeout(e){const t=this.nodes.get(e);if(!t)return;const r=t.status;if(t.status="offline",t.updatedAt=Date.now(),this.storage)try{await this.storage.save(t)}catch(n){this.logger?.error?.("Failed to save node after timeout",{nodeId:e,error:n})}this.emit("node:statusChanged",{type:"node:statusChanged",timestamp:Date.now(),payload:{nodeId:e,oldStatus:r,newStatus:"offline",reason:"heartbeat_timeout"}})}async syncTunnels(e){const{nodeId:t,tunnels:r,timestamp:n}=e,o=this.nodes.get(t);if(!o){this.logger?.warn?.("Tunnel sync failed: node not found",{nodeId:t});return}if(this.tunnelRegistry.set(t,r),o.tunnels=r,o.updatedAt=n,this.storage)try{await this.storage.save(o)}catch(i){this.logger?.error?.("Failed to save node after tunnel sync",{nodeId:t,error:i})}this.emit("tunnel:synced",{type:"tunnel:synced",timestamp:Date.now(),payload:{nodeId:t,tunnelCount:r.length}}),this.logger?.info?.("Tunnels synced for node",{nodeId:t,tunnelCount:r.length})}getNodeTunnels(e){return this.tunnelRegistry.get(e)||[]}getAllTunnels(){return new Map(this.tunnelRegistry)}isRemotePortInUse(e,t){for(const[r,n]of this.tunnelRegistry.entries())if(!(t&&r===t))for(const o of n){const i=o.remotePort;if(i&&i===e)return{inUse:!0,nodeId:r,tunnelName:o.name}}return{inUse:!1}}clearNodeTunnels(e){this.tunnelRegistry.delete(e),this.logger?.info?.("Cleared tunnels for node",{nodeId:e})}async dispose(){for(const e of this.heartbeatTimers.values())clearTimeout(e);this.heartbeatTimers.clear(),this.tunnelRegistry.clear()}}class h extends Error{constructor(e,t,r){super(e),this.code=t,this.details=r,this.name="FrpBridgeError"}}var u=(s=>(s.BINARY_NOT_FOUND="BINARY_NOT_FOUND",s.DOWNLOAD_FAILED="DOWNLOAD_FAILED",s.EXTRACTION_FAILED="EXTRACTION_FAILED",s.CONFIG_NOT_FOUND="CONFIG_NOT_FOUND",s.CONFIG_INVALID="CONFIG_INVALID",s.PROCESS_ALREADY_RUNNING="PROCESS_ALREADY_RUNNING",s.PROCESS_NOT_RUNNING="PROCESS_NOT_RUNNING",s.PROCESS_START_FAILED="PROCESS_START_FAILED",s.UNSUPPORTED_PLATFORM="UNSUPPORTED_PLATFORM",s.VERSION_FETCH_FAILED="VERSION_FETCH_FAILED",s.MODE_ERROR="MODE_ERROR",s.NOT_FOUND="NOT_FOUND",s))(u||{});class re extends z{workDir;version=null;mode;specifiedVersion;logger;process=null;configPath;binaryPath;uptime=null;isManualStop=!1;constructor(e){super(),this.mode=e.mode,this.specifiedVersion=e.version,this.workDir=e.workDir||l(B(),".frp-bridge"),this.configPath=e.configPath||l(this.workDir,`frp${this.mode==="client"?"c":"s"}.toml`),this.logger=e.logger??I.withTag("FrpProcessManager"),f(this.workDir),this.binaryPath=""}async ensureVersion(){if(!this.version){this.version=this.specifiedVersion||await A();const e=this.mode==="client"?R.client:R.server;this.binaryPath=l(this.workDir,"bin",this.version,e)}}async downloadFrpBinary(){await this.ensureVersion();const e=L(),t=k(this.version,e),r=e.startsWith("windows_"),n=r?"zip":"tar.gz",o=l(this.workDir,`frp_${this.version}.${n}`),i=l(this.workDir,"bin",this.version);f(i),await D(t,o);const c=l(this.workDir,"temp");if(f(c),r){if(!await P("unzip"))throw new h("unzip is required for extraction on Windows",u.EXTRACTION_FAILED);await x(`unzip -o "${o}" -d "${c}"`)}else{const g=await P("gzip"),b=await P("tar");if(!g||!b)throw new h("gzip and tar are required for extraction",u.EXTRACTION_FAILED);await x(`tar -xzf "${o}" -C "${c}"`)}const p=l(c,`frp_${this.version}_${e}`),a=l(p,this.mode==="client"?R.client:R.server);if(!m(a))throw new h(`Binary not found: ${a}`,u.BINARY_NOT_FOUND);const d=await import("fs-extra");await d.copy(a,this.binaryPath),r||ae(this.binaryPath,493),await d.remove(o),await d.remove(c)}async updateFrpBinary(e){await this.ensureVersion();const t=e||await A();if(t===this.version)return;if(m(this.binaryPath)){const n=`${this.binaryPath}.bak`;await(await import("fs-extra")).copy(this.binaryPath,n)}this.version=t;const r=this.mode==="client"?R.client:R.server;this.binaryPath=l(this.workDir,"bin",this.version,r),await this.downloadFrpBinary()}hasBinary(){return m(this.binaryPath)}getConfig(){if(!m(this.configPath))return null;const e=v(this.configPath,"utf-8");return w(e)}updateConfig(e){const t={...this.getConfig(),...e},r=O(t);N(this.configPath,r,"utf-8")}async backupConfig(){if(!m(this.configPath))throw new h("Config file does not exist",u.CONFIG_NOT_FOUND);const e=Date.now(),t=`${this.configPath}.${e}.bak`;return await(await import("fs-extra")).copy(this.configPath,t),t}getConfigPath(){return this.configPath}getConfigRaw(){return m(this.configPath)?v(this.configPath,"utf-8"):null}updateConfigRaw(e){const t=this.configPath.includes("/")||this.configPath.includes("\\")?this.configPath.substring(0,Math.max(this.configPath.lastIndexOf("/"),this.configPath.lastIndexOf("\\"))):this.workDir;f(t),N(this.configPath,e,"utf-8")}async start(){if(await this.ensureVersion(),this.isRunning()&&await this.stop(),this.hasBinary()||await this.downloadFrpBinary(),!m(this.configPath))throw new h("Config file does not exist",u.CONFIG_NOT_FOUND);this.process=he(this.binaryPath,["-c",this.configPath],{stdio:"inherit"}),this.uptime=Date.now(),this.isManualStop=!1,this.setupProcessListeners(),this.emit("process:started",{type:"process:started",timestamp:Date.now(),payload:{pid:this.process?.pid,uptime:0}})}async stop(){if(!this.process)return;this.isManualStop=!0;const e=this.process;return new Promise(t=>{const r=()=>{const n=this.uptime?Date.now()-this.uptime:void 0;this.emit("process:stopped",{type:"process:stopped",timestamp:Date.now(),payload:{uptime:n}}),this.uptime=null,t()};e.exitCode===null?(e.once("exit",r),e.kill("SIGTERM"),setTimeout(()=>{e.exitCode===null&&(this.logger.warn("Process did not exit gracefully, forcing kill"),e.kill("SIGKILL"))},5e3)):r()}).finally(()=>{this.process=null})}isRunning(){if(!this.process)return!1;const e=this.process.exitCode===null&&this.process.signalCode===null;return e||(this.process=null),e}addNode(e){if(this.mode!=="client")throw new h("Nodes can only be added in client mode",u.MODE_ERROR);const t=this.getConfig()||{};t.serverAddr=e.serverAddr,t.serverPort=e.serverPort||7e3,e.token&&(t.auth={...t.auth,token:e.token}),e.config&&Object.assign(t,e.config),this.updateConfig(t)}getNode(){if(this.mode!=="client")throw new h("Nodes are only available in client mode",u.MODE_ERROR);const e=this.getConfig();return!e||!e.serverAddr?null:{id:"default",name:"default",serverAddr:e.serverAddr,serverPort:e.serverPort,token:e.auth?.token}}updateNode(e){if(this.mode!=="client")throw new h("Nodes can only be updated in client mode",u.MODE_ERROR);const t=this.getConfig()||{};e.serverAddr&&(t.serverAddr=e.serverAddr),e.serverPort&&(t.serverPort=e.serverPort),e.token&&(t.auth={...t.auth,token:e.token}),e.config&&Object.assign(t,e.config),this.updateConfig(t)}removeNode(){if(this.mode!=="client")throw new h("Nodes can only be removed in client mode",u.MODE_ERROR);m(this.configPath)&&ce(this.configPath)}addTunnel(e){if(this.mode!=="client")throw new Error("Tunnels can only be added in client mode");const t=m(this.configPath)?v(this.configPath,"utf-8"):"",r=t?w(t):{};if(Array.isArray(r.proxies)||(r.proxies=[]),r.proxies.findIndex(i=>i&&i.name===e.name)!==-1)throw new h(`Tunnel ${e.name} already exists`,u.CONFIG_INVALID);const n=e.remotePort;if(n&&this.typeUsesRemotePort(e.type)&&r.proxies.some(i=>{const c=i.remotePort;return i&&c===n&&this.typeUsesRemotePort(i.type)}))throw new h(`Remote port ${n} is already in use`,u.CONFIG_INVALID);r.proxies.push(e);const o=O(r);N(this.configPath,o,"utf-8")}typeUsesRemotePort(e){return["tcp","udp","stcp","xtcp","sudp","tcpmux"].includes(e)}getTunnel(e){if(this.mode!=="client")throw new Error("Tunnels are only available in client mode");if(!m(this.configPath))return null;const t=v(this.configPath,"utf-8"),r=w(t);return Array.isArray(r.proxies)?r.proxies.find(n=>n&&n.name===e)||null:r[e]||null}updateTunnel(e,t){if(this.mode!=="client")throw new h("Tunnels can only be updated in client mode",u.MODE_ERROR);if(!m(this.configPath))throw new h("Config file does not exist",u.CONFIG_NOT_FOUND);const r=v(this.configPath,"utf-8"),n=w(r);if(Array.isArray(n.proxies)){const i=n.proxies.findIndex(d=>d&&d.name===e);if(i===-1)throw new h(`Tunnel ${e} not found`,u.NOT_FOUND);const c=n.proxies[i],p={...c,...t},a=t.remotePort;if(a&&a!==c.remotePort&&this.typeUsesRemotePort(p.type)&&n.proxies.some((d,g)=>{if(g===i)return!1;const b=d.remotePort;return d&&b===a&&this.typeUsesRemotePort(d.type)}))throw new h(`Remote port ${a} is already in use`,u.CONFIG_INVALID);n.proxies[i]=p}else if(n[e])n[e]={...n[e],...t};else throw new h(`Tunnel ${e} not found`,u.NOT_FOUND);const o=O(n);N(this.configPath,o,"utf-8")}removeTunnel(e){if(this.mode!=="client")throw new h("Tunnels can only be removed in client mode",u.MODE_ERROR);if(!m(this.configPath))return;const t=v(this.configPath,"utf-8"),r=w(t);if(Array.isArray(r.proxies)){const o=r.proxies.findIndex(i=>i&&i.name===e);o!==-1&&r.proxies.splice(o,1)}else r[e]&&delete r[e];const n=O(r);N(this.configPath,n,"utf-8")}listTunnels(){if(this.mode!=="client")throw new h("Tunnels are only available in client mode",u.MODE_ERROR);if(!m(this.configPath))return this.logger.warn?.("Config file does not exist",{path:this.configPath}),[];const e=v(this.configPath,"utf-8"),t=w(e);this.logger.info?.("listTunnels - parsed config:",{hasProxies:"proxies"in t,isArray:Array.isArray(t.proxies),length:t.proxies?.length,proxies:t.proxies});const r=[];if(Array.isArray(t.proxies))for(const o of t.proxies)o&&typeof o=="object"&&"type"in o&&r.push(o);const n=new Set(r.map(o=>o.name));for(const[o,i]of Object.entries(t))if(o!=="proxies"&&typeof i=="object"&&i!==null&&"type"in i&&!Array.isArray(i)){const c={...i,name:i.name||o};n.has(c.name)||(r.push(c),n.add(c.name))}return this.logger.info?.("listTunnels - result:",{tunnelCount:r.length,tunnels:r}),r}queryProcess(){const e=this.uptime?Date.now()-this.uptime:0;return{pid:this.process?.pid,uptime:e}}setupProcessListeners(){this.process&&(this.process.on("exit",(e,t)=>{const r=this.uptime?Date.now()-this.uptime:void 0;this.isManualStop||this.emit("process:exited",{type:"process:exited",timestamp:Date.now(),payload:{code:e??void 0,signal:t??void 0,uptime:r}}),this.process=null,this.uptime=null}),this.process.on("error",e=>{this.emit("process:error",{type:"process:error",timestamp:Date.now(),payload:{error:e.message,pid:this.process?.pid}}),this.logger.error("FRP process error",{error:e})}))}}function Ve(s){return typeof s=="string"?s:T.isBuffer(s)?s.toString():s instanceof ArrayBuffer?T.from(new Uint8Array(s)).toString():Array.isArray(s)?T.concat(s.map(e=>T.isBuffer(e)?e:T.from(e))).toString():T.from(s).toString()}function se(s,e){try{const t=Ve(s);return JSON.parse(t)}catch(t){e?.warn?.("parse message failed",t);return}}class ne{constructor(e){this.options=e,this.reconnectInterval=e.reconnectInterval??5e3}ws=null;reconnectTimer;reconnectInterval;async connect(){await this.createConnection()}disconnect(){this.reconnectTimer&&clearTimeout(this.reconnectTimer),this.reconnectTimer=void 0,this.ws?.close(),this.ws=null}async createConnection(){return new Promise((e,t)=>{const r=new E(this.options.url);this.ws=r,r.on("open",async()=>{try{const n=await this.options.getRegisterPayload();this.send({type:"register",nodeId:this.options.nodeId,payload:n}),e()}catch(n){this.options.logger?.error?.("rpc client register failed",n),t(n)}}),r.on("message",n=>{this.handleMessage(n).catch(o=>{this.options.logger?.error?.("rpc client handle message failed",o)})}),r.on("close",()=>{this.scheduleReconnect()}),r.on("error",n=>{this.options.logger?.warn?.("rpc client socket error",n),this.scheduleReconnect(),t(n)})})}async handleMessage(e){const t=se(e,this.options.logger);if(t){if(t.type==="ping"){this.send({type:"pong",timestamp:Date.now()});return}t.method&&await this.handleRpcRequest(t)}}async handleRpcRequest(e){try{const t=await this.options.handleRequest(e);this.send({id:e.id,status:"success",result:t})}catch(t){this.send({id:e.id,status:"error",error:{code:"EXECUTION_ERROR",message:t instanceof Error?t.message:"Unknown error"}})}}send(e){this.ws?.readyState===E.OPEN&&this.ws.send(JSON.stringify(e))}scheduleReconnect(){this.reconnectTimer||(this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=void 0,this.createConnection().catch(e=>{this.options.logger?.error?.("rpc client reconnect failed",e),this.scheduleReconnect()})},this.reconnectInterval))}}class oe{constructor(e){this.options=e}clients=new Map;pendingRequests=new Map;wsToNode=new Map;heartbeatTimer;server;start(){this.server||(this.server=new we({port:this.options.port}),this.server.on("connection",(e,t)=>{const r=new URL(t.url??"/","ws://localhost").searchParams.get("token")??void 0;e.on("message",n=>{this.handleMessage(e,n,r).catch(o=>{this.options.logger?.error?.("rpc server handle message failed",o)})}),e.on("close",()=>{this.handleClose(e)})}),this.startHeartbeat(),this.options.logger?.info?.("RpcServer started",{port:this.options.port}))}stop(){this.heartbeatTimer&&clearInterval(this.heartbeatTimer),this.heartbeatTimer=void 0,this.pendingRequests.forEach(e=>clearTimeout(e.timer)),this.pendingRequests.clear(),this.clients.forEach(e=>e.close()),this.clients.clear(),this.wsToNode.clear(),this.server?.close(),this.server=void 0}async rpcCall(e,t,r,n=3e4){const o=this.clients.get(e);if(!o||o.readyState!==E.OPEN)throw new Error("Client not connected");if(this.options.authorize&&!await this.options.authorize(e,t))throw new Error("UNAUTHORIZED");const i=j(),c={id:i,method:t,params:r,timeout:n};return new Promise((p,a)=>{const d=setTimeout(()=>{this.pendingRequests.delete(i),a(new Error(`RPC timeout: ${t}`))},n);this.pendingRequests.set(i,{resolve:p,reject:a,timer:d}),o.send(JSON.stringify(c))})}async handleMessage(e,t,r){const n=se(t,this.options.logger);if(n){if(n.type==="register"){const o=n.nodeId;if(!o){e.close();return}if(!(!this.options.validateToken||await this.options.validateToken(r,o))){e.close();return}this.clients.set(o,e),this.wsToNode.set(e,o);const i=n.payload;i&&this.options.onRegister&&await this.options.onRegister(o,i);return}n.type!=="pong"&&n.id&&n.status&&this.handleRpcResponse(n)}}handleRpcResponse(e){const t=this.pendingRequests.get(e.id);t&&(clearTimeout(t.timer),this.pendingRequests.delete(e.id),e.status==="success"?t.resolve(e.result):t.reject(new Error(e.error?.message??"RPC error")))}handleClose(e){const t=this.wsToNode.get(e);t&&(this.clients.delete(t),this.wsToNode.delete(e))}startHeartbeat(){const e=this.options.heartbeatInterval??3e4;this.heartbeatTimer=setInterval(()=>{this.clients.forEach((t,r)=>{if(t.readyState===E.OPEN){const n={type:"ping",timestamp:Date.now()};t.send(JSON.stringify(n))}else this.clients.delete(r)})},e)}}class He{constructor(e){this.config=e}initialize(){const{rootWorkDir:e,runtimeDir:t,processDir:r}=this.setupDirectories(),n=this.createLoggers(),o=this.createProcessManager(r,n.processLogger),i=this.createRuntimeContext(t,n.runtimeLogger),c=this.createNodeManager(i,t,n.runtimeLogger),p=this.createClientCollector(),{rpcServer:a,rpcClient:d}=this.createRpcComponents();return{runtimeContext:i,process:o,nodeManager:c,clientCollector:p,rpcServer:a,rpcClient:d,rootWorkDir:e,runtimeDir:t,processDir:r}}setupDirectories(){const e=this.config.workDir??l(B(),".frp-bridge"),t=this.config.runtime?.workDir??l(e,"runtime"),r=this.config.process?.workDir??l(e,"process");return f(e),f(t),f(r),{rootWorkDir:e,runtimeDir:t,processDir:r}}createLoggers(){const e=this.config.runtime?.logger??I.withTag("FrpRuntime"),t=this.config.process?.logger??I.withTag("FrpProcessManager");return{runtimeLogger:e,processLogger:t}}createProcessManager(e,t){return new re({mode:this.config.process?.mode??this.config.mode,version:this.config.process?.version,workDir:e,configPath:this.config.configPath,logger:t})}createRuntimeContext(e,t){return{id:this.config.runtime?.id??"default",mode:this.config.runtime?.mode??this.config.mode,workDir:e,platform:this.config.runtime?.platform??y.platform,clock:this.config.runtime?.clock,logger:t}}createNodeManager(e,t,r){if(this.config.mode!=="server")return;const n=l(t,"nodes");f(n);const o=new ee(n);return new te(e,{heartbeatTimeout:9e4,logger:r},o)}createClientCollector(){if(this.config.mode==="client")return new Z({heartbeatInterval:3e4,logger:I.withTag("ClientNodeCollector")})}createRpcComponents(){const e=this.config.rpc,t={};if(this.config.mode==="server"&&e?.serverPort&&(t.rpcServer=new oe({port:e.serverPort,heartbeatInterval:e.serverHeartbeatInterval,validateToken:e.serverValidateToken,authorize:e.serverAuthorize,logger:I.withTag("RpcServer")})),this.config.mode==="client"&&e?.clientUrl&&e.clientNodeId){const r=this.appendToken(e.clientUrl,e.clientToken);t.rpcClient=new ne({url:r,nodeId:e.clientNodeId,reconnectInterval:e.clientReconnectInterval,getRegisterPayload:e.getRegisterPayload??(async()=>{throw new Error("rpc getRegisterPayload is required in client mode")}),handleRequest:e.handleRequest??(async()=>{}),logger:I.withTag("RpcClient")})}return t}appendToken(e,t){if(!t)return e;const r=new URL(e);return r.searchParams.set("token",t),r.toString()}}function qe(s,e){e&&(s.on("process:started",t=>{e(t)}),s.on("process:stopped",t=>{e(t)}),s.on("process:exited",t=>{e(t)}),s.on("process:error",t=>{e(t)}))}class Be{runtime;process;mode;eventSink;nodeManager;clientCollector;rpcServer;rpcClient;constructor(e){this.mode=e.mode;const t=new He(e).initialize();this.process=t.process,this.nodeManager=t.nodeManager,this.clientCollector=t.clientCollector,this.rpcServer=t.rpcServer,this.rpcClient=t.rpcClient;const r=e.storage??new Y(l(t.runtimeDir,"snapshots"));this.runtime=new W(t.runtimeContext,{storage:r,commands:{},queries:{}});const n={process:this.process,nodeManager:this.nodeManager,rpcServer:this.rpcServer,mode:this.mode},o={process:this.process,nodeManager:this.nodeManager,runtime:this.runtime,mode:this.mode},i=xe(n),c=Ue(o),p={...i,...e.commands??{}},a={...c,...e.queries??{}};Object.entries(p).forEach(([d,g])=>{this.runtime.registerCommand(d,g)}),Object.entries(a).forEach(([d,g])=>{this.runtime.registerQuery(d,g)}),this.eventSink=e.eventSink,qe(this.process,this.eventSink)}execute(e){return this.runtime.execute(e).finally(()=>{this.forwardEvents()})}query(e){return this.runtime.query(e).finally(()=>{this.forwardEvents()})}snapshot(){return this.runtime.snapshot()}drainEvents(){return this.runtime.drainEvents()}getProcessManager(){return this.process}getRuntime(){return this.runtime}getNodeManager(){return this.nodeManager}getClientCollector(){return this.clientCollector}getRpcServer(){return this.rpcServer}getRpcClient(){return this.rpcClient}async initialize(){this.nodeManager&&await this.nodeManager.initialize(),this.rpcServer&&this.rpcServer.start(),this.rpcClient&&await this.rpcClient.connect()}async dispose(){this.nodeManager&&this.nodeManager.dispose(),this.clientCollector&&this.clientCollector.stopHeartbeat(),this.rpcServer&&this.rpcServer.stop(),this.rpcClient&&this.rpcClient.disconnect()}forwardEvents(){this.eventSink&&this.runtime.drainEvents().forEach(e=>this.eventSink?.(e))}}export{G as ARCH_MAP,R as BINARY_NAMES,Z as ClientNodeCollector,u as ErrorCode,ee as FileNodeStorage,Y as FileSnapshotStorage,Be as FrpBridge,h as FrpBridgeError,re as FrpProcessManager,W as FrpRuntime,_ as GITHUB_OWNER,F as GITHUB_REPO,te as NodeManager,J as OS_MAP,ne as RpcClient,oe as RpcServer,P as commandExists,D as downloadFile,f as ensureDir,x as executeCommand,k as getDownloadUrl,A as getLatestVersion,L as getPlatform,w as parseToml,O as toToml};
|