tmux-weblink 2.2.10 → 2.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- const i=1,f=2;function u(t,e,n){const s=typeof n=="string"?Buffer.from(n,"utf-8"):Buffer.isBuffer(n)?n:Buffer.from(n),r=Buffer.allocUnsafe(5+s.length);return r[0]=t,r.writeUInt32BE(e>>>0,1),s.copy(r,5),r}function c(t){return t.length<5?null:{kind:t[0],id:t.readUInt32BE(1),payload:t.subarray(5)}}function l(t){if(typeof t!="object"||t===null)return!1;const e=t.type;return typeof e=="string"&&o.has(e)}function a(t){if(typeof t!="object"||t===null)return!1;const e=t.type;return typeof e=="string"&&_.has(e)}const o=new Set(["hello","sessions","http_resp","http_body_end","attach_ok","attach_err","ws_to_hub","ws_close","activity","rebuild_session_result","ping"]),_=new Set(["hello_ok","hello_err","sessions_req","http_req","http_body_end","attach","ws_to_agent","rebuild_session","detach","pong"]);export{f as BIN_KIND_HTTP_BODY,i as BIN_KIND_TERM,c as decodeBinaryFrame,u as encodeBinaryFrame,l as isAgentToHubMessage,a as isHubToAgentMessage};
1
+ const i=1,f=2;function u(t,e,n){const s=typeof n=="string"?Buffer.from(n,"utf-8"):Buffer.isBuffer(n)?n:Buffer.from(n),r=Buffer.allocUnsafe(5+s.length);return r[0]=t,r.writeUInt32BE(e>>>0,1),s.copy(r,5),r}function c(t){return t.length<5?null:{kind:t[0],id:t.readUInt32BE(1),payload:t.subarray(5)}}function l(t){if(typeof t!="object"||t===null)return!1;const e=t.type;return typeof e=="string"&&o.has(e)}function p(t){if(typeof t!="object"||t===null)return!1;const e=t.type;return typeof e=="string"&&_.has(e)}const o=new Set(["hello","sessions","http_resp","http_body_end","attach_ok","attach_err","ws_to_hub","ws_close","activity","rebuild_session_result","ping"]),_=new Set(["hello_ok","hello_err","sessions_req","http_req","http_body_end","attach","ws_to_agent","rebuild_session","ping","detach","pong"]);export{f as BIN_KIND_HTTP_BODY,i as BIN_KIND_TERM,c as decodeBinaryFrame,u as encodeBinaryFrame,l as isAgentToHubMessage,p as isHubToAgentMessage};
@@ -1 +1 @@
1
- import{WebSocket as w}from"ws";import{attachTerminal as F}from"./attach-terminal.js";import{encodeBinaryFrame as G,decodeBinaryFrame as X,BIN_KIND_HTTP_BODY as O}from"./agent-channel.js";import{ActivityProbe as Y}from"./activity-probe.js";import{sessionWorkingPath as z}from"./sessions-sidebar.js";import{newTmuxSession as Q}from"./tmux-windows.js";const V=parseInt(process.env.TMUX_WEB_AGENT_HEARTBEAT_MS||"15000",10),Z=parseInt(process.env.TMUX_WEB_AGENT_SESSIONS_POLL_MS||"10000",10),ee=new Set(["content-type","content-disposition","location","cache-control","etag"]);function ce(o){const B=o.log??(e=>console.log(`[agent] ${e}`)),h=e=>console.error(`[agent] ${e}`);let s=null,I=1e3,v=null,T=!1;const u=new Map,y=new Map,p=new Map,E=new Set;function H(e){return E.has(e)?Promise.resolve():new Promise(t=>{p.set(e,t),setTimeout(()=>{p.delete(e)&&t()},3e4)})}let _=null,m=null,S=null,g=null,M=new Map,C="",P=null;const b={state:"idle",agentId:null,hub:o.hub,name:o.name};function f(e,t){t!==void 0&&(b.lastError=t),b.state=e,b.agentId=P,o.onStatus?.({...b})}function c(e){s&&s.readyState===w.OPEN&&s.send(JSON.stringify(e))}function k(){let e;try{e=o.listSessions()}catch{return}const t=e.map(r=>({...r,path:z(r.name)})),n=JSON.stringify(t);n!==C&&(C=n,c({type:"sessions",sessions:t}))}function R(){_||(_=setInterval(k,Z)),m||(m=setInterval(()=>c({type:"ping",ts:Date.now()}),V)),S||(g=new Y,S=setInterval(()=>void q(),g.scanMs))}function L(){_&&(clearInterval(_),_=null),m&&(clearInterval(m),m=null),S&&(clearInterval(S),S=null),g=null}async function q(){if(!(!s||s.readyState!==w.OPEN||!g))try{const e=await g.scan(),t=[];for(const[n,r]of e)M.get(n)!==r&&t.push({session:n,state:r});M=e,t.length&&c({type:"activity",activities:t})}catch{}}async function U(e){let t;if(e.hasBody){await H(e.id),E.delete(e.id);const a=y.get(e.id);y.delete(e.id),t=a&&a.length?Buffer.concat(a):Buffer.alloc(0)}const n=new URL(e.path,"http://agent.invalid");let r;try{const a={method:e.method,headers:e.headers};t&&t.length&&(a.body=t),r=await Promise.race([o.app.request(new Request(n,a)),new Promise((te,K)=>setTimeout(()=>K(new Error("app.request timed out")),3e4))])}catch(a){h(`tunnel request failed: ${e.method} ${e.path} ${a.message}`),c({type:"http_resp",id:e.id,status:500,headers:{"content-type":"application/json"},hasBody:!1});return}const d={};r.headers.forEach((a,l)=>{ee.has(l.toLowerCase())&&(d[l]=a)});const i=Buffer.from(await r.arrayBuffer());if(c({type:"http_resp",id:e.id,status:r.status,headers:d,hasBody:i.length>0}),i.length>0){for(let l=0;l<i.length;l+=65536){if(!s||s.readyState!==w.OPEN)return;s.send(G(O,e.id,i.subarray(l,l+65536)))}c({type:"http_body_end",id:e.id})}}function x(e,t){let n=!1,r="";try{Q(e,t),n=!0}catch(d){r=d instanceof Error?d.message:"rebuild failed"}s&&s.readyState===w.OPEN&&s.send(JSON.stringify({type:"rebuild_session_result",session:e,ok:n,message:r})),k()}function J(e){const t=e.session;let n=!1;try{n=o.listSessions().some(i=>i.name===t)}catch{n=!1}if(!n){c({type:"attach_err",connId:e.connId,session:t,message:"no such session"});return}let r=null;const d=F(t,{send:i=>c({type:"ws_to_hub",connId:e.connId,msg:i}),onMessage:i=>{r=i},onClose:()=>{}},{terminalBufferConfig:o.terminalBufferConfig,getSessionPaneTarget:o.getSessionPaneTarget,capturePaneTail:o.capturePaneTail,capturePaneHistoryChunk:o.capturePaneHistoryChunk,toCrlf:o.toCrlf,handleClientMessage:o.handleClientMessage,acquireControlClient:o.acquireControlClient,initialCols:e.cols,initialRows:e.rows,tmuxSocketPath:o.tmuxSocketPath,onPtyExit:(i,a)=>{c({type:"ws_close",connId:e.connId,code:1e3,reason:`tmux exited (${a})`})},onSpawnError:(i,a)=>{h(`attach failed for session "${e.session}": ${a}`),c({type:"attach_err",connId:e.connId,session:t,message:a})},onAttached:i=>{c({type:"attach_ok",connId:e.connId,session:t})}});u.set(e.connId,{onMessage:i=>r?.(i),dispose:()=>{u.delete(e.connId),d.dispose()}})}function D(e){let t;try{t=JSON.parse(e)}catch{return}switch(t.type){case"hello_ok":P=t.agentId,I=1e3,B(`connected to hub as "${P}"`),f("connected"),k(),R();break;case"hello_err":if(h(`hub rejected us: ${t.reason}`),f("error",t.reason),s)try{s.close(4003,"hello rejected")}catch{}break;case"sessions_req":k();break;case"http_req":U(t);break;case"http_body_end":{E.add(t.id);const n=p.get(t.id);n&&(p.delete(t.id),n());break}case"attach":J(t);break;case"rebuild_session":x(t.session,t.dir);break;case"ws_to_agent":{const n=u.get(t.connId);if(n)try{n.onMessage(JSON.stringify(t.msg))}catch{}break}case"detach":{const n=u.get(t.connId);n&&n.dispose();break}case"pong":break;default:break}}function W(e){const t=X(e);if(t&&t.kind===O){const n=y.get(t.id)??[];n.push(t.payload),y.set(t.id,n)}}function N(){L();for(const e of u.values())try{e.dispose()}catch{}u.clear(),y.clear(),p.clear(),E.clear(),C=""}function A(){if(s){try{s.removeAllListeners()}catch{}s=null}if(N(),T)return;f("connecting");const e=I+Math.random()*500;B(`reconnecting in ${Math.round(e)}ms\u2026`),v=setTimeout($,e),I=Math.min(I*2,6e4)}function $(){if(T||s)return;const e=o.hub.replace(/\/+$/,"").replace(/^http:/,"ws:").replace(/^https:/,"wss:")+"/agent/ws";let t;try{t=new w(e)}catch(n){h(`bad hub URL: ${n.message} (${e})`),f("error",`bad hub URL: ${n.message}`);return}s=t,f("connecting"),t.on("open",()=>{B(`connecting to ${e}\u2026`),c({type:"hello",token:o.token,name:o.name,version:o.version})}),t.on("message",(n,r)=>{r?W(Array.isArray(n)?Buffer.concat(n):Buffer.isBuffer(n)?n:Buffer.from(n)):D(typeof n=="string"?n:Buffer.from(n).toString("utf-8"))}),t.on("close",()=>{s===t&&A()}),t.on("error",n=>{h(`connection error: ${n.message}`),s===t&&A()})}function j(){if(!T){if(T=!0,v&&clearTimeout(v),N(),s){try{s.removeAllListeners(),s.close(1e3,"shutdown")}catch{}s=null}P=null,f("idle")}}return $(),{stop:j,status:()=>({...b})}}export{ce as startAgentClient};
1
+ import{WebSocket as h}from"ws";import{attachTerminal as X}from"./attach-terminal.js";import{encodeBinaryFrame as Y,decodeBinaryFrame as z,BIN_KIND_HTTP_BODY as R}from"./agent-channel.js";import{ActivityProbe as Q}from"./activity-probe.js";import{sessionWorkingPath as V}from"./sessions-sidebar.js";import{newTmuxSession as Z}from"./tmux-windows.js";const ee=parseInt(process.env.TMUX_WEB_AGENT_HEARTBEAT_MS||"15000",10),te=parseInt(process.env.TMUX_WEB_AGENT_SESSIONS_POLL_MS||"10000",10),ne=parseInt(process.env.TMUX_WEB_AGENT_PONG_TIMEOUT_MS||"45000",10),H=5,se=6e4,oe=new Set(["content-type","content-disposition","location","cache-control","etag"]);function fe(o){const E=o.log??(t=>console.log(`[agent] ${t}`)),p=t=>console.error(`[agent] ${t}`);let s=null,I=0,y=0,B=null,P=!1;const d=new Map,_=new Map,S=new Map,M=new Set;function U(t){return M.has(t)?Promise.resolve():new Promise(e=>{S.set(t,e),setTimeout(()=>{S.delete(t)&&e()},3e4)})}let m=null,g=null,b=null,w=null,v=new Map,C="",N=null;const T={state:"idle",agentId:null,hub:o.hub,name:o.name};function f(t,e){e!==void 0&&(T.lastError=e),T.state=t,T.agentId=N,o.onStatus?.({...T})}function c(t){s&&s.readyState===h.OPEN&&s.send(JSON.stringify(t))}function k(){let t;try{t=o.listSessions()}catch{return}const e=t.map(r=>({...r,path:V(r.name)})),n=JSON.stringify(e);n!==C&&(C=n,c({type:"sessions",sessions:e}))}function L(){m||(m=setInterval(k,te)),g||(g=setInterval(()=>{if(s&&s.readyState===h.OPEN&&y>0&&Date.now()-y>ne){E(`hub heartbeat timeout (${Date.now()-y}ms without pong), reconnecting`);try{s.close(1001,"hub heartbeat timeout")}catch{}return}c({type:"ping",ts:Date.now()})},ee)),b||(w=new Q,b=setInterval(()=>void D(),w.scanMs))}function q(){m&&(clearInterval(m),m=null),g&&(clearInterval(g),g=null),b&&(clearInterval(b),b=null),w=null}async function D(){if(!(!s||s.readyState!==h.OPEN||!w))try{const t=await w.scan(),e=[];for(const[n,r]of t)v.get(n)!==r&&e.push({session:n,state:r});v=t,e.length&&c({type:"activity",activities:e})}catch{}}async function x(t){let e;if(t.hasBody){await U(t.id),M.delete(t.id);const a=_.get(t.id);_.delete(t.id),e=a&&a.length?Buffer.concat(a):Buffer.alloc(0)}const n=new URL(t.path,"http://agent.invalid");let r;try{const a={method:t.method,headers:t.headers};e&&e.length&&(a.body=e),r=await Promise.race([o.app.request(new Request(n,a)),new Promise((re,K)=>setTimeout(()=>K(new Error("app.request timed out")),3e4))])}catch(a){p(`tunnel request failed: ${t.method} ${t.path} ${a.message}`),c({type:"http_resp",id:t.id,status:500,headers:{"content-type":"application/json"},hasBody:!1});return}const u={};r.headers.forEach((a,l)=>{oe.has(l.toLowerCase())&&(u[l]=a)});const i=Buffer.from(await r.arrayBuffer());if(c({type:"http_resp",id:t.id,status:r.status,headers:u,hasBody:i.length>0}),i.length>0){for(let l=0;l<i.length;l+=65536){if(!s||s.readyState!==h.OPEN)return;s.send(Y(R,t.id,i.subarray(l,l+65536)))}c({type:"http_body_end",id:t.id})}}function G(t,e){let n=!1,r="";try{Z(t,e),n=!0}catch(u){r=u instanceof Error?u.message:"rebuild failed"}s&&s.readyState===h.OPEN&&s.send(JSON.stringify({type:"rebuild_session_result",session:t,ok:n,message:r})),k()}function J(t){const e=t.session;let n=!1;try{n=o.listSessions().some(i=>i.name===e)}catch{n=!1}if(!n){c({type:"attach_err",connId:t.connId,session:e,message:"no such session"});return}let r=null;const u=X(e,{send:i=>c({type:"ws_to_hub",connId:t.connId,msg:i}),onMessage:i=>{r=i},onClose:()=>{}},{terminalBufferConfig:o.terminalBufferConfig,getSessionPaneTarget:o.getSessionPaneTarget,capturePaneTail:o.capturePaneTail,capturePaneHistoryChunk:o.capturePaneHistoryChunk,toCrlf:o.toCrlf,handleClientMessage:o.handleClientMessage,acquireControlClient:o.acquireControlClient,initialCols:t.cols,initialRows:t.rows,tmuxSocketPath:o.tmuxSocketPath,onPtyExit:(i,a)=>{c({type:"ws_close",connId:t.connId,code:1e3,reason:`tmux exited (${a})`})},onSpawnError:(i,a)=>{p(`attach failed for session "${t.session}": ${a}`),c({type:"attach_err",connId:t.connId,session:e,message:a})},onAttached:i=>{c({type:"attach_ok",connId:t.connId,session:e})}});d.set(t.connId,{onMessage:i=>r?.(i),dispose:()=>{d.delete(t.connId),u.dispose()}})}function W(t){let e;try{e=JSON.parse(t)}catch{return}switch(e.type){case"hello_ok":N=e.agentId,I=0,y=Date.now(),E(`connected to hub as "${N}"`),f("connected"),k(),L();break;case"hello_err":if(p(`hub rejected us: ${e.reason}`),f("error",e.reason),s)try{s.close(4003,"hello rejected")}catch{}break;case"sessions_req":k();break;case"http_req":x(e);break;case"http_body_end":{M.add(e.id);const n=S.get(e.id);n&&(S.delete(e.id),n());break}case"attach":J(e);break;case"rebuild_session":G(e.session,e.dir);break;case"ws_to_agent":{const n=d.get(e.connId);if(n)try{n.onMessage(JSON.stringify(e.msg))}catch{}break}case"detach":{const n=d.get(e.connId);n&&n.dispose();break}case"pong":y=Date.now();break;case"ping":c({type:"pong",ts:e.ts});break;default:break}}function j(t){const e=z(t);if(e&&e.kind===R){const n=_.get(e.id)??[];n.push(e.payload),_.set(e.id,n)}}function A(){q();for(const t of d.values())try{t.dispose()}catch{}d.clear(),_.clear(),S.clear(),M.clear(),C=""}function O(){if(s){try{s.removeAllListeners()}catch{}s=null}if(A(),P)return;f("connecting"),I+=1;const e=(I<=H?1e3:Math.min(1e3*Math.pow(2,I-H),se))+Math.random()*500;E(`reconnecting in ${Math.round(e)}ms\u2026`),B=setTimeout($,e)}function $(){if(P||s)return;const t=o.hub.replace(/\/+$/,"").replace(/^http:/,"ws:").replace(/^https:/,"wss:")+"/agent/ws";let e;try{e=new h(t)}catch(n){p(`bad hub URL: ${n.message} (${t})`),f("error",`bad hub URL: ${n.message}`);return}s=e,f("connecting"),e.on("open",()=>{E(`connecting to ${t}\u2026`),c({type:"hello",token:o.token,name:o.name,version:o.version})}),e.on("message",(n,r)=>{r?j(Array.isArray(n)?Buffer.concat(n):Buffer.isBuffer(n)?n:Buffer.from(n)):W(typeof n=="string"?n:Buffer.from(n).toString("utf-8"))}),e.on("close",()=>{s===e&&O()}),e.on("error",n=>{p(`connection error: ${n.message}`),s===e&&O()})}function F(){if(!P){if(P=!0,B&&clearTimeout(B),A(),s){try{s.removeAllListeners(),s.close(1e3,"shutdown")}catch{}s=null}N=null,f("idle")}}return $(),{stop:F,status:()=>({...T})}}export{fe as startAgentClient};
@@ -1,3 +1,3 @@
1
- import{WebSocket as r}from"ws";import{decodeBinaryFrame as u,encodeBinaryFrame as f,BIN_KIND_TERM as g,BIN_KIND_HTTP_BODY as h}from"./agent-channel.js";const S=parseInt(process.env.TMUX_WEB_MAX_AGENT_CONNS||"64",10),_=parseInt(process.env.TMUX_WEB_TUNNEL_TIMEOUT_MS||"30000",10);let E=1,w=1;class k{ws;agentId;pending=new Map;relays=new Map;onSessions;onActivity;onRebuildResult;onClose;disposed=!1;constructor(t,e,s){this.ws=t,this.agentId=e,this.onSessions=s.onSessions,this.onActivity=s.onActivity,this.onRebuildResult=s.onRebuildResult,this.onClose=s.onClose}send(t){return this.disposed||this.ws.readyState!==r.OPEN?!1:(this.ws.send(JSON.stringify(t)),!0)}attach(t,e,s){return this.send({type:"attach",connId:t,session:e,cols:s?.cols,rows:s?.rows})}detach(t){this.send({type:"detach",connId:t})}sendToAgent(t,e){this.send({type:"ws_to_agent",connId:t,msg:e})}sendSessionsReq(){this.send({type:"sessions_req"})}rebuildSession(t,e){return this.send({type:"rebuild_session",session:t,dir:e})}async httpRequest(t,e,s,n){const i=w++,o=!!n,c=new Promise((d,a)=>{const l=setTimeout(()=>{this.pending.delete(i),a(new Error("tunnel request timed out"))},_);this.pending.set(i,{id:i,resolve:d,reject:a,status:null,headers:null,body:[],hasBody:o,timer:l})});if(!this.send({type:"http_req",id:i,method:t,path:e,headers:s,hasBody:o}))return this.pending.delete(i),Promise.reject(new Error("agent offline"));if(n){const d=n.getReader();(async()=>{for(;;){const{done:l,value:p}=await d.read();if(l)break;const y=Buffer.from(p);this.ws.send(f(h,i,y))}this.send({type:"http_body_end",id:i})})().catch(()=>{this.send({type:"http_body_end",id:i})})}return c}registerRelay(t,e,s){this.relays.set(t,{connId:t,session:e,ws:s,closed:!1})}relayClosed(t){const e=this.relays.get(t);e&&!e.closed&&(e.closed=!0,this.relays.delete(t),this.detach(t))}get relayCount(){return this.relays.size}handleData(t){const e=Buffer.isBuffer(t)?t:Buffer.from(t),s=u(e);if(s){if(s.kind===g){const n=this.relays.get(s.id);n&&!n.closed&&n.ws.readyState===r.OPEN&&n.ws.send(JSON.stringify({type:"data",data:s.payload.toString("utf-8")}));return}if(s.kind===h){const n=this.pending.get(s.id);n&&n.body.push(s.payload)}}}handleMessage(t){let e;try{e=JSON.parse(t)}catch{return}switch(e.type){case"sessions":this.onSessions?.(e.sessions);break;case"activity":this.onActivity?.(e.activities);break;case"rebuild_session_result":this.onRebuildResult?.({session:e.session,ok:e.ok,message:e.message});break;case"http_resp":{const s=this.pending.get(e.id);if(!s)break;s.status=e.status,s.headers=e.headers,e.hasBody||this.finalizeHttp(s.id);break}case"http_body_end":this.finalizeHttp(e.id);break;case"attach_ok":{const s=this.relays.get(e.connId);s&&!s.closed&&s.ws.readyState===r.OPEN&&s.ws.send(JSON.stringify({type:"auth.ok",setupMode:!1}));break}case"attach_err":{const s=this.relays.get(e.connId);s&&!s.closed&&s.ws.readyState===r.OPEN&&s.ws.send(JSON.stringify({type:"attach_failed",message:`Agent "${this.agentId}": cannot attach to session "${e.session}": ${e.message}`}));break}case"ws_to_hub":{const s=this.relays.get(e.connId);s&&!s.closed&&s.ws.readyState===r.OPEN&&s.ws.send(JSON.stringify(e.msg));break}case"ws_close":{const s=this.relays.get(e.connId);if(s&&!s.closed){s.closed=!0;try{s.ws.close(e.code??1e3,e.reason??"agent closed")}catch{}this.relays.delete(e.connId)}break}case"ping":this.send({type:"pong",ts:e.ts});break;default:break}}finalizeHttp(t){const e=this.pending.get(t);if(!e)return;this.pending.delete(t),clearTimeout(e.timer);const s=e.status??500,n=e.headers??{},i=Buffer.concat(e.body),o={};for(const[c,d]of Object.entries(n)){const a=c.toLowerCase();(a==="content-type"||a==="content-disposition"||a==="cache-control"||a==="etag"||a==="location")&&(o[c]=d)}e.resolve(new Response(i,{status:s,headers:o}))}failAll(t){for(const e of this.pending.values())clearTimeout(e.timer),e.reject(new Error(t));this.pending.clear();for(const e of this.relays.values()){if(!e.closed&&e.ws.readyState===r.OPEN){e.ws.send(JSON.stringify({type:"data",data:`\r
2
- \x1B[31m--- agent disconnected (${t}) ---\x1B[0m\r
3
- `}));try{e.ws.close(1011,"agent disconnected")}catch{}}e.closed=!0}this.relays.clear()}dispose(){this.disposed||(this.disposed=!0,this.failAll("agent disconnected"),this.onClose?.(this))}}export{k as AgentChannel,S as MAX_RELAY_CONNS_PER_AGENT,_ as TUNNEL_REQUEST_TIMEOUT_MS};
1
+ import{WebSocket as r}from"ws";import{decodeBinaryFrame as u,encodeBinaryFrame as f,BIN_KIND_TERM as g,BIN_KIND_HTTP_BODY as h}from"./agent-channel.js";const _=parseInt(process.env.TMUX_WEB_HUB_HEARTBEAT_MS||"15000",10),w=parseInt(process.env.TMUX_WEB_HUB_PONG_TIMEOUT_MS||"90000",10),N=parseInt(process.env.TMUX_WEB_MAX_AGENT_CONNS||"64",10),b=parseInt(process.env.TMUX_WEB_TUNNEL_TIMEOUT_MS||"30000",10);let O=1,m=1;class B{ws;agentId;pending=new Map;relays=new Map;onSessions;onActivity;onRebuildResult;onClose;lastPongAt=Date.now();heartbeatTimer=null;disposed=!1;constructor(s,e,t){this.ws=s,this.agentId=e,this.onSessions=t.onSessions,this.onActivity=t.onActivity,this.onRebuildResult=t.onRebuildResult,this.onClose=t.onClose,this.heartbeatTimer=setInterval(()=>{if(!(this.disposed||this.ws.readyState!==r.OPEN)){if(Date.now()-this.lastPongAt>w){try{this.ws.close(1001,"agent heartbeat timeout")}catch{}return}this.send({type:"ping",ts:Date.now()})}},_)}send(s){return this.disposed||this.ws.readyState!==r.OPEN?!1:(this.ws.send(JSON.stringify(s)),!0)}attach(s,e,t){return this.send({type:"attach",connId:s,session:e,cols:t?.cols,rows:t?.rows})}detach(s){this.send({type:"detach",connId:s})}sendToAgent(s,e){this.send({type:"ws_to_agent",connId:s,msg:e})}sendSessionsReq(){this.send({type:"sessions_req"})}rebuildSession(s,e){return this.send({type:"rebuild_session",session:s,dir:e})}async httpRequest(s,e,t,n){const i=m++,o=!!n,c=new Promise((d,a)=>{const l=setTimeout(()=>{this.pending.delete(i),a(new Error("tunnel request timed out"))},b);this.pending.set(i,{id:i,resolve:d,reject:a,status:null,headers:null,body:[],hasBody:o,timer:l})});if(!this.send({type:"http_req",id:i,method:s,path:e,headers:t,hasBody:o}))return this.pending.delete(i),Promise.reject(new Error("agent offline"));if(n){const d=n.getReader();(async()=>{for(;;){const{done:l,value:p}=await d.read();if(l)break;const y=Buffer.from(p);this.ws.send(f(h,i,y))}this.send({type:"http_body_end",id:i})})().catch(()=>{this.send({type:"http_body_end",id:i})})}return c}registerRelay(s,e,t){this.relays.set(s,{connId:s,session:e,ws:t,closed:!1})}relayClosed(s){const e=this.relays.get(s);e&&!e.closed&&(e.closed=!0,this.relays.delete(s),this.detach(s))}get relayCount(){return this.relays.size}handleData(s){const e=Buffer.isBuffer(s)?s:Buffer.from(s),t=u(e);if(t){if(t.kind===g){const n=this.relays.get(t.id);n&&!n.closed&&n.ws.readyState===r.OPEN&&n.ws.send(JSON.stringify({type:"data",data:t.payload.toString("utf-8")}));return}if(t.kind===h){const n=this.pending.get(t.id);n&&n.body.push(t.payload)}}}handleMessage(s){let e;try{e=JSON.parse(s)}catch{return}switch(e.type){case"sessions":this.onSessions?.(e.sessions);break;case"activity":this.onActivity?.(e.activities);break;case"rebuild_session_result":this.onRebuildResult?.({session:e.session,ok:e.ok,message:e.message});break;case"http_resp":{const t=this.pending.get(e.id);if(!t)break;t.status=e.status,t.headers=e.headers,e.hasBody||this.finalizeHttp(t.id);break}case"http_body_end":this.finalizeHttp(e.id);break;case"attach_ok":{const t=this.relays.get(e.connId);t&&!t.closed&&t.ws.readyState===r.OPEN&&t.ws.send(JSON.stringify({type:"auth.ok",setupMode:!1}));break}case"attach_err":{const t=this.relays.get(e.connId);t&&!t.closed&&t.ws.readyState===r.OPEN&&t.ws.send(JSON.stringify({type:"attach_failed",message:`Agent "${this.agentId}": cannot attach to session "${e.session}": ${e.message}`}));break}case"ws_to_hub":{const t=this.relays.get(e.connId);t&&!t.closed&&t.ws.readyState===r.OPEN&&t.ws.send(JSON.stringify(e.msg));break}case"ws_close":{const t=this.relays.get(e.connId);if(t&&!t.closed){t.closed=!0;try{t.ws.close(e.code??1e3,e.reason??"agent closed")}catch{}this.relays.delete(e.connId)}break}case"ping":this.lastPongAt=Date.now(),this.send({type:"pong",ts:e.ts});break;default:break}}finalizeHttp(s){const e=this.pending.get(s);if(!e)return;this.pending.delete(s),clearTimeout(e.timer);const t=e.status??500,n=e.headers??{},i=Buffer.concat(e.body),o={};for(const[c,d]of Object.entries(n)){const a=c.toLowerCase();(a==="content-type"||a==="content-disposition"||a==="cache-control"||a==="etag"||a==="location")&&(o[c]=d)}e.resolve(new Response(i,{status:t,headers:o}))}failAll(s){for(const e of this.pending.values())clearTimeout(e.timer),e.reject(new Error(s));this.pending.clear();for(const e of this.relays.values()){if(!e.closed&&e.ws.readyState===r.OPEN){e.ws.send(JSON.stringify({type:"data",data:`\r
2
+ \x1B[31m--- agent disconnected (${s}) ---\x1B[0m\r
3
+ `}));try{e.ws.close(1011,"agent disconnected")}catch{}}e.closed=!0}this.relays.clear()}dispose(){this.disposed||(this.disposed=!0,this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null),this.failAll("agent disconnected"),this.onClose?.(this))}}export{B as AgentChannel,N as MAX_RELAY_CONNS_PER_AGENT,b as TUNNEL_REQUEST_TIMEOUT_MS};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-weblink",
3
- "version": "2.2.10",
3
+ "version": "2.2.11",
4
4
  "description": "Access your tmux sessions from the browser. A continuation of tmux-web and persalink.",
5
5
  "type": "module",
6
6
  "bin": {