tmux-weblink 2.2.3 → 2.2.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.
Files changed (2) hide show
  1. package/dist/index.js +4 -4
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import{chmodSync as he,existsSync as z,readdirSync as ge,statSync as ye}from"node:fs";import{createRequire as G}from"node:module";import{hostname as U}from"node:os";const ke=G(import.meta.url),{version:Te}=ke("../package.json");import $ from"node:path";import{fileURLToPath as ve}from"node:url";import{serve as _e}from"@hono/node-server";import{WebSocketServer as we,WebSocket as E}from"ws";try{const e=$.dirname(G(import.meta.url).resolve("node-pty/package.json")),n=$.join(e,"prebuilds");if(z(n))for(const o of ge(n)){const t=$.join(n,o,"spawn-helper");z(t)&&!(ye(t).mode&73)&&he(t,493)}}catch{}import{loadSecurityConfig as Se}from"./lib/security-config.js";import{TokenStore as Ae,hashPassword as Ce,verifyPassword as be,validatePassword as xe}from"./lib/auth.js";import{RateLimiter as Ie}from"./lib/rateLimiter.js";import{audit as d}from"./lib/auditLog.js";import{db as N}from"./lib/db.js";import{handleClientMessage as V}from"./lib/ws-message.js";import{loadDotEnv as Pe}from"./lib/load-env.js";import{listSessions as Re,resolveTmuxSocketPath as Me}from"./sessions.js";import{cmdSetup as Oe,cmdTheme as $e,printUsage as X,printVersion as Ne}from"./lib/cli.js";import{readSettings as Le}from"./lib/settings.js";import{readActiveTheme as Ue}from"./lib/theme-store.js";import{buildApp as Y}from"./lib/build-app.js";import{attachTerminal as Ee,killAllAttachedPtys as Fe}from"./lib/attach-terminal.js";import{acquireControlClient as K,killAllControlClients as He}from"./lib/tmux-control.js";import{getSessionPaneTarget as Q,capturePaneTail as Z,capturePaneHistoryChunk as q,toCrlf as ee}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as je}from"./lib/terminal-config.js";import{AgentRegistry as Be}from"./lib/agent-registry.js";import{AgentChannel as We,MAX_RELAY_CONNS_PER_AGENT as Je}from"./lib/agent-relay.js";import{findAgentTokenByPlaintext as De,listAgentTokens as ze}from"./lib/agent-tokens.js";import{saveSecurityConfig as Ge}from"./lib/security-config.js";import{readFederationConfig as Ve,writeFederationConfig as Xe}from"./lib/federation-config.js";import{startAgentClient as Ye}from"./lib/agent-client.js";import{ActivityProbe as Ke}from"./lib/activity-probe.js";import{upsertSessionState as te}from"./lib/session-state.js";import{sessionWorkingPath as Qe}from"./lib/sessions-sidebar.js";Pe();const F=je(),g=Se(),T=new Ae,A=new Ie;let J=!1;const Ze="tmux-web-token";function H(e){if(g.security.trustProxy){const n=(e.headers["x-forwarded-for"]||"").split(",")[0].trim();if(n)return n}return e.socket.remoteAddress||"unknown"}function j(e){const n=e.headers.authorization;if(typeof n=="string"&&n.startsWith("Bearer "))return n.slice(7);const o=e.headers.cookie;if(typeof o!="string")return null;const t=o.match(new RegExp(`(?:^|;\\s*)${Ze}=([^;]+)`));return t?decodeURIComponent(t[1]):null}function zt(e){const n=j(e);return n?T.validateToken(n):null}const R=new Map;function qe(e){let n=0;for(const o of R.values())o.ip===e&&n++;return n}function b(e,n,o){try{e.close(n,o)}catch{}}function ne(e,n){e.readyState===E.OPEN&&e.send(JSON.stringify(n))}const oe=process.argv.slice(2),L=oe;if(L.length>0){const[e,n]=L;switch(e){case"setup":await Oe(L),process.exit(0);case"theme":await $e(L.slice(1)),process.exit(0);case"agent":{const o=$.join($.dirname(ve(import.meta.url)),"agent.js"),{spawn:t}=await import("node:child_process"),f=t(process.execPath,[o,...oe],{stdio:"inherit"});f.on("error",s=>{console.error(s),process.exit(1)}),f.on("exit",s=>process.exit(s??0)),await new Promise(()=>{})}case"agent-token":await ft(L.slice(1)),process.exit(0);case"help":case"--help":case"-h":X(),process.exit(0);case"-V":case"--version":case"-v":Ne(),process.exit(0);default:console.error(`unknown argument: ${e}`),X(),process.exit(1)}}function P(e,n){e.readyState===E.OPEN&&e.send(JSON.stringify(n))}const re=new Set;await N.read(),N.data.sessionAccess??=[],N.data.pinnedViews??=[],N.data.watchedPanes??=[],N.data.quickCommands??=[];const se=await Le();let et=await Ue();const k=new Be({onChange:()=>{}}),_=new Map;function ae(e){return k.get(e)?_.get(e)??null:null}const ie={activeTheme:et,settingUpPassword:!1},tt=Y({mode:"hub",securityConfig:g,tokenStore:T,rateLimiter:A,settings:se,terminalBufferConfig:F,state:ie,federation:{getConfig:()=>x,setConfig:async e=>{x=e,await Xe(e),le(e)},status:ot,hostname:U()},hub:{agents:k,getChannel:ae}}),nt=Y({mode:"agent",securityConfig:g,tokenStore:T,rateLimiter:A,settings:se,terminalBufferConfig:F,state:ie});let x=await Ve(),M=null;function ce(){M&&(M.stop(),M=null)}function le(e){ce(),!(!e.enabled||!e.hub||!e.token)&&(M=Ye({hub:e.hub,token:e.token,name:e.name?.trim()||U(),version:Te,app:nt,listSessions:Re,terminalBufferConfig:F,getSessionPaneTarget:Q,capturePaneTail:Z,capturePaneHistoryChunk:q,toCrlf:ee,handleClientMessage:V,acquireControlClient:K}))}function ot(){return x.enabled?M?M.status():{state:"idle",agentId:null,hub:x.hub??"",name:x.name??U()}:{state:"idle",agentId:null,hub:x.hub??"",name:x.name??U()}}le(x);const ue=new Ke,B=new Map;setInterval(()=>{(async()=>{try{const e=await ue.scan(),n=[];for(const[o,t]of e)B.get(o)!==t&&n.push({session:o,state:t});for(const o of B.keys())e.has(o)||n.push({session:o,state:"idle"});B.clear();for(const[o,t]of e)B.set(o,t);for(const o of n)de({type:"session.activity",session:o.session,state:o.state})}catch{}})()},ue.scanMs);const rt=parseInt(process.env.PORT||"21000",10),st=_e({fetch:tt.fetch,port:rt,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),O=new we({noServer:!0}),D=new Set;function de(e){const n=JSON.stringify(e);for(const o of D)o.readyState===E.OPEN&&o.send(n)}function I(e,n,o){e.write(`HTTP/1.1 ${n} ${o}\r
2
+ import{chmodSync as he,existsSync as z,readdirSync as ge,statSync as ye}from"node:fs";import{createRequire as G}from"node:module";import{hostname as U,networkInterfaces as ke}from"node:os";const ve=G(import.meta.url),{version:Te}=ve("../package.json");import $ from"node:path";import{fileURLToPath as _e}from"node:url";import{serve as we}from"@hono/node-server";import{WebSocketServer as Se,WebSocket as E}from"ws";try{const e=$.dirname(G(import.meta.url).resolve("node-pty/package.json")),t=$.join(e,"prebuilds");if(z(t))for(const o of ge(t)){const n=$.join(t,o,"spawn-helper");z(n)&&!(ye(n).mode&73)&&he(n,493)}}catch{}import{loadSecurityConfig as Ae}from"./lib/security-config.js";import{TokenStore as Ce,hashPassword as be,verifyPassword as xe,validatePassword as Ie}from"./lib/auth.js";import{RateLimiter as Pe}from"./lib/rateLimiter.js";import{audit as d}from"./lib/auditLog.js";import{db as N}from"./lib/db.js";import{handleClientMessage as V}from"./lib/ws-message.js";import{loadDotEnv as Re}from"./lib/load-env.js";import{listSessions as Me,resolveTmuxSocketPath as Oe}from"./sessions.js";import{cmdSetup as $e,cmdTheme as Ne,printUsage as X,printVersion as Le}from"./lib/cli.js";import{readSettings as Ue}from"./lib/settings.js";import{readActiveTheme as Ee}from"./lib/theme-store.js";import{buildApp as Y}from"./lib/build-app.js";import{attachTerminal as Fe,killAllAttachedPtys as je}from"./lib/attach-terminal.js";import{acquireControlClient as K,killAllControlClients as He}from"./lib/tmux-control.js";import{getSessionPaneTarget as Q,capturePaneTail as Z,capturePaneHistoryChunk as q,toCrlf as ee}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as Be}from"./lib/terminal-config.js";import{AgentRegistry as We}from"./lib/agent-registry.js";import{AgentChannel as Je,MAX_RELAY_CONNS_PER_AGENT as De}from"./lib/agent-relay.js";import{findAgentTokenByPlaintext as ze,listAgentTokens as Ge}from"./lib/agent-tokens.js";import{saveSecurityConfig as Ve}from"./lib/security-config.js";import{readFederationConfig as Xe,writeFederationConfig as Ye}from"./lib/federation-config.js";import{startAgentClient as Ke}from"./lib/agent-client.js";import{ActivityProbe as Qe}from"./lib/activity-probe.js";import{upsertSessionState as te}from"./lib/session-state.js";import{sessionWorkingPath as Ze}from"./lib/sessions-sidebar.js";Re();const F=Be(),g=Ae(),v=new Ce,A=new Pe;let J=!1;const qe="tmux-web-token";function j(e){if(g.security.trustProxy){const t=(e.headers["x-forwarded-for"]||"").split(",")[0].trim();if(t)return t}return e.socket.remoteAddress||"unknown"}function H(e){const t=e.headers.authorization;if(typeof t=="string"&&t.startsWith("Bearer "))return t.slice(7);const o=e.headers.cookie;if(typeof o!="string")return null;const n=o.match(new RegExp(`(?:^|;\\s*)${qe}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function Vt(e){const t=H(e);return t?v.validateToken(t):null}const R=new Map;function et(e){let t=0;for(const o of R.values())o.ip===e&&t++;return t}function b(e,t,o){try{e.close(t,o)}catch{}}function ne(e,t){e.readyState===E.OPEN&&e.send(JSON.stringify(t))}const oe=process.argv.slice(2),L=oe;if(L.length>0){const[e,t]=L;switch(e){case"setup":await $e(L),process.exit(0);case"theme":await Ne(L.slice(1)),process.exit(0);case"agent":{const o=$.join($.dirname(_e(import.meta.url)),"agent.js"),{spawn:n}=await import("node:child_process"),f=n(process.execPath,[o,...oe],{stdio:"inherit"});f.on("error",s=>{console.error(s),process.exit(1)}),f.on("exit",s=>process.exit(s??0)),await new Promise(()=>{})}case"agent-token":await pt(L.slice(1)),process.exit(0);case"help":case"--help":case"-h":X(),process.exit(0);case"-V":case"--version":case"-v":Le(),process.exit(0);default:console.error(`unknown argument: ${e}`),X(),process.exit(1)}}function P(e,t){e.readyState===E.OPEN&&e.send(JSON.stringify(t))}const re=new Set;await N.read(),N.data.sessionAccess??=[],N.data.pinnedViews??=[],N.data.watchedPanes??=[],N.data.quickCommands??=[];const se=await Ue();let tt=await Ee();const k=new We({onChange:()=>{}}),_=new Map;function ae(e){return k.get(e)?_.get(e)??null:null}const ie={activeTheme:tt,settingUpPassword:!1},nt=Y({mode:"hub",securityConfig:g,tokenStore:v,rateLimiter:A,settings:se,terminalBufferConfig:F,state:ie,federation:{getConfig:()=>x,setConfig:async e=>{x=e,await Ye(e),le(e)},status:rt,hostname:U()},hub:{agents:k,getChannel:ae}}),ot=Y({mode:"agent",securityConfig:g,tokenStore:v,rateLimiter:A,settings:se,terminalBufferConfig:F,state:ie});let x=await Xe(),M=null;function ce(){M&&(M.stop(),M=null)}function le(e){ce(),!(!e.enabled||!e.hub||!e.token)&&(M=Ke({hub:e.hub,token:e.token,name:e.name?.trim()||U(),version:Te,app:ot,listSessions:Me,terminalBufferConfig:F,getSessionPaneTarget:Q,capturePaneTail:Z,capturePaneHistoryChunk:q,toCrlf:ee,handleClientMessage:V,acquireControlClient:K}))}function rt(){return x.enabled?M?M.status():{state:"idle",agentId:null,hub:x.hub??"",name:x.name??U()}:{state:"idle",agentId:null,hub:x.hub??"",name:x.name??U()}}le(x);const ue=new Qe,B=new Map;setInterval(()=>{(async()=>{try{const e=await ue.scan(),t=[];for(const[o,n]of e)B.get(o)!==n&&t.push({session:o,state:n});for(const o of B.keys())e.has(o)||t.push({session:o,state:"idle"});B.clear();for(const[o,n]of e)B.set(o,n);for(const o of t)de({type:"session.activity",session:o.session,state:o.state})}catch{}})()},ue.scanMs);const st=parseInt(process.env.PORT||"21000",10);function at(){const e=[];try{for(const t of Object.values(ke()))for(const o of t??[])o.family==="IPv4"&&!o.internal&&e.push(o.address)}catch{}return e}const it=we({fetch:nt.fetch,port:st,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://localhost:${e.port}`);for(const t of at())console.log(` also on http://${t}:${e.port}`)}),O=new Se({noServer:!0}),D=new Set;function de(e){const t=JSON.stringify(e);for(const o of D)o.readyState===E.OPEN&&o.send(t)}function I(e,t,o){e.write(`HTTP/1.1 ${t} ${o}\r
3
3
  \r
4
- `),e.destroy()}st.on("upgrade",(e,n,o)=>{const t=new URL(e.url||"/",`http://${e.headers.host}`),f=t.pathname;if(f==="/ws/activity"){const w=H(e),a=j(e)||t.searchParams.get("token")||"";if(!(a?T.validateToken(a):null)){I(n,401,"Unauthorized");return}O.handleUpgrade(e,n,o,c=>{c.on("close",()=>D.delete(c)),D.add(c)});return}if(f==="/agent/ws"){lt(e,n,o);return}const s=f.match(/^\/ws\/a\/([^/]+)\/(.+)$/);if(s){at(e,n,o,decodeURIComponent(s[1]),decodeURIComponent(s[2]));return}const i=f.match(/^\/ws\/(.+)$/);if(!i){n.destroy();return}const l=H(e),u=e.headers.origin,p=g.security.allowedOrigins;if(u&&!(u===`http://${e.headers.host}`||u===`https://${e.headers.host}`)&&(p.length===0||!p.includes(u))){d("ws_rejected_origin",{ip:l,origin:u}),I(n,403,"Origin not allowed");return}const m=qe(l);if(m>=g.security.maxConnectionsPerIp){d("ws_rejected_per_ip_cap",{ip:l,liveFromIp:m}),I(n,429,"Too many connections");return}const y=j(e)||t.searchParams.get("token")||"",v=y?T.validateToken(y):null;O.handleUpgrade(e,n,o,w=>{O.emit("connection",w,e,decodeURIComponent(i[1]),l,v)})}),O.on("connection",(e,n,o,t,f)=>{const s={ws:e,ip:t,authenticated:!1,authTimeout:null},i=(()=>{try{const a=new URL(n.url||"/",`http://${n.headers.host}`).searchParams,r=Number(a.get("cols")),c=Number(a.get("rows"));if(Number.isInteger(r)&&Number.isInteger(c)&&r>0&&c>0&&r<=1e3&&c<=1e3)return{cols:r,rows:c}}catch{}})();R.set(e,s),d("ws_connected",{ip:t});let l=null,u=null;function p(a,r){s.authenticated=!0,s.authTimeout&&(clearTimeout(s.authTimeout),s.authTimeout=null),d("auth_success",{ip:t,method:a,tokenName:r}),P(e,{type:"auth.ok",setupMode:!g.passwordHash}),y()}function m(a,r={}){P(e,{type:"auth.failed",message:a,...r})}function y(){l||(te(o,{path:Qe(o)}),l=Ee(o,{send:a=>P(e,a),onMessage:a=>{u=a},onClose:a=>{v=a}},{terminalBufferConfig:F,getSessionPaneTarget:Q,capturePaneTail:Z,capturePaneHistoryChunk:q,toCrlf:ee,handleClientMessage:V,acquireControlClient:K,initialCols:i?.cols,initialRows:i?.rows,tmuxSocketPath:Me()??void 0,onPtyExit:(a,r)=>b(e,1e3,"pty exited"),onSpawnError:(a,r)=>b(e,1011,"pty spawn failed")}))}let v=null;async function w(a){let r;try{r=JSON.parse(a)}catch{return!1}if(r.type==="auth.token"&&typeof r.token=="string"){const c=T.validateToken(r.token);if(!c){d("token_auth_failed",{ip:t});const h=A.recordFailure(t);return h.permanentLock&&(T.revokeAll(),d("permanent_lock",{ip:t,failures:h.failures})),m("Invalid or expired token",{permanentLock:h.permanentLock,retryAfterMs:h.retryAfterMs}),!0}return T.touch(c.tokenHash),p("token",c.name),!0}if(r.type==="auth"&&typeof r.password=="string"){const c=A.check(t);if(!c.allowed)return d("rate_limited",{ip:t,retryAfterMs:c.retryAfterMs,permanentLock:c.permanentLock}),m("Too many attempts",{permanentLock:c.permanentLock,retryAfterMs:c.retryAfterMs}),!0;if(!g.passwordHash){if(!g.security.allowRemoteSetup&&!dt(t))return d("setup_rejected_remote",{ip:t}),m("First-run setup must be performed from localhost"),!0;if(J)return m("Password setup in progress"),!0;const C=xe(r.password);if(C)return m(C),!0;J=!0;try{g.passwordHash=await Ce(r.password),Ge(g),d("password_set",{ip:t})}finally{J=!1}const{stored:W,plaintext:pe}=T.createAccessToken("setup",g.security.tokenTtlDays);return P(e,{type:"auth.ok",setupMode:!0,token:pe}),d("token_created",{ip:t,name:W.name,tokenId:W.id}),d("auth_success",{ip:t,method:"password",tokenName:W.name}),y(),!0}if(!g.passwordHash)return m("Server not configured"),!0;if(!await be(r.password,g.passwordHash)){const C=A.recordFailure(t);return d("auth_failed",{ip:t,method:"password",failures:C.failures,permanentLock:C.permanentLock}),C.permanentLock&&(T.revokeAll(),d("permanent_lock",{ip:t,failures:C.failures})),m("Incorrect password",{permanentLock:C.permanentLock,retryAfterMs:C.retryAfterMs}),!0}A.recordSuccess(t);const S=`ws-${t}`,{plaintext:me}=T.createAccessToken(S,g.security.tokenTtlDays);return p("password",S),P(e,{type:"auth.ok",setupMode:!1,token:me}),!0}return!1}f?(T.touch(f.tokenHash),p("token",f.name)):(P(e,{type:"auth.required",setupMode:!g.passwordHash}),s.authTimeout=setTimeout(()=>{s.authenticated||(d("auth_timeout",{ip:t}),m("Authentication timeout"),b(e,4e3,"Auth timeout"))},g.security.authTimeoutMs)),e.on("message",async a=>{const r=typeof a=="string"?a:a.toString("utf-8");if(!s.authenticated){if(r.length>1e6){P(e,{type:"auth.failed",message:"Message too large"});return}await w(r)||m("Authentication required");return}if(u)try{u(r)}catch{}}),e.on("close",()=>{s.authTimeout&&clearTimeout(s.authTimeout),R.delete(e),d("ws_disconnected",{ip:t}),v?.(),l&&(l.dispose(),l=null)}),e.on("error",()=>{s.authTimeout&&clearTimeout(s.authTimeout),R.delete(e),v?.(),l&&(l.dispose(),l=null)})});function at(e,n,o,t,f){const s=H(e),i=new URL(e.url||"/",`http://${e.headers.host}`),l=j(e)||i.searchParams.get("token")||"";if(!(l?T.validateToken(l):null)){d("ws_rejected_remote_auth",{ip:s,agentId:t}),I(n,401,"Unauthorized");return}const p=ae(t);if(!p){d("ws_rejected_remote_offline",{ip:s,agentId:t}),I(n,503,"Agent offline");return}if(p.relayCount>=Je){I(n,429,"Too many connections to agent");return}const m=e.headers.origin,y=g.security.allowedOrigins;if(m&&!(m===`http://${e.headers.host}`||m===`https://${e.headers.host}`)&&(y.length===0||!y.includes(m))){I(n,403,"Origin not allowed");return}const v=ct(),w=(()=>{try{const a=new URL(e.url||"/",`http://${e.headers.host}`).searchParams,r=Number(a.get("cols")),c=Number(a.get("rows"));if(Number.isInteger(r)&&Number.isInteger(c)&&r>0&&c>0&&r<=1e3&&c<=1e3)return{cols:r,rows:c}}catch{}})();O.handleUpgrade(e,n,o,a=>{if(p.registerRelay(v,f,a),k.bumpRelayConns(t,1),te(f,{agentId:t}),!p.attach(v,f,w)){a.send(JSON.stringify({type:"data",data:`\r
4
+ `),e.destroy()}it.on("upgrade",(e,t,o)=>{const n=new URL(e.url||"/",`http://${e.headers.host}`),f=n.pathname;if(f==="/ws/activity"){const w=j(e),a=H(e)||n.searchParams.get("token")||"";if(!(a?v.validateToken(a):null)){I(t,401,"Unauthorized");return}O.handleUpgrade(e,t,o,c=>{c.on("close",()=>D.delete(c)),D.add(c)});return}if(f==="/agent/ws"){dt(e,t,o);return}const s=f.match(/^\/ws\/a\/([^/]+)\/(.+)$/);if(s){ct(e,t,o,decodeURIComponent(s[1]),decodeURIComponent(s[2]));return}const i=f.match(/^\/ws\/(.+)$/);if(!i){t.destroy();return}const l=j(e),u=e.headers.origin,p=g.security.allowedOrigins;if(u&&!(u===`http://${e.headers.host}`||u===`https://${e.headers.host}`)&&(p.length===0||!p.includes(u))){d("ws_rejected_origin",{ip:l,origin:u}),I(t,403,"Origin not allowed");return}const m=et(l);if(m>=g.security.maxConnectionsPerIp){d("ws_rejected_per_ip_cap",{ip:l,liveFromIp:m}),I(t,429,"Too many connections");return}const y=H(e)||n.searchParams.get("token")||"",T=y?v.validateToken(y):null;O.handleUpgrade(e,t,o,w=>{O.emit("connection",w,e,decodeURIComponent(i[1]),l,T)})}),O.on("connection",(e,t,o,n,f)=>{const s={ws:e,ip:n,authenticated:!1,authTimeout:null},i=(()=>{try{const a=new URL(t.url||"/",`http://${t.headers.host}`).searchParams,r=Number(a.get("cols")),c=Number(a.get("rows"));if(Number.isInteger(r)&&Number.isInteger(c)&&r>0&&c>0&&r<=1e3&&c<=1e3)return{cols:r,rows:c}}catch{}})();R.set(e,s),d("ws_connected",{ip:n});let l=null,u=null;function p(a,r){s.authenticated=!0,s.authTimeout&&(clearTimeout(s.authTimeout),s.authTimeout=null),d("auth_success",{ip:n,method:a,tokenName:r}),P(e,{type:"auth.ok",setupMode:!g.passwordHash}),y()}function m(a,r={}){P(e,{type:"auth.failed",message:a,...r})}function y(){l||(te(o,{path:Ze(o)}),l=Fe(o,{send:a=>P(e,a),onMessage:a=>{u=a},onClose:a=>{T=a}},{terminalBufferConfig:F,getSessionPaneTarget:Q,capturePaneTail:Z,capturePaneHistoryChunk:q,toCrlf:ee,handleClientMessage:V,acquireControlClient:K,initialCols:i?.cols,initialRows:i?.rows,tmuxSocketPath:Oe()??void 0,onPtyExit:(a,r)=>b(e,1e3,"pty exited"),onSpawnError:(a,r)=>b(e,1011,"pty spawn failed")}))}let T=null;async function w(a){let r;try{r=JSON.parse(a)}catch{return!1}if(r.type==="auth.token"&&typeof r.token=="string"){const c=v.validateToken(r.token);if(!c){d("token_auth_failed",{ip:n});const h=A.recordFailure(n);return h.permanentLock&&(v.revokeAll(),d("permanent_lock",{ip:n,failures:h.failures})),m("Invalid or expired token",{permanentLock:h.permanentLock,retryAfterMs:h.retryAfterMs}),!0}return v.touch(c.tokenHash),p("token",c.name),!0}if(r.type==="auth"&&typeof r.password=="string"){const c=A.check(n);if(!c.allowed)return d("rate_limited",{ip:n,retryAfterMs:c.retryAfterMs,permanentLock:c.permanentLock}),m("Too many attempts",{permanentLock:c.permanentLock,retryAfterMs:c.retryAfterMs}),!0;if(!g.passwordHash){if(!g.security.allowRemoteSetup&&!mt(n))return d("setup_rejected_remote",{ip:n}),m("First-run setup must be performed from localhost"),!0;if(J)return m("Password setup in progress"),!0;const C=Ie(r.password);if(C)return m(C),!0;J=!0;try{g.passwordHash=await be(r.password),Ve(g),d("password_set",{ip:n})}finally{J=!1}const{stored:W,plaintext:pe}=v.createAccessToken("setup",g.security.tokenTtlDays);return P(e,{type:"auth.ok",setupMode:!0,token:pe}),d("token_created",{ip:n,name:W.name,tokenId:W.id}),d("auth_success",{ip:n,method:"password",tokenName:W.name}),y(),!0}if(!g.passwordHash)return m("Server not configured"),!0;if(!await xe(r.password,g.passwordHash)){const C=A.recordFailure(n);return d("auth_failed",{ip:n,method:"password",failures:C.failures,permanentLock:C.permanentLock}),C.permanentLock&&(v.revokeAll(),d("permanent_lock",{ip:n,failures:C.failures})),m("Incorrect password",{permanentLock:C.permanentLock,retryAfterMs:C.retryAfterMs}),!0}A.recordSuccess(n);const S=`ws-${n}`,{plaintext:me}=v.createAccessToken(S,g.security.tokenTtlDays);return p("password",S),P(e,{type:"auth.ok",setupMode:!1,token:me}),!0}return!1}f?(v.touch(f.tokenHash),p("token",f.name)):(P(e,{type:"auth.required",setupMode:!g.passwordHash}),s.authTimeout=setTimeout(()=>{s.authenticated||(d("auth_timeout",{ip:n}),m("Authentication timeout"),b(e,4e3,"Auth timeout"))},g.security.authTimeoutMs)),e.on("message",async a=>{const r=typeof a=="string"?a:a.toString("utf-8");if(!s.authenticated){if(r.length>1e6){P(e,{type:"auth.failed",message:"Message too large"});return}await w(r)||m("Authentication required");return}if(u)try{u(r)}catch{}}),e.on("close",()=>{s.authTimeout&&clearTimeout(s.authTimeout),R.delete(e),d("ws_disconnected",{ip:n}),T?.(),l&&(l.dispose(),l=null)}),e.on("error",()=>{s.authTimeout&&clearTimeout(s.authTimeout),R.delete(e),T?.(),l&&(l.dispose(),l=null)})});function ct(e,t,o,n,f){const s=j(e),i=new URL(e.url||"/",`http://${e.headers.host}`),l=H(e)||i.searchParams.get("token")||"";if(!(l?v.validateToken(l):null)){d("ws_rejected_remote_auth",{ip:s,agentId:n}),I(t,401,"Unauthorized");return}const p=ae(n);if(!p){d("ws_rejected_remote_offline",{ip:s,agentId:n}),I(t,503,"Agent offline");return}if(p.relayCount>=De){I(t,429,"Too many connections to agent");return}const m=e.headers.origin,y=g.security.allowedOrigins;if(m&&!(m===`http://${e.headers.host}`||m===`https://${e.headers.host}`)&&(y.length===0||!y.includes(m))){I(t,403,"Origin not allowed");return}const T=ut(),w=(()=>{try{const a=new URL(e.url||"/",`http://${e.headers.host}`).searchParams,r=Number(a.get("cols")),c=Number(a.get("rows"));if(Number.isInteger(r)&&Number.isInteger(c)&&r>0&&c>0&&r<=1e3&&c<=1e3)return{cols:r,rows:c}}catch{}})();O.handleUpgrade(e,t,o,a=>{if(p.registerRelay(T,f,a),k.bumpRelayConns(n,1),te(f,{agentId:n}),!p.attach(T,f,w)){a.send(JSON.stringify({type:"data",data:`\r
5
5
  \x1B[31magent offline\x1B[0m\r
6
- `})),a.close(1011,"agent offline"),p.relayClosed(v),k.bumpRelayConns(t,-1);return}a.on("message",c=>{const h=typeof c=="string"?c:c.toString("utf-8");let S;try{S=JSON.parse(h)}catch{return}(S.type==="input"||S.type==="resize"||S.type==="load_history")&&p.sendToAgent(v,S)});const r=()=>{p.relayClosed(v),k.bumpRelayConns(t,-1)};a.on("close",r),a.on("error",r)})}let it=1;function ct(){return it++}function lt(e,n,o){const t=H(e),f=e.headers.origin;if(f){d("agent_rejected_origin",{ip:t,origin:f}),I(n,403,"Origin not allowed");return}O.handleUpgrade(e,n,o,s=>{let i=null,l=setTimeout(()=>{d("agent_hello_timeout",{ip:t}),b(s,4e3,"hello timeout")},1e4);s.on("message",(u,p)=>{if(i){k.touch(i.agentId),p?i.handleData(Array.isArray(u)?Buffer.concat(u):Buffer.from(u)):i.handleMessage(typeof u=="string"?u:Buffer.from(u).toString("utf-8"));return}const m=typeof u=="string"?u:u.toString("utf-8");let y;try{y=JSON.parse(m)}catch{b(s,1008,"bad hello");return}if(y.type!=="hello"||typeof y.token!="string"){b(s,1008,"expected hello");return}if(!A.check(t).allowed){d("agent_rate_limited",{ip:t}),b(s,429,"Too many attempts");return}const w=De(y.token);if(!w){A.recordFailure(t),d("agent_bad_token",{ip:t}),ne(s,{type:"hello_err",reason:"invalid token"}),b(s,4003,"invalid token");return}const a=typeof y.name=="string"&&y.name.trim()?y.name.trim().slice(0,64):"agent",r=w.id;if(!k.register(r,a,s,w.tokenHash)){d("agent_cap_reached",{ip:t}),ne(s,{type:"hello_err",reason:"too many agents"}),b(s,4004,"too many agents");return}clearTimeout(l),A.recordSuccess(t),d("agent_connected",{ip:t,agentId:r,name:a}),i=new We(s,r,{onSessions:h=>k.setSessions(r,h),onActivity:h=>{for(const S of h)de({type:"session.activity",agentId:r,session:S.session,state:S.state})},onRebuildResult:h=>{console.log(`[agent ${r}] rebuild ${h.session}: ${h.ok?"ok":h.message}`)},onClose:h=>{_.get(r)===h&&_.delete(r)}}),_.set(r,i),k.touch(r),s.send(JSON.stringify({type:"hello_ok",agentId:r})),i.sendSessionsReq()}),s.on("close",()=>{i&&(i.dispose(),_.delete(i.agentId),k.unregister(i.agentId),d("agent_disconnected",{ip:t,agentId:i.agentId})),clearTimeout(l)}),s.on("error",()=>{i&&(i.dispose(),_.delete(i.agentId),k.unregister(i.agentId)),clearTimeout(l)})})}const ut=parseInt(process.env.TMUX_WEB_AGENT_OFFLINE_MS||"45000",10);setInterval(()=>{const e=k.pruneOffline(ut);for(const o of e){const t=k.peek(o);t&&t.ws.readyState===E.CLOSED&&(k.unregister(o),_.get(o)?.dispose(),_.delete(o))}const n=new Set(ze().map(o=>o.tokenHash));for(const[o,t]of _){const f=k.peek(o);if(!(!f||n.has(f.tokenHash))){d("agent_token_revoked",{agentId:o}),t.dispose(),_.delete(o),k.unregister(o);try{f.ws.close(4003,"token revoked")}catch{}}}T.purgeExpired()},10*1e3).unref();function fe(){ce(),He(),Fe();for(const e of _.values())e.dispose();_.clear();for(const[e,n]of R){try{e.close(1001,"Server shutting down")}catch{}n.authTimeout&&clearTimeout(n.authTimeout)}R.clear();for(const e of re)try{e.kill()}catch{}re.clear(),A.dispose(),process.exit(0)}process.on("SIGINT",fe),process.on("SIGTERM",fe);function dt(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}async function ft(e){const{createAgentToken:n,listAgentTokens:o,removeAgentToken:t}=await import("./lib/agent-tokens.js"),[f,...s]=e;switch(f){case"add":{const i=s.find(p=>!p.startsWith("-"))||"agent",{id:l,token:u}=n(i);console.log("\u2713 agent token created"),console.log(` id: ${l}`),console.log(` name: ${i}`),console.log(` token: ${u}`),console.log(`
7
- Run on the remote machine:`),console.log(` tmux-web agent --hub wss://YOUR_HUB --token ${u} --name "${i}"`);return}case"list":{const i=o();if(!i.length){console.log("No agent tokens. Create one with: tmux-web agent-token add --name <name>");return}console.log("Agent tokens:");for(const l of i)console.log(` ${l.id} ${l.name} (created ${new Date(l.createdAt).toISOString()})`);return}case"remove":{const i=s.find(u=>!u.startsWith("-"));i||(console.error("usage: tmux-web agent-token remove <id>"),process.exit(1)),t(i)||(console.error(`no token with id ${i}`),process.exit(1)),console.log(`\u2713 token ${i} removed`);return}default:console.error("usage: tmux-web agent-token <add|list|remove>"),process.exit(1)}}
6
+ `})),a.close(1011,"agent offline"),p.relayClosed(T),k.bumpRelayConns(n,-1);return}a.on("message",c=>{const h=typeof c=="string"?c:c.toString("utf-8");let S;try{S=JSON.parse(h)}catch{return}(S.type==="input"||S.type==="resize"||S.type==="load_history")&&p.sendToAgent(T,S)});const r=()=>{p.relayClosed(T),k.bumpRelayConns(n,-1)};a.on("close",r),a.on("error",r)})}let lt=1;function ut(){return lt++}function dt(e,t,o){const n=j(e),f=e.headers.origin;if(f){d("agent_rejected_origin",{ip:n,origin:f}),I(t,403,"Origin not allowed");return}O.handleUpgrade(e,t,o,s=>{let i=null,l=setTimeout(()=>{d("agent_hello_timeout",{ip:n}),b(s,4e3,"hello timeout")},1e4);s.on("message",(u,p)=>{if(i){k.touch(i.agentId),p?i.handleData(Array.isArray(u)?Buffer.concat(u):Buffer.from(u)):i.handleMessage(typeof u=="string"?u:Buffer.from(u).toString("utf-8"));return}const m=typeof u=="string"?u:u.toString("utf-8");let y;try{y=JSON.parse(m)}catch{b(s,1008,"bad hello");return}if(y.type!=="hello"||typeof y.token!="string"){b(s,1008,"expected hello");return}if(!A.check(n).allowed){d("agent_rate_limited",{ip:n}),b(s,429,"Too many attempts");return}const w=ze(y.token);if(!w){A.recordFailure(n),d("agent_bad_token",{ip:n}),ne(s,{type:"hello_err",reason:"invalid token"}),b(s,4003,"invalid token");return}const a=typeof y.name=="string"&&y.name.trim()?y.name.trim().slice(0,64):"agent",r=w.id;if(!k.register(r,a,s,w.tokenHash)){d("agent_cap_reached",{ip:n}),ne(s,{type:"hello_err",reason:"too many agents"}),b(s,4004,"too many agents");return}clearTimeout(l),A.recordSuccess(n),d("agent_connected",{ip:n,agentId:r,name:a}),i=new Je(s,r,{onSessions:h=>k.setSessions(r,h),onActivity:h=>{for(const S of h)de({type:"session.activity",agentId:r,session:S.session,state:S.state})},onRebuildResult:h=>{console.log(`[agent ${r}] rebuild ${h.session}: ${h.ok?"ok":h.message}`)},onClose:h=>{_.get(r)===h&&_.delete(r)}}),_.set(r,i),k.touch(r),s.send(JSON.stringify({type:"hello_ok",agentId:r})),i.sendSessionsReq()}),s.on("close",()=>{i&&(i.dispose(),_.delete(i.agentId),k.unregister(i.agentId),d("agent_disconnected",{ip:n,agentId:i.agentId})),clearTimeout(l)}),s.on("error",()=>{i&&(i.dispose(),_.delete(i.agentId),k.unregister(i.agentId)),clearTimeout(l)})})}const ft=parseInt(process.env.TMUX_WEB_AGENT_OFFLINE_MS||"45000",10);setInterval(()=>{const e=k.pruneOffline(ft);for(const o of e){const n=k.peek(o);n&&n.ws.readyState===E.CLOSED&&(k.unregister(o),_.get(o)?.dispose(),_.delete(o))}const t=new Set(Ge().map(o=>o.tokenHash));for(const[o,n]of _){const f=k.peek(o);if(!(!f||t.has(f.tokenHash))){d("agent_token_revoked",{agentId:o}),n.dispose(),_.delete(o),k.unregister(o);try{f.ws.close(4003,"token revoked")}catch{}}}v.purgeExpired()},10*1e3).unref();function fe(){ce(),He(),je();for(const e of _.values())e.dispose();_.clear();for(const[e,t]of R){try{e.close(1001,"Server shutting down")}catch{}t.authTimeout&&clearTimeout(t.authTimeout)}R.clear();for(const e of re)try{e.kill()}catch{}re.clear(),A.dispose(),process.exit(0)}process.on("SIGINT",fe),process.on("SIGTERM",fe);function mt(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}async function pt(e){const{createAgentToken:t,listAgentTokens:o,removeAgentToken:n}=await import("./lib/agent-tokens.js"),[f,...s]=e;switch(f){case"add":{const i=s.find(p=>!p.startsWith("-"))||"agent",{id:l,token:u}=t(i);console.log("\u2713 agent token created"),console.log(` id: ${l}`),console.log(` name: ${i}`),console.log(` token: ${u}`),console.log(`
7
+ Run on the remote machine:`),console.log(` tmux-web agent --hub wss://YOUR_HUB --token ${u} --name "${i}"`);return}case"list":{const i=o();if(!i.length){console.log("No agent tokens. Create one with: tmux-web agent-token add --name <name>");return}console.log("Agent tokens:");for(const l of i)console.log(` ${l.id} ${l.name} (created ${new Date(l.createdAt).toISOString()})`);return}case"remove":{const i=s.find(u=>!u.startsWith("-"));i||(console.error("usage: tmux-web agent-token remove <id>"),process.exit(1)),n(i)||(console.error(`no token with id ${i}`),process.exit(1)),console.log(`\u2713 token ${i} removed`);return}default:console.error("usage: tmux-web agent-token <add|list|remove>"),process.exit(1)}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-weblink",
3
- "version": "2.2.3",
3
+ "version": "2.2.4",
4
4
  "description": "Access your tmux sessions from the browser. A continuation of tmux-web and persalink.",
5
5
  "type": "module",
6
6
  "bin": {