tmux-weblink 1.4.4 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +6 -6
- package/dist/lib/web-browse.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{chmodSync as Ge,existsSync as B,mkdirSync as ze,readdirSync as fe,readFileSync as me,statSync as D,unlinkSync as Je,writeFileSync as Ke}from"node:fs";import{readFile as Qe}from"node:fs/promises";import{execSync as V,execFileSync as pe}from"node:child_process";import{createRequire as Xe}from"node:module";import j from"node:path";import{fileURLToPath as Ye}from"node:url";import{Hono as Ze}from"hono";import{serve as et}from"@hono/node-server";import{WebSocketServer as tt,WebSocket as he}from"ws";import*as rt from"node-pty";try{const e=j.dirname(Xe(import.meta.url).resolve("node-pty/package.json")),t=j.join(e,"prebuilds");if(B(t))for(const r of fe(t)){const n=j.join(t,r,"spawn-helper");B(n)&&!(D(n).mode&73)&&Ge(n,493)}}catch{}import{listSessions as A}from"./sessions.js";import{renderLoginPage as nt,renderNotesIndex as ot,renderNotesPage as st,renderSettings as it,renderThemeSettings as at,renderScheduleIndex as dt,renderHistoryIndex as ut,renderQuickCommandsPage as ct,renderFilesIndex as lt,renderShell as ft}from"./frontend.js";import{loadSecurityConfig as mt,saveSecurityConfig as ye}from"./lib/security-config.js";import{hashPassword as we,verifyPassword as ge,validatePassword as je,TokenStore as pt}from"./lib/auth.js";import{RateLimiter as ht}from"./lib/rateLimiter.js";import{atomicWriteFileSync as yt}from"./lib/atomicWrite.js";import{resolveFsPath as H,resolveFsRoots as ke,MAX_FILE_BYTES as
|
|
2
|
+
import{chmodSync as Ge,existsSync as B,mkdirSync as ze,readdirSync as fe,readFileSync as me,statSync as D,unlinkSync as Je,writeFileSync as Ke}from"node:fs";import{readFile as Qe}from"node:fs/promises";import{execSync as V,execFileSync as pe}from"node:child_process";import{createRequire as Xe}from"node:module";import j from"node:path";import{fileURLToPath as Ye}from"node:url";import{Hono as Ze}from"hono";import{serve as et}from"@hono/node-server";import{WebSocketServer as tt,WebSocket as he}from"ws";import*as rt from"node-pty";try{const e=j.dirname(Xe(import.meta.url).resolve("node-pty/package.json")),t=j.join(e,"prebuilds");if(B(t))for(const r of fe(t)){const n=j.join(t,r,"spawn-helper");B(n)&&!(D(n).mode&73)&&Ge(n,493)}}catch{}import{listSessions as A}from"./sessions.js";import{renderLoginPage as nt,renderNotesIndex as ot,renderNotesPage as st,renderSettings as it,renderThemeSettings as at,renderScheduleIndex as dt,renderHistoryIndex as ut,renderQuickCommandsPage as ct,renderFilesIndex as lt,renderShell as ft}from"./frontend.js";import{loadSecurityConfig as mt,saveSecurityConfig as ye}from"./lib/security-config.js";import{hashPassword as we,verifyPassword as ge,validatePassword as je,TokenStore as pt}from"./lib/auth.js";import{RateLimiter as ht}from"./lib/rateLimiter.js";import{atomicWriteFileSync as yt}from"./lib/atomicWrite.js";import{resolveFsPath as H,resolveFsRoots as ke,MAX_FILE_BYTES as be,walkRecursive as wt}from"./lib/fs-access.js";import{captureSessionWindowsWithPath as gt}from"./lib/tmux-windows.js";import{audit as l}from"./lib/auditLog.js";import{db as S}from"./lib/db.js";import{getSessionAccessMap as E}from"./lib/session-access.js";import{listWindowHistory as jt,clearWindowHistory as kt}from"./lib/window-history.js";import{loadExtensions as bt,spawnExtensionBackend as vt,registerExtensionRoutes as St}from"./lib/ext-loader.js";import{SchedulerService as xt,isValidScheduleInput as Tt,isValidRescheduleInput as qt}from"./lib/scheduler.js";import{getScheduleDelayError as ve}from"./lib/schedule-delay.js";import{handleClientMessage as _t}from"./lib/ws-message.js";import{loadDotEnv as At}from"./lib/load-env.js";import{cmdAdd as Ct,cmdRemove as It,cmdList as Et,cmdSetup as Rt,cmdTheme as Ot,printUsage as Se,printVersion as Mt}from"./lib/cli.js";import{readSettings as te,writeSettings as Lt}from"./lib/settings.js";import{readActiveTheme as Pt,setActiveThemeTemplate as xe}from"./lib/theme-store.js";import{isThemeTemplateId as Te,THEME_TEMPLATE_IDS as Nt}from"./lib/themes/index.js";import{installPlugin as Dt,uninstallPlugin as Ht}from"./lib/plugins.js";import{buildCommandbarSessions as O}from"./lib/commandbar.js";import{pinView as Ft,unpinView as Ut,listPinnedViews as Wt}from"./lib/pinned-views.js";import{listWindowLabels as $t,setWindowLabel as Bt}from"./lib/window-labels.js";import{captureAndStoreWindows as qe,getStoredWindows as _e}from"./lib/session-windows.js";import{acquireControlClient as Vt,killAllControlClients as Gt}from"./lib/tmux-control.js";import{buildSidebarSessions as zt}from"./lib/sessions-sidebar.js";import{createQuickCommand as Jt,deleteQuickCommand as Kt,listQuickCommands as Ae,updateQuickCommand as Qt}from"./lib/quick-commands.js";import{getSessionPaneTarget as Xt,capturePaneTail as Yt,capturePaneHistoryChunk as Zt,toCrlf as Ce}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as er}from"./lib/terminal-config.js";import{ImageUploadError as tr,saveUploadedImage as rr}from"./lib/image-upload.js";import{browsePage as nr,browseAsset as or,browseSubmit as sr,BrowseError as re}from"./lib/web-browse.js";import{getSystemStatus as ir,getTopProcesses as ar,killProcess as dr}from"./lib/system-monitor.js";import{listSessionWindows as ur,selectSessionWindow as cr,newSessionWindow as lr,renameSessionWindow as fr,newTmuxSession as mr,renameSession as pr,killSession as hr,TmuxWindowsError as K}from"./lib/tmux-windows.js";At();const Q=er(),f=mt(),w=new pt,M=new ht;let F=!1;const X="tmux-web-token",yr=365;function Ie(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}function Y(e){if(f.security.trustProxy){const t=(e.req.header("x-forwarded-for")||"").split(",")[0].trim();if(t)return t}return e.env?.incoming?.socket?.remoteAddress||"unknown"}function wr(e){if(f.security.trustProxy){const t=(e.headers["x-forwarded-for"]||"").split(",")[0].trim();if(t)return t}return e.socket.remoteAddress||"unknown"}function ne(e){const t=e.req.header("authorization");if(t?.startsWith("Bearer "))return t.slice(7);const r=e.req.header("cookie");if(!r)return null;const n=r.match(new RegExp(`(?:^|;\\s*)${X}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function Ee(e){const t=e.headers.authorization;if(typeof t=="string"&&t.startsWith("Bearer "))return t.slice(7);const r=e.headers.cookie;if(typeof r!="string")return null;const n=r.match(new RegExp(`(?:^|;\\s*)${X}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function Sn(e){const t=ne(e);return t?w.validateToken(t):null}function xn(e){const t=Ee(e);return t?w.validateToken(t):null}function Re(e,t){const r=e.req.header("x-forwarded-proto")==="https"||e.req.url.startsWith("https:");e.header("Set-Cookie",`${X}=${encodeURIComponent(t)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${yr*86400}${r?"; Secure":""}`)}function gr(e){e.header("Set-Cookie",`${X}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}function d(){return async(e,t)=>{const r=ne(e);return!r||!w.validateToken(r)?(l("http_unauthorized",{ip:Y(e)}),e.json({error:"unauthorized"},401)):t()}}function jr(e){const t=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${t}`,302)}function R(){return async(e,t)=>{const r=ne(e);return!r||!w.validateToken(r)?jr(e):t()}}const U=new Map;function kr(e){let t=0;for(const r of U.values())r.ip===e&&t++;return t}function oe(e,t,r){try{e.close(t,r)}catch{}}function Tn(e,t){e.readyState===he.OPEN&&e.send(JSON.stringify(t))}function br(e,t){let r="xterm";(t==="ghostty"||t==="xterm")&&(r=t);const n=process.env.TMUX_WEB_TERMINAL_RENDERER?.trim().toLowerCase();(n==="ghostty"||n==="xterm")&&(r=n);for(const i of e)i==="--ghostty"&&(r="ghostty"),i==="--xterm"&&(r="xterm");return r}const vr=7;function Oe(e){return typeof e!="number"||!Number.isFinite(e)?vr:Math.min(365,Math.max(1,Math.round(e)))}const Me=process.argv.slice(2);{const e=Me.filter(t=>t!=="--ghostty"&&t!=="--xterm");if(e.length>0){const[t,r]=e;switch(t){case"add":r||(console.error("usage: tmux-web add <package>"),process.exit(1)),await Ct(r),process.exit(0);case"remove":case"rm":r||(console.error("usage: tmux-web remove <package>"),process.exit(1)),await It(r),process.exit(0);case"list":case"ls":await Et(),process.exit(0);case"setup":await Rt(e),process.exit(0);case"theme":await Ot(e.slice(1)),process.exit(0);case"help":case"--help":case"-h":Se(),process.exit(0);case"-V":case"--version":case"-v":Mt(),process.exit(0);default:console.error(`unknown argument: ${t}`),Se(),process.exit(1)}}}function k(e,t){e.readyState===he.OPEN&&e.send(JSON.stringify(t))}const W=new Set,Le=[];await S.read(),S.data.sessionAccess??=[],S.data.pinnedViews??=[],S.data.watchedPanes??=[],S.data.triggeredTasks??=[],S.data.quickCommands??=[];const se=await te();let x=await Pt();const b=se.commandbar===!0,ie=br(Me,se.terminalRenderer),Pe=Oe(se.scheduleHistoryDays),Ne=j.join(process.cwd(),"extensions"),De=await bt(Ne);for(const e of De)e.start&&Le.push(vt(e.dir,e));const L=new xt({db:S,historyRetentionMs:Pe*864e5,onMissedTask:e=>console.warn(`[scheduler] dropped missed task ${e.id} (was due ${new Date(e.fireAt).toISOString()})`)});await L.restoreFromDb();const s=new Ze;s.use("*",async(e,t)=>{await t(),e.header("X-Content-Type-Options","nosniff"),e.header("X-Frame-Options","DENY"),e.header("Content-Security-Policy",["default-src 'self'","script-src 'self' 'unsafe-inline'","style-src 'self' 'unsafe-inline'","connect-src 'self' ws: wss: http: https:","img-src 'self' data:","font-src 'self'","worker-src 'self' blob:"].join("; ")),e.header("Referrer-Policy","strict-origin-when-cross-origin")}),s.use("*",async(e,t)=>{const r=e.req.method;if(r==="GET"||r==="HEAD"||r==="OPTIONS")return t();const n=e.req.header("sec-fetch-site");return n&&n!=="same-origin"&&n!=="none"?e.text("cross-site request blocked",403):t()}),St(s,Ne,De);const Sr=j.dirname(Ye(import.meta.url)),xr=[j.join(Sr,"assets"),j.join(process.cwd(),"dist","assets")];s.get("/assets/:file",async e=>{const t=e.req.param("file");if(!/^[a-zA-Z0-9._-]+$/.test(t))return e.notFound();for(const r of xr){const n=j.join(r,t);if(!B(n))continue;const i=await Qe(n),o=j.extname(t),a={".css":"text/css; charset=utf-8",".js":"application/javascript; charset=utf-8",".map":"application/json; charset=utf-8"};return e.body(i,200,{"Content-Type":a[o]??"application/octet-stream","Cache-Control":o===".js"||o===".css"?"no-cache":"public, max-age=3600"})}return e.notFound()});const Tr=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
3
3
|
<rect x="2.5" y="5.5" width="27" height="21" rx="4" fill="#0d1117" stroke="#7dd3fc" stroke-width="2"/>
|
|
4
4
|
<path d="M8 13l4.2 3.1L8 19.2" fill="none" stroke="#7dd3fc" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
5
|
<line x1="15.5" y1="19.6" x2="22.5" y2="19.6" stroke="#7dd3fc" stroke-width="2.2" stroke-linecap="round"/>
|
|
@@ -14,14 +14,14 @@ self.addEventListener("activate", (e) => { e.waitUntil(clients.claim()); });
|
|
|
14
14
|
self.addEventListener("fetch", (e) => {
|
|
15
15
|
e.respondWith(fetch(e.request).catch(() => caches.match(e.request)));
|
|
16
16
|
});
|
|
17
|
-
`;s.get("/sw.js",e=>e.body(_r,200,{"Content-Type":"application/javascript; charset=utf-8"})),s.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!f.passwordHash,n=e.req.query("error");return e.html(nt({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:x}))}),s.get("/",R(),e=>{const t=
|
|
17
|
+
`;s.get("/sw.js",e=>e.body(_r,200,{"Content-Type":"application/javascript; charset=utf-8"})),s.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!f.passwordHash,n=e.req.query("error");return e.html(nt({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:x}))}),s.get("/",R(),e=>{const t=b?O(A(),E()):[],r=ke();return e.html(ft({theme:x,commandbarEnabled:b,commandbarSessions:t,fsRoots:r,terminalCfg:Q,renderer:ie,scrollback:Q.initialLines+2*Q.historyChunk}))}),s.get("/notes",R(),e=>{const t=b?O(A(),E()):[];return e.html(ot(S.data.notes,x,b,t))}),s.get("/notes/:session",R(),e=>{const t=decodeURIComponent(e.req.param("session")),r=b?O(A(),E()):[];return e.html(st(t,x,b,r))}),s.get("/schedule",R(),e=>{const t=b?O(A(),E()):[];return e.html(dt(L.list(),L.listTriggered(),x,Pe,b,t))}),s.get("/history",R(),e=>{const t=A(),r=b?O(t,E()):[],n=new Set(t.map(i=>i.name));return e.html(ut(jt(),x,b,r,n))}),s.get("/quick-commands",R(),e=>{const t=b?O(A(),E()):[];return e.html(ct(Ae(),x,b,t))}),s.get("/files",R(),e=>{const t=b?O(A(),E()):[],r=ke();return e.html(lt(x,b,t,r))}),s.post("/api/history/clear",d(),async e=>(await kt(),e.json({ok:!0}))),s.get("/api/quick-commands",d(),e=>e.json(Ae())),s.post("/api/quick-commands",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Jt(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),s.patch("/api/quick-commands/:id",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Qt(e.req.param("id"),t);return"error"in r?e.json({error:r.error},r.status):e.json(r)}),s.delete("/api/quick-commands/:id",d(),async e=>await Kt(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.post("/api/auth/password",async e=>{const t=Y(e),r=M.check(t);if(!r.allowed)return l("rate_limited",{ip:t,retryAfterMs:r.retryAfterMs,permanentLock:r.permanentLock}),r.permanentLock?e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403):e.json({error:"Too many attempts",retryAfterMs:r.retryAfterMs},429);let n;try{n=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const i=typeof n.password=="string"?n.password:"";if(!f.passwordHash){if(!f.security.allowRemoteSetup&&!Ie(t))return l("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(F)return e.json({error:"Password setup in progress"},409);const m=je(i);if(m)return e.json({error:m},400);F=!0;try{f.passwordHash=await we(i),ye(f),l("password_set",{ip:t})}finally{F=!1}const{plaintext:u}=w.createAccessToken("setup",f.security.tokenTtlDays);return Re(e,u),e.json({ok:!0,token:u,setupMode:!0})}if(!await ge(i,f.passwordHash)){const m=M.recordFailure(t);return l("auth_failed",{ip:t,method:"password",failures:m.failures,permanentLock:m.permanentLock}),m.permanentLock?(w.revokeAll(),l("permanent_lock",{ip:t,failures:m.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):m.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:m.retryAfterMs},429)}M.recordSuccess(t);const a=`browser-${t}`,{plaintext:g}=w.createAccessToken(a,f.security.tokenTtlDays);return Re(e,g),l("auth_success",{ip:t,method:"password",tokenName:a}),e.json({ok:!0,token:g})}),s.post("/api/auth/token",d(),async e=>{const t=Y(e);let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const n=typeof r.name=="string"?r.name:`api-${t}`,i=typeof r.ttlDays=="number"?r.ttlDays:f.security.tokenTtlDays,{stored:o,plaintext:a}=w.createAccessToken(n,i);return l("token_created",{ip:t,name:o.name,tokenId:o.id}),e.json({id:o.id,name:o.name,token:a,expiresAt:o.expiresAt})}),s.get("/api/auth/tokens",d(),e=>e.json(w.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),s.delete("/api/auth/tokens/:id",d(),e=>w.revoke(e.req.param("id"))?(l("token_revoked",{ip:Y(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),s.post("/api/auth/logout",d(),e=>(gr(e),e.json({ok:!0}))),s.get("/settings",R(),async e=>{const t=await te(),r=t.terminalRenderer??"xterm";return e.html(it({settings:t,renderer:ie,rendererOverridden:ie!==r,theme:x,plugins:t.plugins??[],saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),s.post("/settings",d(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await te(),n=t.terminalRenderer==="ghostty"?"ghostty":"xterm",i=t.defaultView==="recent"?"recent":"default",o=Oe(typeof t.scheduleHistoryDays=="string"?Number(t.scheduleHistoryDays):void 0);return await Lt({...r,commandbar:t.commandbar!==void 0,terminalRenderer:n,defaultView:i,scheduleHistoryDays:o}),e.redirect("/settings?saved=1",303)}),s.post("/settings/plugins",d(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=t.action,n=typeof t.pkg=="string"?t.pkg.trim():"";if(!n)return e.redirect("/settings?error="+encodeURIComponent("missing package name"),303);const i=r==="remove"?await Ht(n):r==="add"?await Dt(n):{ok:!1,output:"unknown action"};return i.ok?e.redirect("/settings?saved=1",303):e.redirect("/settings?error="+encodeURIComponent(i.output.slice(0,800)),303)}),s.get("/settings/theme",R(),e=>e.html(at({theme:x,saved:e.req.query("saved")==="1"}))),s.post("/settings/theme",d(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings/theme?error=1",303)}const r=t.template;return typeof r!="string"||!Te(r)?e.redirect("/settings/theme",303):(x=await xe(r),e.redirect("/settings/theme?saved=1",303))});const Ar={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};s.get("/api/theme",d(),e=>e.json({active:x.template,templates:Nt.map(t=>({id:t,name:Ar[t]}))})),s.post("/api/theme",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=t.template;return typeof r!="string"||!Te(r)?e.json({error:"invalid theme template"},400):(x=await xe(r),e.json({ok:!0,active:x.template}))}),s.get("/api/system/status",d(),e=>e.json(ir())),s.get("/api/system/processes",d(),e=>e.json(ar())),s.post("/api/system/kill",d(),async e=>{const{pid:t}=await e.req.json();if(typeof t!="number"||!Number.isInteger(t)||t<=0)return e.json({error:"invalid pid"},400);const r=dr(t);return r.ok?e.json({ok:!0}):e.json({error:r.error},500)}),s.get("/api/sessions",d(),e=>b?e.json(O(A(),E())):e.json({error:"commandbar disabled"},404)),s.post("/api/sessions/new",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name is required"},400);if(!/^[a-zA-Z0-9_\-. ]+$/.test(r))return e.json({error:"name contains invalid characters"},400);const n=typeof t.dir=="string"&&t.dir.trim()?t.dir.trim():void 0;if(A().some(o=>o.name===r))return e.json({error:"session already exists"},409);try{return mr(r,n),e.json({ok:!0})}catch(o){const a=o instanceof K?o.message:"failed to create session";return e.json({error:a},500)}}),s.post("/api/sessions/rename",d(),async e=>{try{const{oldName:t,newName:r}=await e.req.json();return!t||!r?e.json({error:"oldName and newName required"},400):/^[a-zA-Z0-9_\-. ]+$/.test(r)?(pr(t,r),e.json({ok:!0})):e.json({error:"invalid characters in name"},400)}catch{return e.json({error:"rename failed"},500)}}),s.post("/api/sessions/kill",d(),async e=>{try{const{name:t}=await e.req.json();return t?(hr(t),e.json({ok:!0})):e.json({error:"name required"},400)}catch{return e.json({error:"kill failed"},500)}}),s.get("/api/fs/session-path",d(),e=>{const t=e.req.query("session");if(!t)return e.json({error:"session is required"},400);try{const r=gt(t),i=r.find(o=>o.active)?.path??r[0]?.path??process.env.HOME??"/";return e.json({path:i})}catch{return e.json({path:process.env.HOME??"/"})}}),s.get("/api/fs/list",d(),e=>{const t=process.env.HOME??"/";let r=e.req.query("path")??t;r.startsWith("~")&&(r=t+r.slice(1)),r.startsWith("/")||(r=j.join(t,r));const n=e.req.query("recursive")==="true";let i=r,o="",a=r.endsWith("/");if(!a)try{a=D(r).isDirectory()}catch{}a||(i=j.dirname(r),o=j.basename(r).toLowerCase());try{const g=fe(i),m=[],u=[];for(const y of g)if(!y.startsWith(".")&&!(o&&!y.toLowerCase().startsWith(o))){try{const p=j.join(i,y);D(p).isDirectory()?(m.push(p),n&&wt(p,m,u,0)):u.push(p)}catch{}if(m.length+u.length>=5e3)break}return e.json({dirs:m,files:u})}catch{return e.json({dirs:[],files:[]})}});function Cr(e){const t={repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0};try{const r=V("git rev-parse --show-toplevel 2>/dev/null",{cwd:e,encoding:"utf-8",timeout:3e3}).trim();if(!r)return t;let n;try{n=V("git branch --show-current 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}catch{n=V("git rev-parse --short HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}n||(n="HEAD");let i="";try{i=V("git status --porcelain 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3})}catch{}const o=[];for(const u of i.split(`
|
|
18
18
|
`)){if(!u.trim())continue;const y=u[0],p=u[1],q=u.substring(3).trim(),C=p!==" "?p:y;o.push({path:q,status:C,staged:y,unstaged:p,additions:0,deletions:0})}const a=new Map;try{const u=V("git diff --numstat HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3});for(const y of u.split(`
|
|
19
19
|
`)){if(!y.trim())continue;const p=y.split(" ");if(p.length>=3){const q=parseInt(p[0],10)||0,C=parseInt(p[1],10)||0;a.set(p[2],{added:q,deleted:C})}}}catch{}for(const u of o){const y=a.get(u.path);y&&(u.additions=y.added,u.deletions=y.deleted)}let g=0,m=0;for(const u of o)g+=u.additions,m+=u.deletions;for(const u of o)if(u.status==="?"&&u.additions===0)try{const y=j.join(r,u.path),q=me(y,"utf-8").split(`
|
|
20
|
-
`).length;u.additions=q,g+=q}catch{}return{repoRoot:r,branch:n,files:o,linesAdded:g,linesRemoved:m}}catch{return t}}s.get("/api/git/status",d(),e=>{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);try{const r=H(t),n=Cr(r);return e.json(n)}catch(r){return r.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):r.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0})}}),s.get("/api/git/diff",d(),e=>{const t=e.req.query("path"),r=e.req.query("file");if(!t||!r)return e.json({error:"path and file are required"},400);try{const n=H(t);if(console.error("[git/diff] path=%s resolved=%s file=%s",t,n,r),!B(n)||!D(n).isDirectory())return console.error("[git/diff] resolved path does not exist or is not a directory: %s",n),e.json({error:"invalid repository path: "+n},400);try{const i=pe("/usr/bin/git",["diff","HEAD","--",r],{cwd:n,encoding:"utf-8",timeout:5e3}),o=pe("/usr/bin/git",["diff","--cached","--",r],{cwd:n,encoding:"utf-8",timeout:5e3});return e.json({diff:i,stagedDiff:o})}catch(i){throw console.error("[git/diff] execFileSync failed:",i?.code,i.message),i}}catch(n){return n.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):n.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):(console.error("[git/diff] exception:",n),e.json({error:String(n)},500))}}),s.get("/api/file",d(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=H(t);if(!D(r).isFile())return e.json({error:"not a file"},400);const n=D(r).size;if(n>ve)return e.json({error:"file too large",size:n,maxBytes:ve},413);const i=me(r,"utf-8");return e.json({path:r,content:i,size:n})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"internal error"},500)}}),s.put("/api/file",d(),async e=>{try{const t=await e.req.json();if(!t.path||typeof t.content!="string")return e.json({error:"path and content are required"},400);const r=H(t.path);return yt(r,t.content),e.json({ok:!0})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"write failed"},500)}}),s.post("/api/file/delete",d(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=H(t.path);return D(r).isFile()?(Je(r),e.json({ok:!0})):e.json({error:"not a file"},400)}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"delete failed"},500)}}),s.post("/api/file/touch",d(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=H(t.path);return B(r)?e.json({error:"file already exists"},409):(ze(j.dirname(r),{recursive:!0}),Ke(r,"","utf-8"),e.json({ok:!0,path:r}))}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"touch failed"},500)}});function ae(e){return typeof e=="string"&&e.length<=64?e:"default"}s.get("/api/browse",d(),async e=>{const t=e.req.query("url");if(!t)return e.json({error:"url is required"},400);const r=ae(e.req.query("tab"));try{const n=await nr(t,r);return e.json(n)}catch(n){return n instanceof re?e.json({error:n.message},n.status):(console.error("[browse]",n),e.json({error:"browse failed"},500))}}),s.get("/api/browse/asset",d(),async e=>{const t=e.req.query("url");if(!t)return e.json({error:"url is required"},400);const r=ae(e.req.query("tab"));try{const{buf:n,contentType:i}=await or(t,r);return e.body(n,200,{"Content-Type":i,"Cache-Control":"no-cache"})}catch(n){return n instanceof re?e.json({error:n.message},n.status):(console.error("[browse/asset]",n),e.json({error:"asset fetch failed"},500))}}),s.post("/api/browse/submit",d(),async e=>{const t=ae(e.req.query("tab"));let r;try{r=await e.req.parseBody()}catch{return e.json({error:"invalid form body"},400)}const n=typeof r.__browse_action=="string"?r.__browse_action:"",i=typeof r.__browse_method=="string"&&r.__browse_method.toLowerCase()==="get"?"get":"post";if(!n)return e.json({error:"missing form action"},400);const o=[];for(const[a,g]of Object.entries(r))a==="__browse_action"||a==="__browse_method"||o.push([a,String(g)]);try{const a=await sr(n,o,i,t);return e.json(a)}catch(a){return a instanceof re?e.json({error:a.message},a.status):(console.error("[browse/submit]",a),e.json({error:"form submit failed"},500))}});function de(e){return{...zt(A(),E(),Wt()),currentSession:e??null}}function Fe(e){const t=typeof e.sessionName=="string"?e.sessionName.trim():"";if(!t)return{error:"sessionName is required"};if(e.windowIndex===void 0)return{sessionName:t};const r=e.windowIndex;return typeof r!="number"||!Number.isInteger(r)||r<0?{error:"windowIndex must be a non-negative integer"}:{sessionName:t,windowIndex:r}}s.get("/api/sidebar/sessions",d(),e=>{const t=e.req.query("currentSession");return e.json(de(typeof t=="string"&&t?t:void 0))}),s.get("/api/sidebar/session-windows/:session",d(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(_e(t))}),s.post("/api/pinned-views",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Fe(t);return"error"in r?e.json({error:r.error},400):(await Ft(r.sessionName,r.windowIndex),e.json(de()))}),s.delete("/api/pinned-views",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Fe(t);return"error"in r?e.json({error:r.error},400):(await Ut(r.sessionName,r.windowIndex),e.json(de()))}),s.get("/api/notes",d(),e=>{const t=[...S.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),s.get("/api/notes/:scope",d(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=S.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),s.put("/api/notes/:scope",d(),async e=>{const t=decodeURIComponent(e.req.param("scope"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}if(typeof r.content!="string")return e.json({error:"content must be string"},400);const n={scope:t,content:r.content,updatedAt:Date.now()},i=S.data.notes.findIndex(o=>o.scope===t);return i>=0?S.data.notes[i]=n:S.data.notes.push(n),await S.write(),e.json({ok:!0})}),s.post("/api/session/:session/upload",d(),async e=>{const t=decodeURIComponent(e.req.param("session"));if(!A().some(o=>o.name===t))return e.json({error:"session not found"},404);let n;try{n=await e.req.parseBody()}catch{return e.json({error:"invalid multipart body"},400)}const i=n.file;if(!(i instanceof File))return e.json({error:"missing file field"},400);try{const o=await i.arrayBuffer(),a=Buffer.from(o),{path:g}=await rr(a,i.type||void 0,i.name||void 0);return e.json({path:g})}catch(o){return o instanceof tr?e.json({error:o.message},o.status):(console.error("[upload]",o),e.json({error:"upload failed"},500))}}),s.get("/api/session/:session/windows",d(),e=>{const t=decodeURIComponent(e.req.param("session")),r=new Map($t(t).map(o=>[o.windowIndex,o.label])),n=new Map(_e(t).map(o=>[o.index,o])),i=ur(t).map(o=>({...o,label:r.get(o.index)??null,worktree:n.get(o.index)?.worktree??!1}));return e.json(i)}),s.post("/api/session/:session/window-label",d(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);const i=typeof r.label=="string"?r.label:"",o=await Bt(t,n,i);return e.json(o)}),s.post("/api/session/:session/select-window",d(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);try{return cr(t,n),e.json({ok:!0})}catch(i){return i instanceof K?e.json({error:i.message},i.status):(console.error("[select-window]",i),e.json({error:"select-window failed"},500))}}),s.post("/api/session/:session/rename-window",d(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);if(typeof r.name!="string"||!r.name.trim())return e.json({error:"name is required"},400);try{return fr(t,n,r.name),qe(t),e.json({ok:!0})}catch(i){return i instanceof K?e.json({error:i.message},i.status):(console.error("[rename-window]",i),e.json({error:"rename-window failed"},500))}}),s.post("/api/session/:session/new-window",d(),e=>{const t=decodeURIComponent(e.req.param("session"));try{return lr(t),qe(t),e.json({ok:!0})}catch(r){return r instanceof K?e.json({error:r.message},r.status):(console.error("[new-window]",r),e.json({error:"new-window failed"},500))}}),s.get("/api/schedule",d(),e=>e.json(L.list(e.req.query("session")))),s.post("/api/schedule",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=be(t);if(r)return e.json({error:r},400);if(!Tt(t))return e.json({error:"invalid body"},400);const n=await L.create(t);return e.json({id:n.id,fireAt:n.fireAt})}),s.delete("/api/schedule/:id",d(),async e=>await L.delete(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.patch("/api/schedule/:id",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=be(t);if(r)return e.json({error:r},400);if(!qt(t))return e.json({error:"invalid body"},400);const n=await L.reschedule(e.req.param("id"),t.delayMs);return n?e.json({id:n.id,fireAt:n.fireAt}):e.json({error:"not found"},404)});const Ir=parseInt(process.env.PORT||"21000",10),Er=et({fetch:s.fetch,port:Ir,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),ue=new tt({noServer:!0});function Ue(e,t,r){e.write(`HTTP/1.1 ${t} ${r}\r
|
|
20
|
+
`).length;u.additions=q,g+=q}catch{}return{repoRoot:r,branch:n,files:o,linesAdded:g,linesRemoved:m}}catch{return t}}s.get("/api/git/status",d(),e=>{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);try{const r=H(t),n=Cr(r);return e.json(n)}catch(r){return r.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):r.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0})}}),s.get("/api/git/diff",d(),e=>{const t=e.req.query("path"),r=e.req.query("file");if(!t||!r)return e.json({error:"path and file are required"},400);try{const n=H(t);if(console.error("[git/diff] path=%s resolved=%s file=%s",t,n,r),!B(n)||!D(n).isDirectory())return console.error("[git/diff] resolved path does not exist or is not a directory: %s",n),e.json({error:"invalid repository path: "+n},400);try{const i=pe("/usr/bin/git",["diff","HEAD","--",r],{cwd:n,encoding:"utf-8",timeout:5e3}),o=pe("/usr/bin/git",["diff","--cached","--",r],{cwd:n,encoding:"utf-8",timeout:5e3});return e.json({diff:i,stagedDiff:o})}catch(i){throw console.error("[git/diff] execFileSync failed:",i?.code,i.message),i}}catch(n){return n.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):n.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):(console.error("[git/diff] exception:",n),e.json({error:String(n)},500))}}),s.get("/api/file",d(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=H(t);if(!D(r).isFile())return e.json({error:"not a file"},400);const n=D(r).size;if(n>be)return e.json({error:"file too large",size:n,maxBytes:be},413);const i=me(r,"utf-8");return e.json({path:r,content:i,size:n})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"internal error"},500)}}),s.put("/api/file",d(),async e=>{try{const t=await e.req.json();if(!t.path||typeof t.content!="string")return e.json({error:"path and content are required"},400);const r=H(t.path);return yt(r,t.content),e.json({ok:!0})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"write failed"},500)}}),s.post("/api/file/delete",d(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=H(t.path);return D(r).isFile()?(Je(r),e.json({ok:!0})):e.json({error:"not a file"},400)}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"delete failed"},500)}}),s.post("/api/file/touch",d(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=H(t.path);return B(r)?e.json({error:"file already exists"},409):(ze(j.dirname(r),{recursive:!0}),Ke(r,"","utf-8"),e.json({ok:!0,path:r}))}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"touch failed"},500)}});function ae(e){return typeof e=="string"&&e.length<=64?e:"default"}s.get("/api/browse",d(),async e=>{const t=e.req.query("url");if(!t)return e.json({error:"url is required"},400);const r=ae(e.req.query("tab"));try{const n=await nr(t,r);return e.json(n)}catch(n){return n instanceof re?(console.error(`[browse] ${n.status} url=${t} -> ${n.message}`),e.json({error:n.message},n.status)):(console.error("[browse]",n),e.json({error:"browse failed"},500))}}),s.get("/api/browse/asset",d(),async e=>{const t=e.req.query("url");if(!t)return e.json({error:"url is required"},400);const r=ae(e.req.query("tab"));try{const{buf:n,contentType:i}=await or(t,r);return e.body(n,200,{"Content-Type":i,"Cache-Control":"no-cache"})}catch(n){return n instanceof re?(console.error(`[browse/asset] ${n.status} url=${t} -> ${n.message}`),e.json({error:n.message},n.status)):(console.error("[browse/asset]",n),e.json({error:"asset fetch failed"},500))}}),s.post("/api/browse/submit",d(),async e=>{const t=ae(e.req.query("tab"));let r;try{r=await e.req.parseBody()}catch{return e.json({error:"invalid form body"},400)}const n=typeof r.__browse_action=="string"?r.__browse_action:"",i=typeof r.__browse_method=="string"&&r.__browse_method.toLowerCase()==="get"?"get":"post";if(!n)return e.json({error:"missing form action"},400);const o=[];for(const[a,g]of Object.entries(r))a==="__browse_action"||a==="__browse_method"||o.push([a,String(g)]);try{const a=await sr(n,o,i,t);return e.json(a)}catch(a){return a instanceof re?(console.error(`[browse/submit] ${a.status} url=${n} -> ${a.message}`),e.json({error:a.message},a.status)):(console.error("[browse/submit]",a),e.json({error:"form submit failed"},500))}});function de(e){return{...zt(A(),E(),Wt()),currentSession:e??null}}function Fe(e){const t=typeof e.sessionName=="string"?e.sessionName.trim():"";if(!t)return{error:"sessionName is required"};if(e.windowIndex===void 0)return{sessionName:t};const r=e.windowIndex;return typeof r!="number"||!Number.isInteger(r)||r<0?{error:"windowIndex must be a non-negative integer"}:{sessionName:t,windowIndex:r}}s.get("/api/sidebar/sessions",d(),e=>{const t=e.req.query("currentSession");return e.json(de(typeof t=="string"&&t?t:void 0))}),s.get("/api/sidebar/session-windows/:session",d(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(_e(t))}),s.post("/api/pinned-views",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Fe(t);return"error"in r?e.json({error:r.error},400):(await Ft(r.sessionName,r.windowIndex),e.json(de()))}),s.delete("/api/pinned-views",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Fe(t);return"error"in r?e.json({error:r.error},400):(await Ut(r.sessionName,r.windowIndex),e.json(de()))}),s.get("/api/notes",d(),e=>{const t=[...S.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),s.get("/api/notes/:scope",d(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=S.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),s.put("/api/notes/:scope",d(),async e=>{const t=decodeURIComponent(e.req.param("scope"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}if(typeof r.content!="string")return e.json({error:"content must be string"},400);const n={scope:t,content:r.content,updatedAt:Date.now()},i=S.data.notes.findIndex(o=>o.scope===t);return i>=0?S.data.notes[i]=n:S.data.notes.push(n),await S.write(),e.json({ok:!0})}),s.post("/api/session/:session/upload",d(),async e=>{const t=decodeURIComponent(e.req.param("session"));if(!A().some(o=>o.name===t))return e.json({error:"session not found"},404);let n;try{n=await e.req.parseBody()}catch{return e.json({error:"invalid multipart body"},400)}const i=n.file;if(!(i instanceof File))return e.json({error:"missing file field"},400);try{const o=await i.arrayBuffer(),a=Buffer.from(o),{path:g}=await rr(a,i.type||void 0,i.name||void 0);return e.json({path:g})}catch(o){return o instanceof tr?e.json({error:o.message},o.status):(console.error("[upload]",o),e.json({error:"upload failed"},500))}}),s.get("/api/session/:session/windows",d(),e=>{const t=decodeURIComponent(e.req.param("session")),r=new Map($t(t).map(o=>[o.windowIndex,o.label])),n=new Map(_e(t).map(o=>[o.index,o])),i=ur(t).map(o=>({...o,label:r.get(o.index)??null,worktree:n.get(o.index)?.worktree??!1}));return e.json(i)}),s.post("/api/session/:session/window-label",d(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);const i=typeof r.label=="string"?r.label:"",o=await Bt(t,n,i);return e.json(o)}),s.post("/api/session/:session/select-window",d(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);try{return cr(t,n),e.json({ok:!0})}catch(i){return i instanceof K?e.json({error:i.message},i.status):(console.error("[select-window]",i),e.json({error:"select-window failed"},500))}}),s.post("/api/session/:session/rename-window",d(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);if(typeof r.name!="string"||!r.name.trim())return e.json({error:"name is required"},400);try{return fr(t,n,r.name),qe(t),e.json({ok:!0})}catch(i){return i instanceof K?e.json({error:i.message},i.status):(console.error("[rename-window]",i),e.json({error:"rename-window failed"},500))}}),s.post("/api/session/:session/new-window",d(),e=>{const t=decodeURIComponent(e.req.param("session"));try{return lr(t),qe(t),e.json({ok:!0})}catch(r){return r instanceof K?e.json({error:r.message},r.status):(console.error("[new-window]",r),e.json({error:"new-window failed"},500))}}),s.get("/api/schedule",d(),e=>e.json(L.list(e.req.query("session")))),s.post("/api/schedule",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ve(t);if(r)return e.json({error:r},400);if(!Tt(t))return e.json({error:"invalid body"},400);const n=await L.create(t);return e.json({id:n.id,fireAt:n.fireAt})}),s.delete("/api/schedule/:id",d(),async e=>await L.delete(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.patch("/api/schedule/:id",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ve(t);if(r)return e.json({error:r},400);if(!qt(t))return e.json({error:"invalid body"},400);const n=await L.reschedule(e.req.param("id"),t.delayMs);return n?e.json({id:n.id,fireAt:n.fireAt}):e.json({error:"not found"},404)});const Ir=parseInt(process.env.PORT||"21000",10),Er=et({fetch:s.fetch,port:Ir,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),ue=new tt({noServer:!0});function Ue(e,t,r){e.write(`HTTP/1.1 ${t} ${r}\r
|
|
21
21
|
\r
|
|
22
|
-
`),e.destroy()}Er.on("upgrade",(e,t,r)=>{const n=new URL(e.url||"/",`http://${e.headers.host}`),i=n.pathname.match(/^\/ws\/(.+)$/);if(!i){t.destroy();return}const o=wr(e),a=e.headers.origin,g=f.security.allowedOrigins;if(a&&!(a===`http://${e.headers.host}`||a===`https://${e.headers.host}`)&&(g.length===0||!g.includes(a))){l("ws_rejected_origin",{ip:o,origin:a}),Ue(t,403,"Origin not allowed");return}const m=kr(o);if(m>=f.security.maxConnectionsPerIp){l("ws_rejected_per_ip_cap",{ip:o,liveFromIp:m}),Ue(t,429,"Too many connections");return}const u=Ee(e)||n.searchParams.get("token")||"",y=u?w.validateToken(u):null;ue.handleUpgrade(e,t,r,p=>{ue.emit("connection",p,e,decodeURIComponent(i[1]),o,y)})}),ue.on("connection",(e,t,r,n,i)=>{const o={ws:e,ip:n,authenticated:!1,authTimeout:null};U.set(e,o),l("ws_connected",{ip:n});let a=null;const{initialLines:g,historyChunk:m,syncIdleMs:u,syncMaxMs:y}=Q;let p=!0,q="",C=null,G=null,z=r,P=null;const J=()=>{C&&(clearTimeout(C),C=null),G&&(clearTimeout(G),G=null)},ce=()=>{if(p){J(),p=!1;try{z=Xt(r)}catch{z=r}try{const
|
|
22
|
+
`),e.destroy()}Er.on("upgrade",(e,t,r)=>{const n=new URL(e.url||"/",`http://${e.headers.host}`),i=n.pathname.match(/^\/ws\/(.+)$/);if(!i){t.destroy();return}const o=wr(e),a=e.headers.origin,g=f.security.allowedOrigins;if(a&&!(a===`http://${e.headers.host}`||a===`https://${e.headers.host}`)&&(g.length===0||!g.includes(a))){l("ws_rejected_origin",{ip:o,origin:a}),Ue(t,403,"Origin not allowed");return}const m=kr(o);if(m>=f.security.maxConnectionsPerIp){l("ws_rejected_per_ip_cap",{ip:o,liveFromIp:m}),Ue(t,429,"Too many connections");return}const u=Ee(e)||n.searchParams.get("token")||"",y=u?w.validateToken(u):null;ue.handleUpgrade(e,t,r,p=>{ue.emit("connection",p,e,decodeURIComponent(i[1]),o,y)})}),ue.on("connection",(e,t,r,n,i)=>{const o={ws:e,ip:n,authenticated:!1,authTimeout:null};U.set(e,o),l("ws_connected",{ip:n});let a=null;const{initialLines:g,historyChunk:m,syncIdleMs:u,syncMaxMs:y}=Q;let p=!0,q="",C=null,G=null,z=r,P=null;const J=()=>{C&&(clearTimeout(C),C=null),G&&(clearTimeout(G),G=null)},ce=()=>{if(p){J(),p=!1;try{z=Xt(r)}catch{z=r}try{const v=Yt(z,g);k(e,{type:"snapshot",data:Ce(v),lines:g})}catch{k(e,{type:"data",data:`\r
|
|
23
23
|
`})}q&&(k(e,{type:"data",data:q}),q="")}},$e=()=>{p&&(C&&clearTimeout(C),C=setTimeout(ce,u))};function le(){try{a=rt.spawn("tmux",["attach-session","-t",r],{name:"xterm-256color",cols:80,rows:24,cwd:process.env.HOME||"/",env:process.env})}catch(c){k(e,{type:"data",data:`\r
|
|
24
24
|
\x1B[31mFailed to attach to tmux session "${r}": ${c.message}\x1B[0m\r
|
|
25
|
-
`}),oe(e,1011,"pty spawn failed");return}W.add(a);let
|
|
25
|
+
`}),oe(e,1011,"pty spawn failed");return}W.add(a);let v=-1,h="";P=Vt(r,({activeIndex:c,windows:T})=>{k(e,{type:"window_changed",activeIndex:c,windows:T});const N=T.map(_=>_.index).join(","),$=c!==v||N!==h;v=c,h=N}),G=setTimeout(ce,y),a.onData(c=>{if(p){q+=c,$e();return}k(e,{type:"data",data:c})}),a.onExit(({exitCode:c})=>{J(),a&&W.delete(a),a=null,k(e,{type:"data",data:`\r
|
|
26
26
|
\x1B[2m--- tmux exited (code ${c}) ---\x1B[0m\r
|
|
27
|
-
`}),oe(e,1e3,"pty exited")})}function Z(
|
|
27
|
+
`}),oe(e,1e3,"pty exited")})}function Z(v,h){o.authenticated=!0,o.authTimeout&&(clearTimeout(o.authTimeout),o.authTimeout=null),l("auth_success",{ip:n,method:v,tokenName:h}),k(e,{type:"auth.ok",setupMode:!f.passwordHash}),le()}function I(v,h={}){k(e,{type:"auth.failed",message:v,...h})}async function Be(v){let h;try{h=JSON.parse(v)}catch{return!1}if(h.type==="auth.token"&&typeof h.token=="string"){const c=w.validateToken(h.token);if(!c){l("token_auth_failed",{ip:n});const T=M.recordFailure(n);return T.permanentLock&&(w.revokeAll(),l("permanent_lock",{ip:n,failures:T.failures})),I("Invalid or expired token",{permanentLock:T.permanentLock,retryAfterMs:T.retryAfterMs}),!0}return w.touch(c.tokenHash),Z("token",c.name),!0}if(h.type==="auth"&&typeof h.password=="string"){const c=M.check(n);if(!c.allowed)return l("rate_limited",{ip:n,retryAfterMs:c.retryAfterMs,permanentLock:c.permanentLock}),I("Too many attempts",{permanentLock:c.permanentLock,retryAfterMs:c.retryAfterMs}),!0;if(!f.passwordHash){if(!f.security.allowRemoteSetup&&!Ie(n))return l("setup_rejected_remote",{ip:n}),I("First-run setup must be performed from localhost"),!0;if(F)return I("Password setup in progress"),!0;const _=je(h.password);if(_)return I(_),!0;F=!0;try{f.passwordHash=await we(h.password),ye(f),l("password_set",{ip:n})}finally{F=!1}const{stored:ee,plaintext:Ve}=w.createAccessToken("setup",f.security.tokenTtlDays);return k(e,{type:"auth.ok",setupMode:!0,token:Ve}),l("token_created",{ip:n,name:ee.name,tokenId:ee.id}),l("auth_success",{ip:n,method:"password",tokenName:ee.name}),le(),!0}if(!f.passwordHash)return I("Server not configured"),!0;if(!await ge(h.password,f.passwordHash)){const _=M.recordFailure(n);return l("auth_failed",{ip:n,method:"password",failures:_.failures,permanentLock:_.permanentLock}),_.permanentLock&&(w.revokeAll(),l("permanent_lock",{ip:n,failures:_.failures})),I("Incorrect password",{permanentLock:_.permanentLock,retryAfterMs:_.retryAfterMs}),!0}M.recordSuccess(n);const N=`ws-${n}`,{plaintext:$}=w.createAccessToken(N,f.security.tokenTtlDays);return Z("password",N),k(e,{type:"auth.ok",setupMode:!1,token:$}),!0}return!1}i?(w.touch(i.tokenHash),Z("token",i.name)):(k(e,{type:"auth.required",setupMode:!f.passwordHash}),o.authTimeout=setTimeout(()=>{o.authenticated||(l("auth_timeout",{ip:n}),I("Authentication timeout"),oe(e,4e3,"Auth timeout"))},f.security.authTimeoutMs)),e.on("message",async v=>{const h=typeof v=="string"?v:v.toString("utf-8");if(!o.authenticated){if(h.length>1e6){k(e,{type:"auth.failed",message:"Message too large"});return}await Be(h)||I("Authentication required");return}if(!a)return;try{if(_t(h,a))return}catch{return}let c;try{c=JSON.parse(h)}catch{return}if(c.type==="load_history"&&typeof c.before=="number"){const T=Math.max(0,Math.floor(c.before));try{const{data:N,lines:$}=Zt(z,T,m);k(e,{type:"history",data:Ce(N),before:T,lines:$})}catch{k(e,{type:"history",data:"",before:T,lines:0})}}}),e.on("close",()=>{o.authTimeout&&clearTimeout(o.authTimeout),U.delete(e),l("ws_disconnected",{ip:n}),J(),P&&(P(),P=null),a&&(W.delete(a),a.kill(),a=null)}),e.on("error",()=>{o.authTimeout&&clearTimeout(o.authTimeout),U.delete(e),J(),P&&(P(),P=null),a&&(W.delete(a),a.kill(),a=null)})});function We(){L.cleanup(),Gt();for(const[e,t]of U){try{e.close(1001,"Server shutting down")}catch{}t.authTimeout&&clearTimeout(t.authTimeout)}U.clear();for(const e of W)try{e.kill()}catch{}W.clear();for(const e of Le)try{e.kill("SIGTERM")}catch{}M.dispose(),process.exit(0)}process.on("SIGINT",We),process.on("SIGTERM",We),setInterval(()=>{w.purgeExpired()},3600*1e3).unref();
|
package/dist/lib/web-browse.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{lookup as
|
|
1
|
+
import{lookup as U}from"node:dns/promises";const A=3*1024*1024,O=8*1024*1024,$=15e3;class v extends Error{status;constructor(n,o=400){super(n),this.status=o,this.name="BrowseError"}}const k=new Map;function J(t){k.delete(t)}function C(t,n,o){let e=k.get(t);e||(e=new Map,k.set(t,e));const s=new URL(n).hostname.toLowerCase(),r=typeof o.getSetCookie=="function"?o.getSetCookie():[];for(const c of r){const i=c.split(";")[0]?.trim();if(!i)continue;const u=/;\s*domain=([^;\s]+)/i.exec(c)?.[1]?.trim().replace(/^\./,"").toLowerCase();e.set(u??s,i)}}function R(t,n){const o=k.get(t);if(!o||o.size===0)return"";const e=new URL(n).hostname.toLowerCase(),s=[];for(const[r,c]of o)(e===r||e.endsWith("."+r))&&s.push(c);return s.join("; ")}function M(t){const n=t.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);if(n){const[e,s]=[Number(n[1]),Number(n[2])];return e===0||e===10||e===127||e===169&&s===254||e===172&&s>=16&&s<=31||e===192&&s===168||e>=224}const o=t.toLowerCase();return o==="::1"||o==="::"||o.startsWith("fe80:")||o.startsWith("fc")||o.startsWith("fd")||o.startsWith("fec0:")}async function T(t){const n=new URL(t);if(n.protocol!=="http:"&&n.protocol!=="https:")throw new v("only http/https URLs are supported");if(process.env.TMUX_WEB_BROWSE_ALLOW_PRIVATE==="1")return;const o=n.hostname;if(M(o))throw new v("private network addresses are blocked");try{const e=await U(o,{all:!0});for(const{address:s}of e)if(M(s))throw new v("private network addresses are blocked")}catch(e){if(e instanceof v)throw e}}async function _(t,n,o){const e=new AbortController,s=setTimeout(()=>e.abort(),$);try{const r=await fetch(t,{...n,redirect:"follow",signal:e.signal});if(!r.ok)throw new v(`the site responded with HTTP ${r.status}`,502);if(!r.body)return{res:r,buf:Buffer.alloc(0),truncated:!1};const c=r.body.getReader(),i=[];let u=0,l=!1;for(;;){const{done:a,value:f}=await c.read();if(a)break;if(u+=f.byteLength,u>o){l=!0,await c.cancel();break}i.push(f)}return{res:r,buf:Buffer.concat(i),truncated:l}}catch(r){if(r instanceof v)throw r;if(r instanceof Error&&r.name==="AbortError")throw new v("timed out after "+$+"ms",504);let c;const i=r.cause;i&&(c=typeof i=="string"?i:i.code||i.message||String(i));const u=r.message||"request failed";throw new v(c&&u!==c?`${u} (${c})`:u,502)}finally{clearTimeout(s)}}function W(t,n){const o=/charset=([\w.-]+)/i.exec(t||"");if(o)return o[1];const e=n.subarray(0,4096).toString("latin1"),s=/<meta[^>]+charset=["']?([\w.-]+)/i.exec(e);return s?s[1]:"utf-8"}const q={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:"\xA0",copy:"\xA9",reg:"\xAE",trade:"\u2122",hellip:"\u2026",mdash:"\u2014",ndash:"\u2013",lsquo:"\u2018",rsquo:"\u2019",ldquo:"\u201C",rdquo:"\u201D"};function E(t){return t.replace(/&#x([0-9a-f]+);/gi,(n,o)=>String.fromCodePoint(parseInt(o,16)||0)).replace(/&#(\d+);/g,(n,o)=>String.fromCodePoint(parseInt(o,10)||0)).replace(/&([a-zA-Z][a-zA-Z0-9]*);/g,(n,o)=>q[o]??n)}function H(t,n,o){return`${o==="nav"?"/api/browse":"/api/browse/asset"}?url=${encodeURIComponent(t)}&tab=${encodeURIComponent(n)}`}function y(t,n,o,e){const s=t.trim();if(!s)return t;if(s.startsWith("#"))return s;if(/^(javascript|data|vbscript):/i.test(s))return e==="nav"?"":t;if(/^(mailto|tel|sms|about|blob|file):/i.test(s))return t;let r;try{r=new URL(s,n).href}catch{return t}if(!/^https?:/i.test(r))return t;try{const c=new URL(r);if(/(^|\.)duckduckgo\.com$/i.test(c.hostname)&&c.pathname==="/l/"&&e==="nav"){const i=c.searchParams.get("uddg");i&&/^https?:/i.test(i)&&(r=i)}}catch{}return H(r,o,e)}function z(t,n,o){return t.split(",").map(e=>{const s=e.trim().match(/^(\S+)(\s+.+)?$/);if(!s)return e;const r=y(s[1],n,o,"asset");return r?r+(s[2]??""):e}).join(", ")}function B(t,n,o){return t.replace(/url\(\s*(['"]?)(.*?)\1\s*\)/g,(e,s,r)=>{const c=E(r.trim()),i=y(c,n,o,"asset");return i?`url(${i})`:e})}function N(t){const n=/([a-zA-Z-]+)(?:\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g,o=[];let e;for(;e=n.exec(t);)o.push({name:e[1],value:e[3]??e[4]??e[5]??""});return o}function b(t){return t.replace(/&/g,"&").replace(/"/g,""")}function j(t,n,o){const e=/<title[^>]*>([\s\S]*?)<\/title>/i.exec(t),s=e?E(e[1]).replace(/\s+/g," ").trim():"",r=n,c=[];let i=0;const u=/<(\/)?([a-zA-Z][a-zA-Z0-9]*)((?:\s+[^<>]*?)?)(\/?)>/gs;let l;for(;l=u.exec(t);){c.push(t.slice(i,l.index));const[a,f,d,h,S]=l,p=d.toLowerCase();if(i=l.index+a.length,f){c.push(a);continue}if(p==="script"||p==="style"){const m=l.index+a.length,x=new RegExp(`</${p}\\s*>`,"gi"),g=t.slice(m),w=x.exec(g),P=w?g.slice(0,w.index):g,L=w?w[0]:"";p==="style"?c.push(a+B(P,r,o)+L):c.push(a+L),i=w?m+w.index+L.length:t.length,u.lastIndex=i;continue}if(p==="iframe"||p==="object"||p==="embed"){if(!S){const m=l.index+a.length,x=new RegExp(`</${p}\\s*>`,"gi"),g=t.slice(m),w=x.exec(g);i=w?m+w.index+w[0].length:t.length}u.lastIndex=i;continue}if(p==="base"){u.lastIndex=i;continue}if(p==="meta"&&/http-equiv=["']?refresh/i.test(h)){u.lastIndex=i;continue}c.push(Z(a,p,h,r,o,l.index,t))}return c.push(t.slice(i)),{html:c.join(""),title:s}}function Z(t,n,o,e,s,r,c){const i=N(o),u=[];for(const a of i){const f=a.name.toLowerCase(),d=a.value;if(f==="href"){if(n==="a"||n==="area")/^javascript:/i.test(d.trim())?a.remove=!0:a.value=y(d,e,s,"nav");else if(n==="link"){const h=(i.find(S=>S.name.toLowerCase()==="rel")?.value||"").toLowerCase();h.includes("stylesheet")||h.includes("icon")?a.value=y(d,e,s,"asset"):a.remove=!0}}else f==="src"&&(n==="img"||n==="source"||n==="video"||n==="audio"||n==="input")?a.value=y(d,e,s,"asset"):f==="srcset"&&n==="img"?a.value=z(d,e,s):f==="poster"&&n==="video"?a.value=y(d,e,s,"asset"):f==="style"?a.value=B(d,e,s):f.startsWith("on")&&(a.remove=!0)}if(n==="form"){let a=i.find(h=>h.name.toLowerCase()==="action"),f=e;if(a&&a.value.trim())try{f=new URL(E(a.value.trim()),e).href}catch{f=e}const d=(i.find(h=>h.name.toLowerCase()==="method")?.value||"get").toLowerCase();a?a.value=`/api/browse/submit?tab=${encodeURIComponent(s)}`:i.push({name:"action",value:`/api/browse/submit?tab=${encodeURIComponent(s)}`}),u.push(`<input type="hidden" name="__browse_action" value="${b(f)}">`),u.push(`<input type="hidden" name="__browse_method" value="${b(d)}">`)}let l="<"+n;for(const a of i)a.remove||a.name==="href"&&n==="a"&&a.value===""||(l+=" "+a.name+'="'+b(a.value)+'"');return l+=">",u.length&&(l+=u.join("")),l}async function X(t,n){await T(t);const o=R(n,t),{res:e,buf:s,truncated:r}=await _(t,{headers:o?{cookie:o}:void 0},A);C(n,t,e.headers);const c=e.headers.get("content-type")||"",i=W(c,s),u=new TextDecoder(i).decode(s),l=e.url||t;if(/html/i.test(c)||!/^(text|application)\/(json|xml|xhtml)/i.test(c)&&/<html[\s>]/i.test(u.slice(0,2e3))){const{html:f,title:d}=j(u,l,n);return{url:t,finalUrl:l,title:d,html:f,contentType:c,truncated:r}}if(/json/i.test(c))try{const f=JSON.stringify(JSON.parse(u),null,2);return{url:t,finalUrl:l,title:new URL(l).hostname,html:`<pre>${b(f)}</pre>`,contentType:c,truncated:r}}catch{}const a=b(u);return{url:t,finalUrl:l,title:new URL(l).hostname,html:`<pre style="white-space:pre-wrap;word-break:break-word;font-family:var(--font-mono);font-size:12px;line-height:1.6;padding:16px">${a}</pre>`,contentType:c,truncated:r}}async function V(t,n){await T(t);const o=R(n,t),{res:e,buf:s,truncated:r}=await _(t,{headers:o?{cookie:o}:void 0},O);return C(n,t,e.headers),{buf:s,contentType:e.headers.get("content-type")||"application/octet-stream"}}async function Y(t,n,o,e){let s=t,r;if(o==="get"){const m=new URL(t);for(const[x,g]of n)m.searchParams.append(x,g);s=m.href,r={}}else{const m=new URLSearchParams;for(const[x,g]of n)m.append(x,g);r={method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:m.toString()}}await T(s);const c=R(e,s),{res:i,buf:u,truncated:l}=await _(s,{...r,headers:{...r.headers,...c?{cookie:c}:{}}},A);C(e,s,i.headers);const a=i.headers.get("content-type")||"",f=W(a,u),d=new TextDecoder(f).decode(u),h=i.url||s,{html:S,title:p}=j(d,h,e);return{url:s,finalUrl:h,title:p,html:S,contentType:a,truncated:l}}export{O as BROWSE_MAX_ASSET_BYTES,A as BROWSE_MAX_HTML_BYTES,$ as BROWSE_TIMEOUT_MS,v as BrowseError,V as browseAsset,X as browsePage,Y as browseSubmit,J as clearTabCookies,j as rewriteHtml};
|