2025-09-12 14:42:00 -05:00
import express from "express" ;
2026-04-22 16:55:23 -05:00
import { createCorsMiddleware } from "../utils/cors-config.js" ;
2025-10-01 15:40:10 -05:00
import cookieParser from "cookie-parser" ;
2025-11-05 10:36:16 -06:00
import axios from "axios" ;
2025-09-12 14:42:00 -05:00
import { Client as SSHClient } from "ssh2" ;
2026-04-22 16:55:23 -05:00
import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js" ;
2025-10-01 15:40:10 -05:00
import { getDb } from "../database/db/index.js" ;
2026-06-04 15:16:53 -04:00
import { hosts } from "../database/db/schema.js" ;
2025-09-12 14:42:00 -05:00
import { eq , and } from "drizzle-orm" ;
2026-03-08 18:02:14 -05:00
import { fileLogger } from "../utils/logger.js" ;
2025-10-01 15:40:10 -05:00
import { SimpleDBOps } from "../utils/simple-db-ops.js" ;
import { AuthManager } from "../utils/auth-manager.js" ;
2026-03-08 18:02:14 -05:00
import type { AuthenticatedRequest , ProxyNode } from "../../types/index.js" ;
import {
createSocks5Connection ,
type SOCKS5Config ,
} from "../utils/socks5-helper.js" ;
2026-01-24 19:49:42 -06:00
import type { LogEntry , ConnectionStage } from "../../types/connection-log.js" ;
2026-02-12 22:28:13 -06:00
import { SSHHostKeyVerifier } from "./host-key-verifier.js" ;
2026-06-04 15:16:53 -04:00
import { resolveHostById } from "./host-resolver.js" ;
import type { SSHHost } from "../../types/index.js" ;
import {
startHostTransfer ,
getTransferStatus ,
listActiveTransfers ,
probeHungStreamTransfers ,
requestTransferCancel ,
cleanupCancelledTransfer ,
retryHostTransfer ,
previewArchiveTransferMethod ,
type HostTransferDeps ,
} from "./host-transfer.js" ;
import { registerFileContentRoutes } from "./file-manager-content-routes.js" ;
import { createConnectionLog } from "./file-manager-log.js" ;
import { createJumpHostChain } from "./jump-host-chain.js" ;
import {
ChannelOpenSerializer ,
execChannel ,
getSessionSftp ,
type PendingTOTPSession ,
type SSHSession ,
} from "./file-manager-session.js" ;
import { registerFileListingRoutes } from "./file-manager-list-routes.js" ;
import { registerFileOperationRoutes } from "./file-manager-operation-routes.js" ;
import { registerFileDownloadRoutes } from "./file-manager-download-routes.js" ;
import { registerFileActionRoutes } from "./file-manager-action-routes.js" ;
2026-01-24 19:49:42 -06:00
2025-08-07 02:20:27 -05:00
const app = express ();
2026-04-22 16:55:23 -05:00
app . use ( createCorsMiddleware ([ "GET" , "POST" , "PUT" , "DELETE" , "OPTIONS" ]));
2025-10-01 15:40:10 -05:00
app . use ( cookieParser ());
app . use ( express . json ({ limit : "1gb" }));
app . use ( express . urlencoded ({ limit : "1gb" , extended : true }));
app . use ( express . raw ({ limit : "5gb" , type : "application/octet-stream" }));
2026-03-08 18:02:14 -05:00
app . use (( _req , res , next ) => {
res . setHeader ( "Cache-Control" , "no-store" );
next ();
});
2025-10-01 15:40:10 -05:00
const authManager = AuthManager . getInstance ();
app . use ( authManager . createAuthMiddleware ());
2025-08-07 02:20:27 -05:00
const sshSessions : Record < string , SSHSession > = {};
2025-11-05 10:36:16 -06:00
const pendingTOTPSessions : Record < string , PendingTOTPSession > = {};
2026-05-06 15:12:07 -05:00
// Keyed by "sessionId:path" to prevent concurrent requests for the same path
const activeListRequests : Record < string , boolean > = {};
2025-08-07 02:20:27 -05:00
function cleanupSession ( sessionId : string ) {
2025-09-12 14:42:00 -05:00
const session = sshSessions [ sessionId ];
if ( session ) {
2025-11-17 09:46:05 -06:00
if ( session . activeOperations > 0 ) {
fileLogger . warn (
`Deferring session cleanup for ${ sessionId } - ${ session . activeOperations } active operations` ,
{
operation : "cleanup_deferred" ,
sessionId ,
activeOperations : session.activeOperations ,
},
);
scheduleSessionCleanup ( sessionId );
return ;
}
2026-03-08 18:02:14 -05:00
try {
if ( session . sftp ) {
session . sftp . end ();
session . sftp = undefined ;
}
} catch {
// expected
}
2025-09-12 14:42:00 -05:00
try {
session . client . end ();
2026-03-08 18:02:14 -05:00
} catch {
// expected
}
2025-09-12 14:42:00 -05:00
clearTimeout ( session . timeout );
delete sshSessions [ sessionId ];
}
2025-08-07 02:20:27 -05:00
}
function scheduleSessionCleanup ( sessionId : string ) {
2025-09-12 14:42:00 -05:00
const session = sshSessions [ sessionId ];
if ( session ) {
if ( session . timeout ) clearTimeout ( session . timeout );
2025-10-01 15:40:10 -05:00
session . timeout = setTimeout (
() => {
cleanupSession ( sessionId );
},
30 * 60 * 1000 ,
);
2025-09-12 14:42:00 -05:00
}
2025-08-07 02:20:27 -05:00
}
2026-04-22 16:55:23 -05:00
function verifySessionOwnership ( session : SSHSession , userId : string ) : boolean {
return ! session . userId || session . userId === userId ;
}
2026-06-04 15:16:53 -04:00
function resolveBrowseHostId (
browseSessionId : string ,
browseSession : SSHSession ,
) : number | undefined {
if ( browseSession . hostId ) return browseSession . hostId ;
const parsed = Number . parseInt ( browseSessionId , 10 );
return Number . isFinite ( parsed ) ? parsed : undefined ;
2025-10-01 15:40:10 -05:00
}
2026-06-04 15:16:53 -04:00
async function buildDedicatedTransferConnectConfig (
host : SSHHost ,
userId : string ,
client : SSHClient ,
) : Promise < Record < string , unknown >> {
const { ip , port , username } = host ;
2026-06-21 15:55:41 -05:00
const preloadedHostData = await SSHHostKeyVerifier . preloadHostData ( host . id );
2026-06-04 15:16:53 -04:00
const config : Record < string , unknown > = {
host : ip?.replace ( /^\[|\]$/g , "" ) || ip ,
port ,
username ,
tryKeyboard : true ,
keepaliveInterval : 30000 ,
keepaliveCountMax : 120 ,
readyTimeout : 60000 ,
tcpKeepAlive : true ,
tcpKeepAliveInitialDelay : 5000 ,
hostVerifier : await SSHHostKeyVerifier . createHostVerifier (
host . id ,
ip ,
port ,
null ,
userId ,
false ,
2026-06-21 15:55:41 -05:00
preloadedHostData ,
2026-06-04 15:16:53 -04:00
),
env : {
TERM : "xterm-256color" ,
LANG : "en_US.UTF-8" ,
LC_ALL : "en_US.UTF-8" ,
},
algorithms : {
kex : [
"curve25519-sha256" ,
"curve25519-sha256@libssh.org" ,
"ecdh-sha2-nistp521" ,
"ecdh-sha2-nistp384" ,
"ecdh-sha2-nistp256" ,
"diffie-hellman-group-exchange-sha256" ,
"diffie-hellman-group14-sha256" ,
"diffie-hellman-group14-sha1" ,
"diffie-hellman-group-exchange-sha1" ,
"diffie-hellman-group1-sha1" ,
],
serverHostKey : [
"ssh-ed25519" ,
"ecdsa-sha2-nistp521" ,
"ecdsa-sha2-nistp384" ,
"ecdsa-sha2-nistp256" ,
"rsa-sha2-512" ,
"rsa-sha2-256" ,
"ssh-rsa" ,
"ssh-dss" ,
],
cipher : SSH_ALGORITHMS.cipher ,
hmac : [
"hmac-sha2-512-etm@openssh.com" ,
"hmac-sha2-256-etm@openssh.com" ,
"hmac-sha2-512" ,
"hmac-sha2-256" ,
"hmac-sha1" ,
"hmac-md5" ,
],
compress : [ "none" , "zlib@openssh.com" , "zlib" ],
},
};
2026-01-02 00:33:10 -06:00
2026-06-04 15:16:53 -04:00
const authType = host . authType ;
2026-01-02 00:33:10 -06:00
2026-06-04 15:16:53 -04:00
if ( authType === "key" && host . key ? . trim ()) {
const cleanKey = host . key
. trim ()
. replace ( /\r\n/g , "\n" )
. replace ( /\r/g , "\n" );
config . privateKey = Buffer . from ( cleanKey , "utf8" );
if ( host . keyPassword ) config . passphrase = host . keyPassword ;
} else if ( authType === "password" ) {
if ( ! host . password ) {
throw new Error ( "Password required for transfer connection" );
}
config . password = host . password ;
} else if ( authType === "opkssh" ) {
const { getOPKSSHToken } = await import ( "./opkssh-auth.js" );
const token = await getOPKSSHToken ( userId , host . id );
if ( ! token ) {
throw new Error (
"OPKSSH authentication required. Open a Terminal connection to this host first." ,
);
}
const { setupOPKSSHCertAuth } = await import ( "./opkssh-cert-auth.js" );
await setupOPKSSHCertAuth (
config as import ( "ssh2" ). ConnectConfig ,
client ,
token ,
username ,
);
2026-06-16 15:59:53 -05:00
} else if ( authType !== "none" && authType !== "tailscale" ) {
2026-06-04 15:16:53 -04:00
throw new Error ( `Unsupported auth type for transfer: ${ authType } ` );
}
2026-01-02 00:33:10 -06:00
2026-06-04 15:16:53 -04:00
return config ;
}
function attachDedicatedKeyboardInteractive (
client : SSHClient ,
host : SSHHost ,
) : void {
client . on (
"keyboard-interactive" ,
(
_name : string ,
_instructions : string ,
_instructionsLang : string ,
prompts : Array < { prompt : string ; echo : boolean } > ,
finish : ( responses : string []) => void ,
) => {
const responses = prompts . map (( p ) => {
if ( /password/i . test ( p . prompt ) && host . password ) {
return host . password ;
}
return "" ;
});
finish ( responses );
},
);
}
async function startDedicatedTransferConnect (
client : SSHClient ,
config : Record < string , unknown >,
host : SSHHost ,
userId : string ,
) : Promise < void > {
const proxyConfig : SOCKS5Config | null =
host . useSocks5 &&
( host . socks5Host ||
( host . socks5ProxyChain && host . socks5ProxyChain . length > 0 ))
? {
useSocks5 : host.useSocks5 ,
socks5Host : host.socks5Host ,
socks5Port : host.socks5Port ,
socks5Username : host.socks5Username ,
socks5Password : host.socks5Password ,
socks5ProxyChain : host.socks5ProxyChain ,
}
: null ;
const jumpHosts = host . jumpHosts ;
const hasJumpHosts = jumpHosts && jumpHosts . length > 0 ;
if ( hasJumpHosts ) {
const jumpClient = await createJumpHostChain (
jumpHosts ,
userId ,
proxyConfig ,
);
if ( ! jumpClient ) {
throw new Error ( "Failed to connect through jump hosts for transfer" );
2026-01-02 00:33:10 -06:00
}
2026-06-04 15:16:53 -04:00
await new Promise < void >(( resolve , reject ) => {
jumpClient . forwardOut (
"127.0.0.1" ,
0 ,
host . ip ,
host . port ,
( err , stream ) => {
if ( err ) {
jumpClient . end ();
reject (
new Error (
`Failed to forward through jump host for transfer: ${ err . message } ` ,
),
);
return ;
}
config . sock = stream ;
client . connect ( config );
resolve ();
},
);
});
return ;
}
if ( proxyConfig ) {
const proxySocket = await createSocks5Connection (
host . ip ,
host . port ,
proxyConfig ,
);
if ( proxySocket ) {
config . sock = proxySocket ;
2026-01-02 00:33:10 -06:00
}
}
2026-06-04 15:16:53 -04:00
client . connect ( config );
2026-01-02 00:33:10 -06:00
}
2026-06-04 15:16:53 -04:00
async function openDedicatedTransferSession (
browseSessionId : string ,
dedicatedSessionId : string ,
userId : string ,
transferId : string ,
options ?: { allowBrowseDisconnected? : boolean },
) : Promise < SSHSession > {
const browseSession = sshSessions [ browseSessionId ];
if ( ! options ? . allowBrowseDisconnected && ! browseSession ? . isConnected ) {
throw new Error ( "Browse SSH session not connected" );
}
if ( browseSession && ! verifySessionOwnership ( browseSession , userId )) {
throw new Error ( "Session access denied" );
}
const hostId = browseSession
? resolveBrowseHostId ( browseSessionId , browseSession )
: (() => {
const parsed = Number . parseInt ( browseSessionId , 10 );
return Number . isFinite ( parsed ) ? parsed : undefined ;
})();
if ( ! hostId ) {
throw new Error ( "Cannot open transfer connection: unknown host" );
}
const host = await resolveHostById ( hostId , userId );
if ( ! host ) {
throw new Error ( "Host not found for transfer connection" );
}
if ( sshSessions [ dedicatedSessionId ] ? . isConnected ) {
closeDedicatedTransferSession ( dedicatedSessionId );
}
const client = new SSHClient ();
attachDedicatedKeyboardInteractive ( client , host );
const config = await buildDedicatedTransferConnectConfig (
host ,
userId ,
client ,
);
fileLogger . info ( "Opening dedicated transfer SSH session" , {
operation : "transfer_ssh_connect" ,
transferId ,
browseSessionId ,
dedicatedSessionId ,
hostId ,
ip : host.ip ,
port : host.port ,
username : host.username ,
});
await new Promise < void >(( resolve , reject ) => {
const connectTimeout = setTimeout (() => {
client . end ();
reject ( new Error ( "Transfer SSH connection timed out" ));
}, 60000 );
const fail = ( err : Error ) => {
clearTimeout ( connectTimeout );
reject ( err );
};
client . once ( "ready" , () => {
clearTimeout ( connectTimeout );
resolve ();
});
client . once ( "error" , fail );
void startDedicatedTransferConnect ( client , config , host , userId ). catch (
fail ,
);
});
const session : SSHSession = {
client ,
isConnected : true ,
lastActive : Date.now (),
activeOperations : 0 ,
channelOpener : new ChannelOpenSerializer (),
userId ,
ip : host.ip ,
port : host.port ,
hostId : host.id ,
username : host.username ,
transferDedicated : true ,
transferId ,
browseSessionId ,
};
client . on ( "close" , () => {
fileLogger . info ( "Dedicated transfer SSH connection closed" , {
operation : "transfer_ssh_disconnected" ,
transferId ,
dedicatedSessionId ,
browseSessionId ,
hostId ,
});
const existing = sshSessions [ dedicatedSessionId ];
if ( existing ) {
existing . isConnected = false ;
closeDedicatedTransferSession ( dedicatedSessionId );
}
});
sshSessions [ dedicatedSessionId ] = session ;
return session ;
}
function closeDedicatedTransferSession ( sessionId : string ) : void {
const session = sshSessions [ sessionId ];
if ( ! session ? . transferDedicated ) return ;
fileLogger . info ( "Closing dedicated transfer SSH session" , {
operation : "transfer_ssh_close" ,
sessionId ,
transferId : session.transferId ,
browseSessionId : session.browseSessionId ,
});
try {
if ( session . sftp ) {
session . sftp . end ();
session . sftp = undefined ;
}
} catch {
// expected
}
session . sftpPending = undefined ;
try {
session . client . end ();
} catch {
// expected
}
clearTimeout ( session . timeout );
delete sshSessions [ sessionId ];
}
app . use ( "/ssh/file_manager/ssh" , ( req , res , next ) => {
if (
req . path === "/connect" ||
req . path === "/connect-totp" ||
req . path === "/connect-warpgate"
) {
return next ();
}
const sessionId = ( req . query . sessionId as string ) || req . body ? . sessionId ;
if ( ! sessionId ) return next ();
const session = sshSessions [ sessionId ];
if ( ! session ) return next ();
const userId = ( req as AuthenticatedRequest ). userId ;
if ( ! verifySessionOwnership ( session , userId )) {
return res . status ( 403 ). json ({ error : "Session access denied" });
}
next ();
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/connect:
* post:
* summary: Connect to SSH for file management
* description: Establishes an SSH/SFTP connection for file manager operations. Supports password, key-based, and keyboard-interactive authentication, as well as jump hosts and SOCKS5 proxies.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - sessionId
* - ip
* - port
* - username
* properties:
* sessionId:
* type: string
* description: Unique session identifier
* hostId:
* type: number
* description: Host ID from database
* ip:
* type: string
* description: SSH server IP address
* port:
* type: number
* description: SSH server port
* username:
* type: string
* description: SSH username
* password:
* type: string
* description: SSH password (for password auth)
* sshKey:
* type: string
* description: SSH private key (for key-based auth)
* keyPassword:
* type: string
* description: Private key passphrase
* authType:
* type: string
* enum: [password, key, none]
* description: Authentication method
* credentialId:
* type: number
* description: Credential ID to use from database
* userProvidedPassword:
* type: string
* description: User-provided password for keyboard-interactive auth
* forceKeyboardInteractive:
* type: boolean
* description: Force keyboard-interactive authentication
* jumpHosts:
* type: array
* description: Jump host configuration
* items:
* type: object
* properties:
* hostId:
* type: number
* useSocks5:
* type: boolean
* description: Use SOCKS5 proxy
* socks5Host:
* type: string
* description: SOCKS5 proxy host
* socks5Port:
* type: number
* description: SOCKS5 proxy port
* socks5Username:
* type: string
* description: SOCKS5 proxy username
* socks5Password:
* type: string
* description: SOCKS5 proxy password
* socks5ProxyChain:
* type: array
* description: Chain of SOCKS5 proxies
* responses:
* 200:
* description: SSH connection established successfully, or requires TOTP/Warpgate authentication.
* content:
* application/json:
* schema:
* oneOf:
* - type: object
* properties:
* status:
* type: string
* example: success
* message:
* type: string
* connectionLogs:
* type: array
* - type: object
* properties:
* requires_totp:
* type: boolean
* sessionId:
* type: string
* prompt:
* type: string
* connectionLogs:
* type: array
* - type: object
* properties:
* requires_warpgate:
* type: boolean
* sessionId:
* type: string
* url:
* type: string
* securityKey:
* type: string
* connectionLogs:
* type: array
* - type: object
* properties:
* status:
* type: string
* example: auth_required
* reason:
* type: string
* connectionLogs:
* type: array
* 400:
* description: Missing required parameters or invalid SSH key format.
* 401:
* description: Authentication required.
* 500:
* description: SSH connection failed.
*/
2025-09-12 14:42:00 -05:00
app . post ( "/ssh/file_manager/ssh/connect" , async ( req , res ) => {
const {
sessionId ,
hostId ,
ip ,
port ,
username ,
password ,
sshKey ,
keyPassword ,
authType ,
credentialId ,
2025-11-17 09:46:05 -06:00
jumpHosts ,
2025-12-31 22:20:12 -06:00
useSocks5 ,
socks5Host ,
socks5Port ,
socks5Username ,
socks5Password ,
socks5ProxyChain ,
2025-09-12 14:42:00 -05:00
} = req . body ;
2025-08-07 02:20:27 -05:00
2025-11-05 10:36:16 -06:00
const userId = ( req as AuthenticatedRequest ). userId ;
2026-01-24 19:49:42 -06:00
const connectionLogs : Array < Omit < LogEntry , "id" | "timestamp" >> = [];
connectionLogs . push (
createConnectionLog (
"info" ,
"sftp_connecting" ,
`Initiating SFTP connection to ${ username } @ ${ ip } : ${ port } ` ,
),
);
2025-10-01 15:40:10 -05:00
if ( ! userId ) {
fileLogger . error ( "SSH connection rejected: no authenticated user" , {
operation : "file_connect_auth" ,
sessionId ,
});
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"error" ,
"sftp_auth" ,
"Authentication required - no user session" ,
),
);
return res
. status ( 401 )
. json ({ error : "Authentication required" , connectionLogs });
2025-10-01 15:40:10 -05:00
}
2025-09-12 14:42:00 -05:00
if ( ! sessionId || ! ip || ! username || ! port ) {
fileLogger . warn ( "Missing SSH connection parameters for file manager" , {
operation : "file_connect" ,
sessionId ,
hasIp : !! ip ,
hasUsername : !! username ,
hasPort : !! port ,
2025-08-07 02:20:27 -05:00
});
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"error" ,
"sftp_connecting" ,
"Missing required connection parameters" ,
),
);
return res
. status ( 400 )
. json ({ error : "Missing SSH connection parameters" , connectionLogs });
2025-09-12 14:42:00 -05:00
}
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
if ( sshSessions [ sessionId ] ? . isConnected ) {
2025-08-07 02:20:27 -05:00
cleanupSession ( sessionId );
2025-09-12 14:42:00 -05:00
}
2025-12-31 22:20:12 -06:00
if ( pendingTOTPSessions [ sessionId ]) {
try {
pendingTOTPSessions [ sessionId ]. client . end ();
2026-03-08 18:02:14 -05:00
} catch {
// expected
}
2025-12-31 22:20:12 -06:00
delete pendingTOTPSessions [ sessionId ];
}
2025-09-12 14:42:00 -05:00
const client = new SSHClient ();
2025-08-07 02:20:27 -05:00
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"info" ,
"sftp_auth" ,
"Resolving authentication credentials" ,
),
);
2026-04-22 16:55:23 -05:00
// Resolve credentials server-side when frontend doesn't provide them
2026-05-28 22:29:20 -05:00
let resolvedCredentials = {
password ,
sshKey ,
keyPassword ,
authType ,
sudoPassword : undefined as string | undefined ,
};
2026-05-28 22:05:25 -04:00
let hostKeepaliveInterval : number | undefined ;
let hostKeepaliveCountMax : number | undefined ;
2026-06-04 15:16:53 -04:00
let resolvedIp = ip ;
let resolvedPort = port ;
let resolvedUsername = username ;
2026-06-16 15:59:53 -05:00
let resolvedJumpHosts = jumpHosts ;
2026-06-21 15:55:41 -05:00
let resolvedScpLegacy = false ;
let resolvedUseSocks5 = useSocks5 ;
let resolvedSocks5Host = socks5Host ;
let resolvedSocks5Port = socks5Port ;
let resolvedSocks5Username = socks5Username ;
let resolvedSocks5Password = socks5Password ;
let resolvedSocks5ProxyChain = socks5ProxyChain ;
2026-04-22 16:55:23 -05:00
if ( hostId && userId && ! password && ! sshKey ) {
try {
const { resolveHostById } = await import ( "./host-resolver.js" );
const resolvedHost = await resolveHostById ( hostId , userId );
if ( resolvedHost ) {
2026-06-04 15:16:53 -04:00
resolvedIp = resolvedHost . ip ;
resolvedPort = resolvedHost . port ;
resolvedUsername = resolvedHost . username ;
2026-04-22 16:55:23 -05:00
resolvedCredentials = {
password : resolvedHost.password ,
sshKey : resolvedHost.key ,
keyPassword : resolvedHost.keyPassword ,
authType : resolvedHost.authType ,
2026-05-28 22:29:20 -05:00
sudoPassword : resolvedHost.sudoPassword as string | undefined ,
2026-04-22 16:55:23 -05:00
};
2026-05-28 22:05:25 -04:00
hostKeepaliveInterval = resolvedHost . terminalConfig ? . keepaliveInterval ;
hostKeepaliveCountMax = resolvedHost . terminalConfig ? . keepaliveCountMax ;
2026-06-21 15:55:41 -05:00
resolvedScpLegacy = resolvedHost . scpLegacy ?? false ;
if ( resolvedHost . useSocks5 ) {
resolvedUseSocks5 = resolvedHost . useSocks5 ;
resolvedSocks5Host = resolvedHost . socks5Host ;
resolvedSocks5Port = resolvedHost . socks5Port ;
resolvedSocks5Username = resolvedHost . socks5Username ;
resolvedSocks5Password = resolvedHost . socks5Password ;
resolvedSocks5ProxyChain = resolvedHost . socks5ProxyChain ;
}
2026-06-16 15:59:53 -05:00
if (
( ! resolvedJumpHosts || resolvedJumpHosts . length === 0 ) &&
resolvedHost . jumpHosts &&
resolvedHost . jumpHosts . length > 0
) {
resolvedJumpHosts = resolvedHost . jumpHosts ;
connectionLogs . push (
createConnectionLog (
"info" ,
"jump" ,
`Loaded ${ resolvedHost . jumpHosts . length } jump host(s) from server-side host data` ,
),
);
}
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
2026-04-22 16:55:23 -05:00
"info" ,
2026-01-24 19:49:42 -06:00
"sftp_auth" ,
2026-04-22 16:55:23 -05:00
"Credentials resolved from server-side host data" ,
2026-01-24 19:49:42 -06:00
),
);
2026-03-14 20:05:05 -05:00
}
2026-04-22 16:55:23 -05:00
} catch ( error ) {
fileLogger . warn ( `Failed to resolve host credentials for ${ hostId } ` , {
operation : "ssh_credentials" ,
hostId ,
error : error instanceof Error ? error . message : "Unknown error" ,
});
2025-08-07 02:20:27 -05:00
}
2026-04-22 16:55:23 -05:00
} else if ( credentialId && hostId && userId ) {
// Legacy: credential resolution from credentialId
try {
const { resolveHostById } = await import ( "./host-resolver.js" );
const resolvedHost = await resolveHostById ( hostId , userId );
if ( resolvedHost ) {
2026-06-04 15:16:53 -04:00
resolvedIp = resolvedHost . ip ;
resolvedPort = resolvedHost . port ;
resolvedUsername = resolvedHost . username ;
2026-04-22 16:55:23 -05:00
resolvedCredentials = {
password : resolvedHost.password ,
sshKey : resolvedHost.key ,
keyPassword : resolvedHost.keyPassword ,
authType : resolvedHost.authType ,
2026-05-28 22:29:20 -05:00
sudoPassword : resolvedHost.sudoPassword as string | undefined ,
2026-04-22 16:55:23 -05:00
};
2026-05-28 22:05:25 -04:00
hostKeepaliveInterval = resolvedHost . terminalConfig ? . keepaliveInterval ;
hostKeepaliveCountMax = resolvedHost . terminalConfig ? . keepaliveCountMax ;
2026-06-21 15:55:41 -05:00
resolvedScpLegacy = resolvedHost . scpLegacy ?? false ;
if ( resolvedHost . useSocks5 ) {
resolvedUseSocks5 = resolvedHost . useSocks5 ;
resolvedSocks5Host = resolvedHost . socks5Host ;
resolvedSocks5Port = resolvedHost . socks5Port ;
resolvedSocks5Username = resolvedHost . socks5Username ;
resolvedSocks5Password = resolvedHost . socks5Password ;
resolvedSocks5ProxyChain = resolvedHost . socks5ProxyChain ;
}
2026-06-16 15:59:53 -05:00
if (
( ! resolvedJumpHosts || resolvedJumpHosts . length === 0 ) &&
resolvedHost . jumpHosts &&
resolvedHost . jumpHosts . length > 0
) {
resolvedJumpHosts = resolvedHost . jumpHosts ;
connectionLogs . push (
createConnectionLog (
"info" ,
"jump" ,
`Loaded ${ resolvedHost . jumpHosts . length } jump host(s) from server-side host data` ,
),
);
}
2026-04-22 16:55:23 -05:00
connectionLogs . push (
createConnectionLog (
"info" ,
"sftp_auth" ,
"Credentials resolved from credential store" ,
),
);
}
} catch ( error ) {
fileLogger . warn ( `Failed to resolve credentials for host ${ hostId } ` , {
2025-10-01 15:40:10 -05:00
operation : "ssh_credentials" ,
2025-09-12 14:42:00 -05:00
hostId ,
credentialId ,
2026-04-22 16:55:23 -05:00
error : error instanceof Error ? error . message : "Unknown error" ,
});
}
2025-09-12 14:42:00 -05:00
}
2025-08-07 02:20:27 -05:00
2026-06-21 15:55:41 -05:00
const preloadedHostData = await SSHHostKeyVerifier . preloadHostData ( hostId );
2025-11-05 10:36:16 -06:00
const config : Record < string , unknown > = {
2026-06-04 15:16:53 -04:00
host : resolvedIp?.replace ( /^\[|\]$/g , "" ) || resolvedIp ,
port : resolvedPort ,
username : resolvedUsername ,
2025-11-05 10:36:16 -06:00
tryKeyboard : true ,
2026-05-28 22:05:25 -04:00
keepaliveInterval :
typeof hostKeepaliveInterval === "number"
2026-06-21 15:55:41 -05:00
? Math . max ( 5000 , hostKeepaliveInterval * 1000 )
2026-05-28 22:05:25 -04:00
: 60000 ,
keepaliveCountMax :
2026-06-21 15:55:41 -05:00
typeof hostKeepaliveCountMax === "number"
? Math . max ( 1 , hostKeepaliveCountMax )
: 5 ,
2025-11-05 10:36:16 -06:00
readyTimeout : 60000 ,
tcpKeepAlive : true ,
2026-05-28 22:05:25 -04:00
tcpKeepAliveInitialDelay : 30000 ,
2026-02-12 22:28:13 -06:00
hostVerifier : await SSHHostKeyVerifier . createHostVerifier (
hostId ,
2026-06-04 15:16:53 -04:00
resolvedIp ,
resolvedPort ,
2026-02-12 22:28:13 -06:00
null ,
userId ,
false ,
2026-06-21 15:55:41 -05:00
preloadedHostData ,
2026-02-12 22:28:13 -06:00
),
2025-11-05 10:36:16 -06:00
env : {
TERM : "xterm-256color" ,
LANG : "en_US.UTF-8" ,
LC_ALL : "en_US.UTF-8" ,
LC_CTYPE : "en_US.UTF-8" ,
LC_MESSAGES : "en_US.UTF-8" ,
LC_MONETARY : "en_US.UTF-8" ,
LC_NUMERIC : "en_US.UTF-8" ,
LC_TIME : "en_US.UTF-8" ,
LC_COLLATE : "en_US.UTF-8" ,
COLORTERM : "truecolor" ,
},
2025-09-12 14:42:00 -05:00
algorithms : {
kex : [
2025-11-05 10:36:16 -06:00
"curve25519-sha256" ,
"curve25519-sha256@libssh.org" ,
"ecdh-sha2-nistp521" ,
"ecdh-sha2-nistp384" ,
"ecdh-sha2-nistp256" ,
"diffie-hellman-group-exchange-sha256" ,
2025-09-12 14:42:00 -05:00
"diffie-hellman-group14-sha256" ,
"diffie-hellman-group14-sha1" ,
"diffie-hellman-group-exchange-sha1" ,
2025-11-05 10:36:16 -06:00
"diffie-hellman-group1-sha1" ,
],
serverHostKey : [
"ssh-ed25519" ,
"ecdsa-sha2-nistp521" ,
"ecdsa-sha2-nistp384" ,
"ecdsa-sha2-nistp256" ,
"rsa-sha2-512" ,
"rsa-sha2-256" ,
"ssh-rsa" ,
"ssh-dss" ,
2025-09-12 14:42:00 -05:00
],
2026-04-22 16:55:23 -05:00
cipher : SSH_ALGORITHMS.cipher ,
2025-10-01 15:40:10 -05:00
hmac : [
"hmac-sha2-512-etm@openssh.com" ,
2025-11-05 10:36:16 -06:00
"hmac-sha2-256-etm@openssh.com" ,
2025-10-01 15:40:10 -05:00
"hmac-sha2-512" ,
2025-11-05 10:36:16 -06:00
"hmac-sha2-256" ,
2025-10-01 15:40:10 -05:00
"hmac-sha1" ,
"hmac-md5" ,
],
2025-09-12 14:42:00 -05:00
compress : [ "none" , "zlib@openssh.com" , "zlib" ],
},
};
2025-10-01 15:40:10 -05:00
if (
resolvedCredentials . authType === "key" &&
resolvedCredentials . sshKey &&
resolvedCredentials . sshKey . trim ()
) {
2025-09-12 14:42:00 -05:00
try {
if (
! resolvedCredentials . sshKey . includes ( "-----BEGIN" ) ||
! resolvedCredentials . sshKey . includes ( "-----END" )
) {
throw new Error ( "Invalid private key format" );
}
const cleanKey = resolvedCredentials . sshKey
. trim ()
. replace ( /\r\n/g , "\n" )
. replace ( /\r/g , "\n" );
config . privateKey = Buffer . from ( cleanKey , "utf8" );
if ( resolvedCredentials . keyPassword )
config . passphrase = resolvedCredentials . keyPassword ;
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"info" ,
"sftp_auth" ,
"Using SSH key authentication" ,
),
);
2025-09-12 14:42:00 -05:00
} catch ( keyError ) {
fileLogger . error ( "SSH key format error for file manager" , {
operation : "file_connect" ,
sessionId ,
hostId ,
error : keyError.message ,
});
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"error" ,
"sftp_auth" ,
`Invalid SSH key format: ${ keyError . message } ` ,
),
);
return res
. status ( 400 )
. json ({ error : "Invalid SSH key format" , connectionLogs });
2025-08-07 02:20:27 -05:00
}
2025-11-05 10:36:16 -06:00
} else if ( resolvedCredentials . authType === "password" ) {
2026-05-12 21:55:14 -05:00
if ( ! resolvedCredentials . password ) {
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"error" ,
"sftp_auth" ,
"Password required for password authentication" ,
),
);
return res . status ( 400 ). json ({
error : "Password required for password authentication" ,
connectionLogs ,
});
2025-11-05 10:36:16 -06:00
}
2025-12-31 22:20:12 -06:00
config . password = resolvedCredentials . password ;
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog ( "info" , "sftp_auth" , "Using password authentication" ),
);
2026-02-12 22:28:13 -06:00
} else if ( resolvedCredentials . authType === "opkssh" ) {
try {
const { getOPKSSHToken } = await import ( "./opkssh-auth.js" );
const token = await getOPKSSHToken ( userId , hostId );
if ( ! token ) {
connectionLogs . push (
createConnectionLog (
"error" ,
"sftp_auth" ,
"OPKSSH authentication required. Please open a Terminal connection to this host first to complete browser-based authentication. Your session will be cached for 24 hours." ,
),
);
return res . status ( 401 ). json ({
error :
"OPKSSH authentication required. Please open a Terminal connection to this host first to complete browser-based authentication. Your session will be cached for 24 hours." ,
requiresOPKSSHAuth : true ,
connectionLogs ,
});
}
2026-04-22 16:55:23 -05:00
const { setupOPKSSHCertAuth } = await import ( "./opkssh-cert-auth.js" );
await setupOPKSSHCertAuth (
config as import ( "ssh2" ). ConnectConfig ,
client ,
token ,
username ,
);
2026-02-12 22:28:13 -06:00
connectionLogs . push (
createConnectionLog (
"info" ,
"sftp_auth" ,
"Using OPKSSH certificate authentication" ,
),
);
} catch ( opksshError ) {
fileLogger . error ( "OPKSSH authentication error for file manager" , {
operation : "file_connect" ,
sessionId ,
hostId ,
error :
opksshError instanceof Error ? opksshError . message : "Unknown error" ,
});
connectionLogs . push (
createConnectionLog (
"error" ,
"sftp_auth" ,
`OPKSSH authentication failed: ${ opksshError instanceof Error ? opksshError . message : "Unknown error" } ` ,
),
);
return res . status ( 500 ). json ({
error : "OPKSSH authentication failed" ,
connectionLogs ,
});
}
2026-06-16 15:59:53 -05:00
} else if (
resolvedCredentials . authType === "none" ||
2026-06-21 15:55:41 -05:00
resolvedCredentials . authType === "tailscale" ||
resolvedCredentials . authType === "warpgate"
2026-06-16 15:59:53 -05:00
) {
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"info" ,
"sftp_auth" ,
"Using keyboard-interactive authentication" ,
),
);
2025-09-12 14:42:00 -05:00
} else {
2025-10-01 15:40:10 -05:00
fileLogger . warn (
"No valid authentication method provided for file manager" ,
{
operation : "file_connect" ,
sessionId ,
hostId ,
authType : resolvedCredentials.authType ,
hasPassword : !! resolvedCredentials . password ,
hasKey : !! resolvedCredentials . sshKey ,
},
);
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"error" ,
"sftp_auth" ,
"No valid authentication method provided" ,
),
);
return res . status ( 400 ). json ({
error : "Either password or SSH key must be provided" ,
connectionLogs ,
});
2025-09-12 14:42:00 -05:00
}
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
let responseSent = false ;
2025-08-07 02:20:27 -05:00
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog ( "info" , "dns" , `Resolving DNS for ${ ip } ` ),
);
connectionLogs . push (
createConnectionLog ( "info" , "tcp" , `Connecting to ${ ip } : ${ port } ` ),
);
connectionLogs . push (
createConnectionLog ( "info" , "handshake" , "Initiating SSH handshake" ),
);
connectionLogs . push (
createConnectionLog (
"info" ,
"sftp_connecting" ,
"Establishing SSH connection..." ,
),
);
2025-09-12 14:42:00 -05:00
client . on ( "ready" , () => {
if ( responseSent ) return ;
responseSent = true ;
2026-02-12 22:28:13 -06:00
fileLogger . info ( "File manager SSH connection established" , {
operation : "file_ssh_connected" ,
sessionId ,
userId ,
hostId ,
ip ,
port ,
username ,
});
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"success" ,
"connected" ,
"SSH connection established successfully" ,
),
);
connectionLogs . push (
createConnectionLog (
"success" ,
"sftp_connected" ,
"SFTP session established successfully" ,
),
);
2025-09-12 14:42:00 -05:00
sshSessions [ sessionId ] = {
client ,
isConnected : true ,
lastActive : Date.now (),
2025-11-17 09:46:05 -06:00
activeOperations : 0 ,
2026-05-06 15:12:07 -05:00
channelOpener : new ChannelOpenSerializer (),
2026-04-22 16:55:23 -05:00
userId ,
2026-06-04 15:16:53 -04:00
ip ,
port ,
hostId ,
username ,
2026-05-28 22:29:20 -05:00
sudoPassword : resolvedCredentials.sudoPassword ,
2026-06-21 15:55:41 -05:00
scpLegacy : resolvedScpLegacy ,
2025-08-17 22:57:25 -05:00
};
2025-10-01 15:40:10 -05:00
scheduleSessionCleanup ( sessionId );
2026-01-24 19:49:42 -06:00
res . json ({
status : "success" ,
message : "SSH connection established" ,
connectionLogs ,
});
2025-11-05 10:36:16 -06:00
if ( hostId && userId ) {
( async () => {
try {
2026-03-14 20:05:05 -05:00
const hostResults = await SimpleDBOps . select (
2025-11-05 10:36:16 -06:00
getDb ()
. select ()
2026-03-14 20:05:05 -05:00
. from ( hosts )
. where ( and ( eq ( hosts . id , hostId ), eq ( hosts . userId , userId ))),
2025-11-05 10:36:16 -06:00
"ssh_data" ,
userId ,
);
const hostName =
2026-03-14 20:05:05 -05:00
hostResults . length > 0 && hostResults [ 0 ]. name
? hostResults [ 0 ]. name
2025-11-05 10:36:16 -06:00
: ` ${ username } @ ${ ip } : ${ port } ` ;
const authManager = AuthManager . getInstance ();
await axios . post (
"http://localhost:30006/activity/log" ,
{
type : "file_manager" ,
hostId ,
hostName ,
},
{
headers : {
Authorization : `Bearer ${ await authManager . generateJWTToken ( userId ) } ` ,
},
},
);
} catch ( error ) {
fileLogger . warn ( "Failed to log file manager activity" , {
operation : "activity_log_error" ,
userId ,
hostId ,
error : error instanceof Error ? error . message : "Unknown error" ,
});
}
})();
}
2025-09-12 14:42:00 -05:00
});
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
client . on ( "error" , ( err ) => {
if ( responseSent ) return ;
responseSent = true ;
fileLogger . error ( "SSH connection failed for file manager" , {
operation : "file_connect" ,
sessionId ,
hostId ,
ip ,
port ,
username ,
error : err.message ,
});
2025-11-05 10:36:16 -06:00
2026-05-06 15:12:07 -05:00
let errorStage : ConnectionStage ;
2026-01-24 19:49:42 -06:00
if (
err . message . includes ( "ENOTFOUND" ) ||
err . message . includes ( "getaddrinfo" )
) {
errorStage = "dns" ;
connectionLogs . push (
createConnectionLog (
"error" ,
errorStage ,
`DNS resolution failed: ${ err . message } ` ,
2026-03-08 18:02:14 -05:00
{
errorCode : ( err as unknown as Record < string , unknown >). code ,
errorLevel : ( err as unknown as Record < string , unknown >). level ,
},
2026-01-24 19:49:42 -06:00
),
);
} else if (
err . message . includes ( "ECONNREFUSED" ) ||
err . message . includes ( "ETIMEDOUT" )
) {
errorStage = "tcp" ;
connectionLogs . push (
createConnectionLog (
"error" ,
errorStage ,
`TCP connection failed: ${ err . message } ` ,
2026-03-08 18:02:14 -05:00
{
errorCode : ( err as unknown as Record < string , unknown >). code ,
errorLevel : ( err as unknown as Record < string , unknown >). level ,
},
2026-01-24 19:49:42 -06:00
),
);
} else if (
err . message . includes ( "handshake" ) ||
err . message . includes ( "key exchange" )
) {
errorStage = "handshake" ;
connectionLogs . push (
createConnectionLog (
"error" ,
errorStage ,
`SSH handshake failed: ${ err . message } ` ,
2026-03-08 18:02:14 -05:00
{
errorCode : ( err as unknown as Record < string , unknown >). code ,
errorLevel : ( err as unknown as Record < string , unknown >). level ,
},
2026-01-24 19:49:42 -06:00
),
);
} else if (
err . message . includes ( "authentication" ) ||
err . message . includes ( "Authentication" )
) {
errorStage = "auth" ;
connectionLogs . push (
createConnectionLog (
"error" ,
errorStage ,
`Authentication failed: ${ err . message } ` ,
2026-03-08 18:02:14 -05:00
{
errorCode : ( err as unknown as Record < string , unknown >). code ,
errorLevel : ( err as unknown as Record < string , unknown >). level ,
},
2026-01-24 19:49:42 -06:00
),
);
2026-02-12 22:28:13 -06:00
} else if ( err . message . includes ( "verification failed" )) {
errorStage = "handshake" ;
connectionLogs . push (
createConnectionLog (
"error" ,
errorStage ,
`SSH host key has changed. For security, please open a Terminal connection to this host first to verify and accept the new key fingerprint.` ,
2026-03-08 18:02:14 -05:00
{
errorCode : ( err as unknown as Record < string , unknown >). code ,
errorLevel : ( err as unknown as Record < string , unknown >). level ,
},
2026-02-12 22:28:13 -06:00
),
);
2026-01-24 19:49:42 -06:00
} else {
connectionLogs . push (
createConnectionLog (
"error" ,
"error" ,
`SSH connection failed: ${ err . message } ` ,
2026-03-08 18:02:14 -05:00
{
errorCode : ( err as unknown as Record < string , unknown >). code ,
errorLevel : ( err as unknown as Record < string , unknown >). level ,
},
2026-01-24 19:49:42 -06:00
),
);
}
2025-11-05 10:36:16 -06:00
if (
2026-06-21 15:55:41 -05:00
err . message . includes ( "Cannot parse privateKey" ) &&
err . message . includes ( "no passphrase" )
) {
res . json ({
status : "passphrase_required" ,
connectionLogs ,
});
} else if (
2026-06-16 15:59:53 -05:00
( resolvedCredentials . authType === "none" ||
resolvedCredentials . authType === "tailscale" ) &&
2025-11-05 10:36:16 -06:00
( err . message . includes ( "authentication" ) ||
err . message . includes ( "All configured authentication methods failed" ))
) {
res . json ({
status : "auth_required" ,
reason : "no_keyboard" ,
2026-01-24 19:49:42 -06:00
connectionLogs ,
2025-11-05 10:36:16 -06:00
});
} else {
2026-01-24 19:49:42 -06:00
res
. status ( 500 )
. json ({ status : "error" , message : err.message , connectionLogs });
2025-11-05 10:36:16 -06:00
}
2025-09-12 14:42:00 -05:00
});
client . on ( "close" , () => {
2026-02-12 22:28:13 -06:00
fileLogger . info ( "File manager SSH connection closed" , {
operation : "file_ssh_disconnected" ,
sessionId ,
userId ,
hostId ,
});
2025-09-12 14:42:00 -05:00
if ( sshSessions [ sessionId ]) sshSessions [ sessionId ]. isConnected = false ;
cleanupSession ( sessionId );
});
2025-11-05 10:36:16 -06:00
client . on (
"keyboard-interactive" ,
(
name : string ,
instructions : string ,
instructionsLang : string ,
prompts : Array < { prompt : string ; echo : boolean } > ,
finish : ( responses : string []) => void ,
) => {
const promptTexts = prompts . map (( p ) => p . prompt );
2026-01-24 19:49:42 -06:00
const warpgatePattern = /warpgate\s+authentication/i ;
const isWarpgate =
warpgatePattern . test ( name ) ||
warpgatePattern . test ( instructions ) ||
promptTexts . some (( p ) => warpgatePattern . test ( p ));
if ( isWarpgate ) {
const fullText = ` ${ name } \ n ${ instructions } \ n ${ promptTexts . join ( "\n" ) } ` ;
const urlMatch = fullText . match ( /https?:\/\/[^\s\n]+/i );
const keyMatch = fullText . match (
/security key[:\s]+([a-z0-9](?:\s+[a-z0-9]){3}|[a-z0-9]{4})/i ,
);
if ( urlMatch ) {
if ( responseSent ) return ;
responseSent = true ;
connectionLogs . push (
createConnectionLog (
"info" ,
"sftp_auth" ,
"Warpgate authentication required" ,
{ url : urlMatch [ 0 ] },
),
);
pendingTOTPSessions [ sessionId ] = {
client ,
finish ,
config ,
createdAt : Date.now (),
sessionId ,
hostId ,
ip ,
port ,
username ,
userId ,
prompts ,
totpPromptIndex : - 1 ,
resolvedPassword : resolvedCredentials.password ,
totpAttempts : 0 ,
isWarpgate : true ,
};
res . json ({
requires_warpgate : true ,
sessionId ,
url : urlMatch [ 0 ],
securityKey : keyMatch ? keyMatch [ 1 ] : "N/A" ,
connectionLogs ,
});
return ;
}
}
2025-11-05 10:36:16 -06:00
const totpPromptIndex = prompts . findIndex (( p ) =>
/verification code|verification_code|token|otp|2fa|authenticator|google.*auth/i . test (
p . prompt ,
),
);
if ( totpPromptIndex !== - 1 ) {
if ( responseSent ) {
const responses = prompts . map (( p ) => {
if ( /password/i . test ( p . prompt ) && resolvedCredentials . password ) {
return resolvedCredentials . password ;
}
return "" ;
});
finish ( responses );
return ;
}
responseSent = true ;
if ( pendingTOTPSessions [ sessionId ]) {
const responses = prompts . map (( p ) => {
if ( /password/i . test ( p . prompt ) && resolvedCredentials . password ) {
return resolvedCredentials . password ;
}
return "" ;
});
finish ( responses );
return ;
}
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"info" ,
"sftp_auth" ,
"TOTP verification required" ,
{ prompt : prompts [ totpPromptIndex ]. prompt },
),
);
2025-11-05 10:36:16 -06:00
pendingTOTPSessions [ sessionId ] = {
client ,
finish ,
config ,
createdAt : Date.now (),
sessionId ,
hostId ,
ip ,
port ,
username ,
userId ,
prompts ,
totpPromptIndex ,
resolvedPassword : resolvedCredentials.password ,
2025-12-31 22:20:12 -06:00
totpAttempts : 0 ,
2025-11-05 10:36:16 -06:00
};
res . json ({
requires_totp : true ,
sessionId ,
prompt : prompts [ totpPromptIndex ]. prompt ,
2026-01-24 19:49:42 -06:00
connectionLogs ,
2025-11-05 10:36:16 -06:00
});
} else {
const hasStoredPassword =
resolvedCredentials . password &&
2026-06-16 15:59:53 -05:00
resolvedCredentials . authType !== "none" &&
2026-06-21 15:55:41 -05:00
resolvedCredentials . authType !== "tailscale" &&
resolvedCredentials . authType !== "warpgate" ;
2025-11-05 10:36:16 -06:00
const passwordPromptIndex = prompts . findIndex (( p ) =>
/password/i . test ( p . prompt ),
);
2026-06-21 15:55:41 -05:00
if ( resolvedCredentials . authType === "warpgate" ) {
finish ( prompts . map (() => "" ));
return ;
}
2025-11-05 10:36:16 -06:00
if (
2026-06-16 15:59:53 -05:00
( resolvedCredentials . authType === "none" ||
resolvedCredentials . authType === "tailscale" ) &&
2025-11-05 10:36:16 -06:00
passwordPromptIndex !== - 1
) {
if ( responseSent ) return ;
responseSent = true ;
client . end ();
res . json ({
status : "auth_required" ,
reason : "no_keyboard" ,
});
return ;
}
if ( ! hasStoredPassword && passwordPromptIndex !== - 1 ) {
if ( responseSent ) {
const responses = prompts . map (( p ) => {
if ( /password/i . test ( p . prompt ) && resolvedCredentials . password ) {
return resolvedCredentials . password ;
}
return "" ;
});
finish ( responses );
return ;
}
responseSent = true ;
if ( pendingTOTPSessions [ sessionId ]) {
const responses = prompts . map (( p ) => {
if ( /password/i . test ( p . prompt ) && resolvedCredentials . password ) {
return resolvedCredentials . password ;
}
return "" ;
});
finish ( responses );
return ;
}
pendingTOTPSessions [ sessionId ] = {
client ,
finish ,
config ,
createdAt : Date.now (),
sessionId ,
hostId ,
ip ,
port ,
username ,
userId ,
prompts ,
totpPromptIndex : passwordPromptIndex ,
resolvedPassword : resolvedCredentials.password ,
2025-12-31 22:20:12 -06:00
totpAttempts : 0 ,
2025-11-05 10:36:16 -06:00
};
res . json ({
requires_totp : true ,
sessionId ,
prompt : prompts [ passwordPromptIndex ]. prompt ,
isPassword : true ,
});
return ;
}
const responses = prompts . map (( p ) => {
if ( /password/i . test ( p . prompt ) && resolvedCredentials . password ) {
return resolvedCredentials . password ;
}
return "" ;
});
finish ( responses );
}
},
);
2026-03-08 18:02:14 -05:00
const proxyConfig : SOCKS5Config | null =
2026-06-21 15:55:41 -05:00
resolvedUseSocks5 &&
( resolvedSocks5Host ||
( resolvedSocks5ProxyChain &&
( resolvedSocks5ProxyChain as ProxyNode []). length > 0 ))
2026-03-08 18:02:14 -05:00
? {
2026-06-21 15:55:41 -05:00
useSocks5 : resolvedUseSocks5 ,
socks5Host : resolvedSocks5Host ,
socks5Port : resolvedSocks5Port ,
socks5Username : resolvedSocks5Username ,
socks5Password : resolvedSocks5Password ,
socks5ProxyChain : resolvedSocks5ProxyChain as ProxyNode [],
2026-03-08 18:02:14 -05:00
}
: null ;
2025-12-31 22:20:12 -06:00
2026-06-16 15:59:53 -05:00
const hasJumpHosts =
resolvedJumpHosts && resolvedJumpHosts . length > 0 && userId ;
2026-03-08 18:02:14 -05:00
if ( hasJumpHosts ) {
try {
if ( proxyConfig ) {
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
2026-03-08 18:02:14 -05:00
"info" ,
2026-01-24 19:49:42 -06:00
"proxy" ,
2026-03-08 18:02:14 -05:00
"Connecting via proxy + jump hosts" ,
2026-01-24 19:49:42 -06:00
),
);
2025-12-31 22:20:12 -06:00
}
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"info" ,
"jump" ,
2026-06-16 15:59:53 -05:00
`Connecting via ${ resolvedJumpHosts . length } jump host(s)` ,
2026-01-24 19:49:42 -06:00
),
);
2026-03-08 18:02:14 -05:00
const jumpClient = await createJumpHostChain (
2026-06-16 15:59:53 -05:00
resolvedJumpHosts ,
2026-03-08 18:02:14 -05:00
userId ,
proxyConfig ,
);
2025-11-17 09:46:05 -06:00
if ( ! jumpClient ) {
fileLogger . error ( "Failed to establish jump host chain" , {
operation : "file_jump_chain" ,
sessionId ,
hostId ,
});
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"error" ,
"jump" ,
"Failed to establish jump host chain" ,
),
);
return res . status ( 500 ). json ({
error : "Failed to connect through jump hosts" ,
connectionLogs ,
});
2025-11-17 09:46:05 -06:00
}
2026-06-21 15:55:41 -05:00
let forwardOutDone = false ;
const forwardOutTimeout = setTimeout (() => {
if ( ! forwardOutDone ) {
forwardOutDone = true ;
fileLogger . error ( "Timeout waiting for jump host forwardOut" , {
operation : "file_jump_forward_timeout" ,
sessionId ,
hostId ,
ip ,
port ,
});
connectionLogs . push (
createConnectionLog (
"error" ,
"jump" ,
"Timed out waiting for jump host tunnel to target host" ,
),
);
jumpClient . end ();
res . status ( 500 ). json ({
error : "Jump host tunnel timed out" ,
connectionLogs ,
});
}
}, 30000 );
2025-11-17 09:46:05 -06:00
jumpClient . forwardOut ( "127.0.0.1" , 0 , ip , port , ( err , stream ) => {
2026-06-21 15:55:41 -05:00
if ( forwardOutDone ) return ;
forwardOutDone = true ;
clearTimeout ( forwardOutTimeout );
2025-11-17 09:46:05 -06:00
if ( err ) {
fileLogger . error ( "Failed to forward through jump host" , err , {
operation : "file_jump_forward" ,
sessionId ,
hostId ,
ip ,
port ,
});
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"error" ,
"jump" ,
`Failed to forward through jump host: ${ err . message } ` ,
),
);
2025-11-17 09:46:05 -06:00
jumpClient . end ();
return res . status ( 500 ). json ({
error : "Failed to forward through jump host: " + err . message ,
2026-01-24 19:49:42 -06:00
connectionLogs ,
2025-11-17 09:46:05 -06:00
});
}
config . sock = stream ;
client . connect ( config );
});
} catch ( error ) {
fileLogger . error ( "Jump host error" , error , {
operation : "file_jump_host" ,
sessionId ,
hostId ,
});
2026-01-24 19:49:42 -06:00
connectionLogs . push (
createConnectionLog (
"error" ,
"jump" ,
`Jump host error: ${ error instanceof Error ? error . message : "Unknown error" } ` ,
),
);
return res . status ( 500 ). json ({
error : "Failed to connect through jump hosts" ,
connectionLogs ,
});
2025-11-17 09:46:05 -06:00
}
2026-03-08 18:02:14 -05:00
} else if ( proxyConfig ) {
connectionLogs . push (
createConnectionLog ( "info" , "proxy" , "Connecting via proxy" , {
proxyHost : socks5Host ,
proxyPort : socks5Port || 1080 ,
}),
);
try {
const proxySocket = await createSocks5Connection ( ip , port , proxyConfig );
if ( proxySocket ) {
connectionLogs . push (
createConnectionLog (
"success" ,
"proxy" ,
"Proxy connected successfully" ,
),
);
config . sock = proxySocket ;
}
client . connect ( config );
} catch ( proxyError ) {
fileLogger . error ( "Proxy connection failed" , proxyError , {
operation : "proxy_connect" ,
sessionId ,
hostId ,
proxyHost : socks5Host ,
proxyPort : socks5Port || 1080 ,
});
connectionLogs . push (
createConnectionLog (
"error" ,
"proxy" ,
`Proxy connection failed: ${ proxyError instanceof Error ? proxyError . message : "Unknown error" } ` ,
),
);
return res . status ( 500 ). json ({
error :
"Proxy connection failed: " +
( proxyError instanceof Error ? proxyError . message : "Unknown error" ),
connectionLogs ,
});
}
2025-11-17 09:46:05 -06:00
} else {
client . connect ( config );
}
2025-09-12 14:42:00 -05:00
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/connect-totp:
* post:
* summary: Verify TOTP and complete connection
* description: Verifies the TOTP code and completes the SSH connection for file manager.
* tags:
* - File Manager
* responses:
* 200:
* description: TOTP verified, SSH connection established.
* 400:
* description: Session ID and TOTP code required.
* 401:
* description: Invalid TOTP code or authentication required.
* 404:
* description: TOTP session expired.
* 408:
* description: TOTP session timeout.
*/
2025-11-05 10:36:16 -06:00
app . post ( "/ssh/file_manager/ssh/connect-totp" , async ( req , res ) => {
const { sessionId , totpCode } = req . body ;
const userId = ( req as AuthenticatedRequest ). userId ;
if ( ! userId ) {
fileLogger . error ( "TOTP verification rejected: no authenticated user" , {
operation : "file_totp_auth" ,
sessionId ,
});
return res . status ( 401 ). json ({ error : "Authentication required" });
}
if ( ! sessionId || ! totpCode ) {
return res . status ( 400 ). json ({ error : "Session ID and TOTP code required" });
}
const session = pendingTOTPSessions [ sessionId ];
if ( ! session ) {
fileLogger . warn ( "TOTP session not found or expired" , {
operation : "file_totp_verify" ,
sessionId ,
userId ,
availableSessions : Object.keys ( pendingTOTPSessions ),
});
return res
. status ( 404 )
. json ({ error : "TOTP session expired. Please reconnect." });
}
if ( Date . now () - session . createdAt > 180000 ) {
delete pendingTOTPSessions [ sessionId ];
try {
session . client . end ();
2026-03-08 18:02:14 -05:00
} catch {
// expected
}
2025-11-05 10:36:16 -06:00
fileLogger . warn ( "TOTP session timeout before code submission" , {
operation : "file_totp_verify" ,
sessionId ,
userId ,
age : Date.now () - session . createdAt ,
});
return res
. status ( 408 )
. json ({ error : "TOTP session timeout. Please reconnect." });
}
const responses = ( session . prompts || []). map (( p , index ) => {
if ( index === session . totpPromptIndex ) {
return totpCode ;
}
if ( /password/i . test ( p . prompt ) && session . resolvedPassword ) {
return session . resolvedPassword ;
}
return "" ;
});
let responseSent = false ;
session . client . once ( "ready" , () => {
if ( responseSent ) return ;
responseSent = true ;
clearTimeout ( responseTimeout );
delete pendingTOTPSessions [ sessionId ];
setTimeout (() => {
sshSessions [ sessionId ] = {
client : session.client ,
isConnected : true ,
lastActive : Date.now (),
2025-11-17 09:46:05 -06:00
activeOperations : 0 ,
2026-05-06 15:12:07 -05:00
channelOpener : new ChannelOpenSerializer (),
2026-04-22 16:55:23 -05:00
userId ,
2026-06-04 15:16:53 -04:00
ip : session.ip ,
port : session.port ,
hostId : session.hostId ,
username : session.username ,
2025-11-05 10:36:16 -06:00
};
scheduleSessionCleanup ( sessionId );
res . json ({
status : "success" ,
message : "TOTP verified, SSH connection established" ,
});
if ( session . hostId && session . userId ) {
( async () => {
try {
2026-03-14 20:05:05 -05:00
const hostResults = await SimpleDBOps . select (
2025-11-05 10:36:16 -06:00
getDb ()
. select ()
2026-03-14 20:05:05 -05:00
. from ( hosts )
2025-11-05 10:36:16 -06:00
. where (
and (
2026-03-14 20:05:05 -05:00
eq ( hosts . id , session . hostId ! ),
eq ( hosts . userId , session . userId ! ),
2025-11-05 10:36:16 -06:00
),
),
"ssh_data" ,
session . userId ! ,
);
const hostName =
2026-03-14 20:05:05 -05:00
hostResults . length > 0 && hostResults [ 0 ]. name
? hostResults [ 0 ]. name
2025-11-05 10:36:16 -06:00
: ` ${ session . username } @ ${ session . ip } : ${ session . port } ` ;
const authManager = AuthManager . getInstance ();
await axios . post (
"http://localhost:30006/activity/log" ,
{
type : "file_manager" ,
hostId : session.hostId ,
hostName ,
},
{
headers : {
Authorization : `Bearer ${ await authManager . generateJWTToken ( session . userId ! ) } ` ,
},
},
);
} catch ( error ) {
fileLogger . warn ( "Failed to log file manager activity (TOTP)" , {
operation : "activity_log_error" ,
userId : session.userId ,
hostId : session.hostId ,
error : error instanceof Error ? error . message : "Unknown error" ,
});
}
})();
}
}, 200 );
});
session . client . once ( "error" , ( err ) => {
if ( responseSent ) return ;
responseSent = true ;
clearTimeout ( responseTimeout );
delete pendingTOTPSessions [ sessionId ];
fileLogger . error ( "TOTP verification failed" , {
operation : "file_totp_verify" ,
sessionId ,
userId ,
error : err.message ,
});
res . status ( 401 ). json ({ status : "error" , message : "Invalid TOTP code" });
});
2026-03-08 18:02:14 -05:00
const responseTimeout = setTimeout (() => {
2025-11-05 10:36:16 -06:00
if ( ! responseSent ) {
responseSent = true ;
delete pendingTOTPSessions [ sessionId ];
fileLogger . warn ( "TOTP verification timeout" , {
operation : "file_totp_verify" ,
sessionId ,
userId ,
});
res . status ( 408 ). json ({ error : "TOTP verification timeout" });
}
}, 60000 );
session . finish ( responses );
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/connect-warpgate:
* post:
* summary: Complete Warpgate authentication
* description: Submits empty response to complete Warpgate authentication after user completes browser auth.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - sessionId
* properties:
* sessionId:
* type: string
* description: Session ID from initial connection attempt
* responses:
* 200:
* description: Warpgate authentication completed successfully.
* 401:
* description: Authentication failed or unauthorized.
* 404:
* description: Warpgate session expired.
* 408:
* description: Warpgate session timeout.
*/
app . post ( "/ssh/file_manager/ssh/connect-warpgate" , async ( req , res ) => {
const { sessionId } = req . body ;
const userId = ( req as AuthenticatedRequest ). userId ;
if ( ! userId ) {
fileLogger . error ( "Warpgate verification rejected: no authenticated user" , {
operation : "file_warpgate_auth" ,
sessionId ,
});
return res . status ( 401 ). json ({ error : "Authentication required" });
}
if ( ! sessionId ) {
return res . status ( 400 ). json ({ error : "Session ID required" });
}
const session = pendingTOTPSessions [ sessionId ];
if ( ! session ) {
fileLogger . warn ( "Warpgate session not found or expired" , {
operation : "file_warpgate_verify" ,
sessionId ,
userId ,
availableSessions : Object.keys ( pendingTOTPSessions ),
});
return res
. status ( 404 )
. json ({ error : "Warpgate session expired. Please reconnect." });
}
if ( ! session . isWarpgate ) {
return res . status ( 400 ). json ({ error : "Session is not a Warpgate session" });
}
if ( Date . now () - session . createdAt > 300000 ) {
delete pendingTOTPSessions [ sessionId ];
try {
session . client . end ();
2026-03-08 18:02:14 -05:00
} catch {
// expected
}
2026-01-24 19:49:42 -06:00
fileLogger . warn ( "Warpgate session timeout before completion" , {
operation : "file_warpgate_verify" ,
sessionId ,
userId ,
age : Date.now () - session . createdAt ,
});
return res
. status ( 408 )
. json ({ error : "Warpgate session timeout. Please reconnect." });
}
let responseSent = false ;
2026-03-08 18:02:14 -05:00
const responseTimeout = setTimeout (() => {
if ( ! responseSent ) {
responseSent = true ;
delete pendingTOTPSessions [ sessionId ];
fileLogger . warn ( "Warpgate verification timeout" , {
operation : "file_warpgate_verify" ,
sessionId ,
userId ,
});
res . status ( 408 ). json ({ error : "Warpgate verification timeout" });
}
}, 60000 );
2026-01-24 19:49:42 -06:00
session . client . once ( "ready" , () => {
if ( responseSent ) return ;
responseSent = true ;
clearTimeout ( responseTimeout );
delete pendingTOTPSessions [ sessionId ];
setTimeout (() => {
sshSessions [ sessionId ] = {
client : session.client ,
isConnected : true ,
lastActive : Date.now (),
activeOperations : 0 ,
2026-05-06 15:12:07 -05:00
channelOpener : new ChannelOpenSerializer (),
2026-04-22 16:55:23 -05:00
userId ,
2026-06-04 15:16:53 -04:00
ip : session.ip ,
port : session.port ,
hostId : session.hostId ,
username : session.username ,
2026-01-24 19:49:42 -06:00
};
scheduleSessionCleanup ( sessionId );
res . json ({
status : "success" ,
message : "Warpgate verified, SSH connection established" ,
});
if ( session . hostId && session . userId ) {
( async () => {
try {
2026-03-14 20:05:05 -05:00
const hostResults = await SimpleDBOps . select (
2026-01-24 19:49:42 -06:00
getDb ()
. select ()
2026-03-14 20:05:05 -05:00
. from ( hosts )
2026-01-24 19:49:42 -06:00
. where (
and (
2026-03-14 20:05:05 -05:00
eq ( hosts . id , session . hostId ! ),
eq ( hosts . userId , session . userId ! ),
2026-01-24 19:49:42 -06:00
),
),
"ssh_data" ,
session . userId ! ,
);
const hostName =
2026-03-14 20:05:05 -05:00
hostResults . length > 0 && hostResults [ 0 ]. name
? hostResults [ 0 ]. name
2026-01-24 19:49:42 -06:00
: ` ${ session . username } @ ${ session . ip } : ${ session . port } ` ;
await axios . post (
"http://localhost:30006/activity/log" ,
{
type : "file_manager" ,
hostId : session.hostId ,
hostName ,
},
{
headers : {
Authorization : `Bearer ${ await authManager . generateJWTToken ( session . userId ! ) } ` ,
},
},
);
} catch ( error ) {
fileLogger . warn ( "Failed to log file manager activity" , {
operation : "activity_log_error" ,
userId : session.userId ,
hostId : session.hostId ,
error : error instanceof Error ? error . message : "Unknown error" ,
});
}
})();
}
}, 200 );
});
session . client . once ( "error" , ( err ) => {
if ( responseSent ) return ;
responseSent = true ;
clearTimeout ( responseTimeout );
delete pendingTOTPSessions [ sessionId ];
fileLogger . error ( "Warpgate verification failed" , {
operation : "file_warpgate_verify" ,
sessionId ,
userId ,
error : err.message ,
});
res
. status ( 401 )
. json ({ status : "error" , message : "Warpgate authentication failed" });
});
session . finish ([ "" ]);
});
/**
* @openapi
* /ssh/file_manager/ssh/disconnect:
* post:
* summary: Disconnect from SSH
* description: Closes an active SSH connection for file manager.
* tags:
* - File Manager
* responses:
* 200:
* description: SSH connection disconnected.
*/
2025-09-12 14:42:00 -05:00
app . post ( "/ssh/file_manager/ssh/disconnect" , ( req , res ) => {
const { sessionId } = req . body ;
2026-02-12 22:28:13 -06:00
const userId = ( req as AuthenticatedRequest ). userId ;
2026-05-06 15:12:07 -05:00
const session = sshSessions [ sessionId ];
if ( session && ! verifySessionOwnership ( session , userId )) {
return res . status ( 403 ). json ({ error : "Session access denied" });
}
2026-02-12 22:28:13 -06:00
fileLogger . info ( "File manager disconnection requested" , {
operation : "file_disconnect_request" ,
sessionId ,
userId ,
});
2025-09-12 14:42:00 -05:00
cleanupSession ( sessionId );
res . json ({ status : "success" , message : "SSH connection disconnected" });
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/sudo-password:
* post:
* summary: Set sudo password for session
* description: Stores sudo password temporarily in session for elevated operations.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* password:
* type: string
* responses:
* 200:
* description: Sudo password set successfully.
* 400:
* description: Invalid session.
*/
app . post ( "/ssh/file_manager/sudo-password" , ( req , res ) => {
const { sessionId , password } = req . body ;
2026-04-22 16:55:23 -05:00
const userId = ( req as AuthenticatedRequest ). userId ;
2026-01-24 19:49:42 -06:00
const session = sshSessions [ sessionId ];
if ( ! session || ! session . isConnected ) {
return res . status ( 400 ). json ({ error : "Invalid or disconnected session" });
}
2026-04-22 16:55:23 -05:00
if ( ! verifySessionOwnership ( session , userId )) {
return res . status ( 403 ). json ({ error : "Session access denied" });
}
2026-01-24 19:49:42 -06:00
session . sudoPassword = password ;
session . lastActive = Date . now ();
res . json ({ status : "success" , message : "Sudo password set" });
});
/**
* @openapi
* /ssh/file_manager/ssh/status:
* get:
* summary: Get SSH connection status
* description: Checks the status of an SSH connection for file manager.
* tags:
* - File Manager
* parameters:
* - in: query
* name: sessionId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: SSH connection status.
*/
2025-09-12 14:42:00 -05:00
app . get ( "/ssh/file_manager/ssh/status" , ( req , res ) => {
const sessionId = req . query . sessionId as string ;
2026-04-22 16:55:23 -05:00
const userId = ( req as AuthenticatedRequest ). userId ;
const session = sshSessions [ sessionId ];
if ( session && ! verifySessionOwnership ( session , userId )) {
return res . status ( 403 ). json ({ error : "Session access denied" });
}
const isConnected = !! session ? . isConnected ;
2025-09-12 14:42:00 -05:00
res . json ({ status : "success" , connected : isConnected });
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/keepalive:
* post:
* summary: Keep SSH session alive
* description: Keeps an active SSH session for file manager alive.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* responses:
* 200:
* description: Session keepalive successful.
* 400:
* description: Session ID is required or session not found.
*/
2026-05-06 15:12:07 -05:00
app . post ( "/ssh/file_manager/ssh/keepalive" , async ( req , res ) => {
2025-10-01 15:40:10 -05:00
const { sessionId } = req . body ;
2026-04-22 16:55:23 -05:00
const userId = ( req as AuthenticatedRequest ). userId ;
2025-10-01 15:40:10 -05:00
if ( ! sessionId ) {
return res . status ( 400 ). json ({ error : "Session ID is required" });
}
const session = sshSessions [ sessionId ];
if ( ! session || ! session . isConnected ) {
return res . status ( 400 ). json ({
error : "SSH session not found or not connected" ,
connected : false ,
});
}
2026-04-22 16:55:23 -05:00
if ( ! verifySessionOwnership ( session , userId )) {
return res . status ( 403 ). json ({ error : "Session access denied" });
}
2025-10-01 15:40:10 -05:00
session . lastActive = Date . now ();
scheduleSessionCleanup ( sessionId );
2026-05-06 15:12:07 -05:00
// Probe the cached SFTP channel. If stale, clear it so the next operation
// opens a fresh one via the serialized getSessionSftp.
if ( session . sftp && ! session . sftpPending ) {
try {
await new Promise < void >(( resolve , reject ) => {
session . sftp ! . stat ( "/" , ( err ) => ( err ? reject ( err ) : resolve ()));
});
} catch {
session . sftp = undefined ;
}
}
2025-10-01 15:40:10 -05:00
res . json ({
status : "success" ,
connected : true ,
message : "Session keepalive successful" ,
lastActive : session.lastActive ,
});
});
2026-06-04 15:16:53 -04:00
registerFileListingRoutes ( app , {
sshSessions ,
activeListRequests ,
verifySessionOwnership ,
2025-10-01 15:40:10 -05:00
});
2026-06-04 15:16:53 -04:00
registerFileContentRoutes ( app , {
sshSessions ,
verifySessionOwnership ,
2025-09-12 14:42:00 -05:00
});
2026-06-04 15:16:53 -04:00
registerFileOperationRoutes ( app , {
sshSessions ,
verifySessionOwnership ,
2026-03-08 18:02:14 -05:00
});
2026-06-04 15:16:53 -04:00
registerFileDownloadRoutes ( app , {
sshSessions ,
scheduleSessionCleanup ,
verifySessionOwnership ,
2025-09-12 14:42:00 -05:00
});
2026-06-04 15:16:53 -04:00
registerFileActionRoutes ( app , {
sshSessions ,
scheduleSessionCleanup ,
verifySessionOwnership ,
2025-11-17 09:46:05 -06:00
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/extractArchive:
* post:
* summary: Extract archive file
* description: Extracts an archive file (.tar, .tar.gz, .tgz, .zip, .tar.bz2, .tbz2, .tar.xz, .txz) to a specified or default location on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - sessionId
* - archivePath
* properties:
* sessionId:
* type: string
* description: SSH session ID
* archivePath:
* type: string
* description: Path to the archive file on remote host
* extractPath:
* type: string
* description: Optional custom extraction path (defaults to same directory as archive)
* responses:
* 200:
* description: Archive extracted successfully.
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* example: success
* message:
* type: string
* extractPath:
* type: string
* 400:
* description: Missing required parameters, SSH connection not established, or unsupported archive format.
* 403:
* description: Permission denied.
* 500:
* description: Failed to extract archive.
*/
2025-11-17 09:46:05 -06:00
app . post ( "/ssh/file_manager/ssh/extractArchive" , async ( req , res ) => {
const { sessionId , archivePath , extractPath } = req . body ;
2026-05-06 15:12:07 -05:00
const userId = ( req as AuthenticatedRequest ). userId ;
2025-11-17 09:46:05 -06:00
if ( ! sessionId || ! archivePath ) {
return res . status ( 400 ). json ({ error : "Missing required parameters" });
}
const session = sshSessions [ sessionId ];
if ( ! session || ! session . isConnected ) {
return res . status ( 400 ). json ({ error : "SSH session not connected" });
}
2026-05-06 15:12:07 -05:00
if ( ! verifySessionOwnership ( session , userId )) {
return res . status ( 403 ). json ({ error : "Session access denied" });
}
2025-11-17 09:46:05 -06:00
session . lastActive = Date . now ();
scheduleSessionCleanup ( sessionId );
const fileName = archivePath . split ( "/" ). pop () || "" ;
const fileExt = fileName . toLowerCase ();
2026-05-06 15:12:07 -05:00
let extractCommand : string ;
2025-11-17 09:46:05 -06:00
const targetPath =
extractPath || archivePath . substring ( 0 , archivePath . lastIndexOf ( "/" ));
2026-04-22 16:55:23 -05:00
const escapedArchive = archivePath . replace ( /'/g , "'\"'\"'" );
const escapedTarget = targetPath . replace ( /'/g , "'\"'\"'" );
const escapedDecompressed = archivePath
. replace ( /\.gz$/ , "" )
. replace ( /'/g , "'\"'\"'" );
2025-11-17 09:46:05 -06:00
if ( fileExt . endsWith ( ".tar.gz" ) || fileExt . endsWith ( ".tgz" )) {
2026-04-22 16:55:23 -05:00
extractCommand = `tar -xzf ' ${ escapedArchive } ' -C ' ${ escapedTarget } '` ;
2025-11-17 09:46:05 -06:00
} else if ( fileExt . endsWith ( ".tar.bz2" ) || fileExt . endsWith ( ".tbz2" )) {
2026-04-22 16:55:23 -05:00
extractCommand = `tar -xjf ' ${ escapedArchive } ' -C ' ${ escapedTarget } '` ;
2025-11-17 09:46:05 -06:00
} else if ( fileExt . endsWith ( ".tar.xz" )) {
2026-04-22 16:55:23 -05:00
extractCommand = `tar -xJf ' ${ escapedArchive } ' -C ' ${ escapedTarget } '` ;
2025-11-17 09:46:05 -06:00
} else if ( fileExt . endsWith ( ".tar" )) {
2026-04-22 16:55:23 -05:00
extractCommand = `tar -xf ' ${ escapedArchive } ' -C ' ${ escapedTarget } '` ;
2025-11-17 09:46:05 -06:00
} else if ( fileExt . endsWith ( ".zip" )) {
2026-04-22 16:55:23 -05:00
extractCommand = `unzip -o ' ${ escapedArchive } ' -d ' ${ escapedTarget } '` ;
2025-11-17 09:46:05 -06:00
} else if ( fileExt . endsWith ( ".gz" ) && ! fileExt . endsWith ( ".tar.gz" )) {
2026-04-22 16:55:23 -05:00
extractCommand = `gunzip -c ' ${ escapedArchive } ' > ' ${ escapedDecompressed } '` ;
2025-11-17 09:46:05 -06:00
} else if ( fileExt . endsWith ( ".bz2" ) && ! fileExt . endsWith ( ".tar.bz2" )) {
2026-04-22 16:55:23 -05:00
extractCommand = `bunzip2 -k ' ${ escapedArchive } '` ;
2025-11-17 09:46:05 -06:00
} else if ( fileExt . endsWith ( ".xz" ) && ! fileExt . endsWith ( ".tar.xz" )) {
2026-04-22 16:55:23 -05:00
extractCommand = `unxz -k ' ${ escapedArchive } '` ;
2025-11-17 09:46:05 -06:00
} else if ( fileExt . endsWith ( ".7z" )) {
2026-04-22 16:55:23 -05:00
extractCommand = `7z x ' ${ escapedArchive } ' -o' ${ escapedTarget } '` ;
2025-11-17 09:46:05 -06:00
} else if ( fileExt . endsWith ( ".rar" )) {
2026-04-22 16:55:23 -05:00
extractCommand = `unrar x ' ${ escapedArchive } ' ' ${ escapedTarget } /'` ;
2025-11-17 09:46:05 -06:00
} else {
return res . status ( 400 ). json ({ error : "Unsupported archive format" });
}
fileLogger . info ( "Extracting archive" , {
operation : "extract_archive" ,
sessionId ,
archivePath ,
extractPath : targetPath ,
command : extractCommand ,
});
2026-05-06 15:12:07 -05:00
execChannel ( session , extractCommand , ( err , stream ) => {
2025-11-17 09:46:05 -06:00
if ( err ) {
fileLogger . error ( "SSH exec error during extract:" , err , {
operation : "extract_archive" ,
sessionId ,
archivePath ,
});
return res
. status ( 500 )
. json ({ error : "Failed to execute extract command" });
}
let errorOutput = "" ;
2026-03-08 18:02:14 -05:00
stream . on ( "data" , () => {
/* consume stdout */
});
2025-11-17 09:46:05 -06:00
stream . stderr . on ( "data" , ( data : Buffer ) => {
errorOutput += data . toString ();
});
stream . on ( "close" , ( code : number ) => {
if ( code !== 0 ) {
fileLogger . error ( "Extract command failed" , {
operation : "extract_archive" ,
sessionId ,
archivePath ,
exitCode : code ,
error : errorOutput ,
});
let friendlyError = errorOutput || "Failed to extract archive" ;
if (
errorOutput . includes ( "command not found" ) ||
errorOutput . includes ( "not found" )
) {
let missingCmd = "" ;
let installHint = "" ;
if ( fileExt . endsWith ( ".zip" )) {
missingCmd = "unzip" ;
installHint =
"apt install unzip / yum install unzip / brew install unzip" ;
} else if (
fileExt . endsWith ( ".tar.gz" ) ||
fileExt . endsWith ( ".tgz" ) ||
fileExt . endsWith ( ".tar.bz2" ) ||
fileExt . endsWith ( ".tbz2" ) ||
fileExt . endsWith ( ".tar.xz" ) ||
fileExt . endsWith ( ".tar" )
) {
missingCmd = "tar" ;
installHint = "Usually pre-installed on Linux/Unix systems" ;
} else if ( fileExt . endsWith ( ".gz" )) {
missingCmd = "gunzip" ;
installHint =
"apt install gzip / yum install gzip / Usually pre-installed" ;
} else if ( fileExt . endsWith ( ".bz2" )) {
missingCmd = "bunzip2" ;
installHint =
"apt install bzip2 / yum install bzip2 / brew install bzip2" ;
} else if ( fileExt . endsWith ( ".xz" )) {
missingCmd = "unxz" ;
installHint =
"apt install xz-utils / yum install xz / brew install xz" ;
} else if ( fileExt . endsWith ( ".7z" )) {
missingCmd = "7z" ;
installHint =
"apt install p7zip-full / yum install p7zip / brew install p7zip" ;
} else if ( fileExt . endsWith ( ".rar" )) {
missingCmd = "unrar" ;
installHint =
"apt install unrar / yum install unrar / brew install unrar" ;
}
if ( missingCmd ) {
friendlyError = `Command ' ${ missingCmd } ' not found on remote server. Please install it first: ${ installHint } ` ;
}
}
return res . status ( 500 ). json ({ error : friendlyError });
}
fileLogger . success ( "Archive extracted successfully" , {
operation : "extract_archive" ,
sessionId ,
archivePath ,
extractPath : targetPath ,
});
res . json ({
success : true ,
message : "Archive extracted successfully" ,
extractPath : targetPath ,
});
});
stream . on ( "error" , ( streamErr ) => {
fileLogger . error ( "SSH extractArchive stream error:" , streamErr , {
operation : "extract_archive" ,
sessionId ,
archivePath ,
});
if ( ! res . headersSent ) {
res
. status ( 500 )
. json ({ error : "Stream error while extracting archive" });
}
});
});
});
2026-01-24 19:49:42 -06:00
/**
* @openapi
* /ssh/file_manager/ssh/compressFiles:
* post:
* summary: Compress files
* description: Compresses files and/or directories on the remote host.
* tags:
* - File Manager
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* sessionId:
* type: string
* paths:
* type: array
* items:
* type: string
* archiveName:
* type: string
* format:
* type: string
* responses:
* 200:
* description: Files compressed successfully.
* 400:
* description: Missing required parameters or unsupported compression format.
* 500:
* description: Failed to compress files.
*/
2025-11-17 09:46:05 -06:00
app . post ( "/ssh/file_manager/ssh/compressFiles" , async ( req , res ) => {
const { sessionId , paths , archiveName , format } = req . body ;
2026-05-06 15:12:07 -05:00
const userId = ( req as AuthenticatedRequest ). userId ;
2025-11-17 09:46:05 -06:00
if (
! sessionId ||
! paths ||
! Array . isArray ( paths ) ||
paths . length === 0 ||
! archiveName
) {
return res . status ( 400 ). json ({ error : "Missing required parameters" });
}
const session = sshSessions [ sessionId ];
if ( ! session || ! session . isConnected ) {
return res . status ( 400 ). json ({ error : "SSH session not connected" });
}
2026-05-06 15:12:07 -05:00
if ( ! verifySessionOwnership ( session , userId )) {
return res . status ( 403 ). json ({ error : "Session access denied" });
}
2025-11-17 09:46:05 -06:00
session . lastActive = Date . now ();
scheduleSessionCleanup ( sessionId );
const compressionFormat = format || "zip" ;
2026-05-06 15:12:07 -05:00
let compressCommand : string ;
2025-11-17 09:46:05 -06:00
const firstPath = paths [ 0 ];
const workingDir = firstPath . substring ( 0 , firstPath . lastIndexOf ( "/" )) || "/" ;
2026-04-22 16:55:23 -05:00
const escapeShell = ( s : string ) => s . replace ( /'/g , "'\"'\"'" );
2025-11-17 09:46:05 -06:00
const fileNames = paths
. map (( p ) => {
const name = p . split ( "/" ). pop ();
2026-06-04 15:16:53 -04:00
return `'./ ${ escapeShell ( name || "" ) } '` ;
2025-11-17 09:46:05 -06:00
})
. join ( " " );
let archivePath = "" ;
if ( archiveName . includes ( "/" )) {
archivePath = archiveName ;
} else {
archivePath = workingDir . endsWith ( "/" )
? ` ${ workingDir }${ archiveName } `
: ` ${ workingDir } / ${ archiveName } ` ;
}
2026-04-22 16:55:23 -05:00
const escapedDir = escapeShell ( workingDir );
const escapedArchive = escapeShell ( archivePath );
2025-11-17 09:46:05 -06:00
if ( compressionFormat === "zip" ) {
2026-06-04 15:16:53 -04:00
compressCommand = `cd ' ${ escapedDir } ' && zip -r ' ${ escapedArchive } ' -- ${ fileNames } ` ;
2025-11-17 09:46:05 -06:00
} else if ( compressionFormat === "tar.gz" || compressionFormat === "tgz" ) {
2026-06-04 15:16:53 -04:00
compressCommand = `cd ' ${ escapedDir } ' && tar -czf ' ${ escapedArchive } ' -- ${ fileNames } ` ;
2025-11-17 09:46:05 -06:00
} else if ( compressionFormat === "tar.bz2" || compressionFormat === "tbz2" ) {
2026-06-04 15:16:53 -04:00
compressCommand = `cd ' ${ escapedDir } ' && tar -cjf ' ${ escapedArchive } ' -- ${ fileNames } ` ;
2025-11-17 09:46:05 -06:00
} else if ( compressionFormat === "tar.xz" ) {
2026-06-04 15:16:53 -04:00
compressCommand = `cd ' ${ escapedDir } ' && tar -cJf ' ${ escapedArchive } ' -- ${ fileNames } ` ;
2025-11-17 09:46:05 -06:00
} else if ( compressionFormat === "tar" ) {
2026-06-04 15:16:53 -04:00
compressCommand = `cd ' ${ escapedDir } ' && tar -cf ' ${ escapedArchive } ' -- ${ fileNames } ` ;
2025-11-17 09:46:05 -06:00
} else if ( compressionFormat === "7z" ) {
2026-06-04 15:16:53 -04:00
compressCommand = `cd ' ${ escapedDir } ' && 7z a ' ${ escapedArchive } ' -- ${ fileNames } ` ;
2025-11-17 09:46:05 -06:00
} else {
return res . status ( 400 ). json ({ error : "Unsupported compression format" });
}
fileLogger . info ( "Compressing files" , {
operation : "compress_files" ,
sessionId ,
paths ,
archivePath ,
format : compressionFormat ,
command : compressCommand ,
});
2026-05-06 15:12:07 -05:00
execChannel ( session , compressCommand , ( err , stream ) => {
2025-11-17 09:46:05 -06:00
if ( err ) {
fileLogger . error ( "SSH exec error during compress:" , err , {
operation : "compress_files" ,
sessionId ,
paths ,
});
return res
. status ( 500 )
. json ({ error : "Failed to execute compress command" });
}
let errorOutput = "" ;
2026-03-08 18:02:14 -05:00
stream . on ( "data" , () => {
/* consume stdout */
});
2025-11-17 09:46:05 -06:00
stream . stderr . on ( "data" , ( data : Buffer ) => {
errorOutput += data . toString ();
});
stream . on ( "close" , ( code : number ) => {
if ( code !== 0 ) {
fileLogger . error ( "Compress command failed" , {
operation : "compress_files" ,
sessionId ,
paths ,
archivePath ,
exitCode : code ,
error : errorOutput ,
});
let friendlyError = errorOutput || "Failed to compress files" ;
if (
errorOutput . includes ( "command not found" ) ||
errorOutput . includes ( "not found" )
) {
const commandMap : Record < string , { cmd : string ; install : string }> = {
zip : {
cmd : "zip" ,
install : "apt install zip / yum install zip / brew install zip" ,
},
"tar.gz" : {
cmd : "tar" ,
install : "Usually pre-installed on Linux/Unix systems" ,
},
"tar.bz2" : {
cmd : "tar" ,
install : "Usually pre-installed on Linux/Unix systems" ,
},
"tar.xz" : {
cmd : "tar" ,
install : "Usually pre-installed on Linux/Unix systems" ,
},
tar : {
cmd : "tar" ,
install : "Usually pre-installed on Linux/Unix systems" ,
},
"7z" : {
cmd : "7z" ,
install :
"apt install p7zip-full / yum install p7zip / brew install p7zip" ,
},
};
const info = commandMap [ compressionFormat ];
if ( info ) {
friendlyError = `Command ' ${ info . cmd } ' not found on remote server. Please install it first: ${ info . install } ` ;
}
}
return res . status ( 500 ). json ({ error : friendlyError });
}
fileLogger . success ( "Files compressed successfully" , {
operation : "compress_files" ,
sessionId ,
paths ,
archivePath ,
format : compressionFormat ,
});
res . json ({
success : true ,
message : "Files compressed successfully" ,
archivePath : archivePath ,
});
});
stream . on ( "error" , ( streamErr ) => {
fileLogger . error ( "SSH compressFiles stream error:" , streamErr , {
operation : "compress_files" ,
sessionId ,
paths ,
});
if ( ! res . headersSent ) {
res . status ( 500 ). json ({ error : "Stream error while compressing files" });
}
});
});
});
2026-06-04 15:16:53 -04:00
const hostTransferDeps : HostTransferDeps = {
sshSessions ,
getSessionSftp ,
execChannel ,
verifySessionOwnership ,
openDedicatedTransferSession ,
closeDedicatedTransferSession ,
};
app . post ( "/ssh/file_manager/ssh/transferMethodPreview" , async ( req , res ) => {
const {
sourceSessionId ,
destSessionId ,
sourcePaths ,
destPath ,
methodPreference ,
} = req . body ;
const userId = ( req as AuthenticatedRequest ). userId ;
if (
! sourceSessionId ||
! destSessionId ||
! destPath ||
! sourcePaths ||
! Array . isArray ( sourcePaths ) ||
sourcePaths . length === 0
) {
return res . status ( 400 ). json ({ error : "Missing required parameters" });
}
try {
const preview = await previewArchiveTransferMethod ( hostTransferDeps , {
sourceSessionId ,
destSessionId ,
sourcePaths ,
destPath ,
methodPreference :
methodPreference === "tar" || methodPreference === "item_sftp"
? methodPreference
: "auto" ,
userId ,
});
res . json ( preview );
} catch ( err ) {
fileLogger . error ( "Failed to preview transfer method" , err , {
operation : "host_transfer" ,
sourceSessionId ,
destSessionId ,
sourcePaths ,
});
res . status ( 500 ). json ({
error : err instanceof Error ? err . message : "Failed to preview method" ,
});
}
});
app . post ( "/ssh/file_manager/ssh/transferToHost" , async ( req , res ) => {
const {
sourceSessionId ,
sourcePaths ,
destSessionId ,
destPath ,
move ,
methodPreference ,
parallelSegmentCount : parallelSegmentCountRaw ,
} = req . body ;
const userId = ( req as AuthenticatedRequest ). userId ;
if (
! sourceSessionId ||
! destSessionId ||
! destPath ||
! sourcePaths ||
! Array . isArray ( sourcePaths ) ||
sourcePaths . length === 0
) {
return res . status ( 400 ). json ({ error : "Missing required parameters" });
}
const sourceSession = sshSessions [ sourceSessionId ];
const destSession = sshSessions [ destSessionId ];
if ( ! sourceSession ? . isConnected || ! destSession ? . isConnected ) {
return res . status ( 400 ). json ({ error : "SSH session not connected" });
}
if (
! verifySessionOwnership ( sourceSession , userId ) ||
! verifySessionOwnership ( destSession , userId )
) {
return res . status ( 403 ). json ({ error : "Session access denied" });
}
sourceSession . lastActive = Date . now ();
destSession . lastActive = Date . now ();
scheduleSessionCleanup ( sourceSessionId );
scheduleSessionCleanup ( destSessionId );
try {
const rawParallel = Number ( parallelSegmentCountRaw );
const parallelSegmentCount = Number . isFinite ( rawParallel )
? Math . max ( 1 , Math . min ( 8 , Math . floor ( rawParallel )))
: 2 ;
const { transferId } = startHostTransfer ( hostTransferDeps , {
sourceSessionId ,
sourcePaths ,
destSessionId ,
destPath ,
move : !! move ,
userId ,
methodPreference :
methodPreference === "tar" || methodPreference === "item_sftp"
? methodPreference
: "auto" ,
parallelSegmentCount ,
});
res . json ({ transferId });
} catch ( err ) {
fileLogger . error ( "Failed to start host transfer" , err , {
operation : "host_transfer" ,
sourceSessionId ,
destSessionId ,
sourcePaths ,
});
res . status ( 500 ). json ({ error : "Failed to start transfer" });
}
});
app . get ( "/ssh/file_manager/ssh/activeTransfers" , async ( req , res ) => {
const userId = ( req as unknown as AuthenticatedRequest ). userId ;
if ( ! userId ) {
return res . status ( 401 ). json ({ error : "Authentication required" });
}
await probeHungStreamTransfers ( hostTransferDeps );
res . json ({ transfers : listActiveTransfers ( userId ) });
});
app . get (
"/ssh/file_manager/ssh/transferStatus/:transferId" ,
async ( req , res ) => {
const userId = ( req as unknown as AuthenticatedRequest ). userId ;
const transferId = req . params . transferId ;
if ( ! userId ) {
return res . status ( 401 ). json ({ error : "Authentication required" });
}
await probeHungStreamTransfers ( hostTransferDeps );
const status = getTransferStatus ( transferId , userId );
if ( ! status ) {
return res . status ( 404 ). json ({ error : "Transfer not found" });
}
res . json ( status );
},
);
app . post ( "/ssh/file_manager/ssh/transferCancel/:transferId" , ( req , res ) => {
const userId = ( req as unknown as AuthenticatedRequest ). userId ;
const transferId = req . params . transferId ;
const cancelled = requestTransferCancel ( transferId , userId );
if ( ! cancelled ) {
return res . status ( 404 ). json ({ error : "Transfer not found or not running" });
}
res . json ({ ok : true });
});
app . post (
"/ssh/file_manager/ssh/transferCleanup/:transferId" ,
async ( req , res ) => {
const userId = ( req as unknown as AuthenticatedRequest ). userId ;
const transferId = req . params . transferId ;
if ( ! userId ) {
return res . status ( 401 ). json ({ error : "Authentication required" });
}
try {
const result = await cleanupCancelledTransfer (
hostTransferDeps ,
transferId ,
userId ,
);
res . json ( result );
} catch ( err ) {
const message =
err instanceof Error ? err . message : "Failed to clean up transfer" ;
const status = message === "Transfer not found" ? 404 : 400 ;
res . status ( status ). json ({ error : message });
}
},
);
app . post ( "/ssh/file_manager/ssh/transferRetry/:transferId" , ( req , res ) => {
const userId = ( req as unknown as AuthenticatedRequest ). userId ;
const transferId = req . params . transferId ;
if ( ! userId ) {
return res . status ( 401 ). json ({ error : "Authentication required" });
}
const retried = retryHostTransfer ( hostTransferDeps , transferId , userId );
if ( ! retried ) {
return res
. status ( 404 )
. json ({ error : "Transfer not found or not retryable" });
}
res . json ({ ok : true , transferId });
});
2025-09-12 14:42:00 -05:00
process . on ( "SIGINT" , () => {
Object . keys ( sshSessions ). forEach ( cleanupSession );
process . exit ( 0 );
});
process . on ( "SIGTERM" , () => {
Object . keys ( sshSessions ). forEach ( cleanupSession );
process . exit ( 0 );
2025-08-07 02:20:27 -05:00
});
2025-10-01 15:40:10 -05:00
const PORT = 30004 ;
try {
const server = app . listen ( PORT , async () => {
try {
await authManager . initialize ();
} catch ( err ) {
fileLogger . error ( "Failed to initialize AuthManager" , err , {
operation : "auth_init_error" ,
});
}
});
server . on ( "error" , ( err ) => {
fileLogger . error ( "File Manager server error" , err , {
operation : "file_manager_server_error" ,
port : PORT ,
});
});
} catch ( err ) {
fileLogger . error ( "Failed to start File Manager server" , err , {
operation : "file_manager_server_start_failed" ,
2025-09-12 14:42:00 -05:00
port : PORT ,
});
2025-10-01 15:40:10 -05:00
}