#!/usr/bin/env node import fs from 'node:fs'; import path from 'node:path'; import net from 'node:net'; import crypto from 'node:crypto'; import ssh2 from 'ssh2'; import { loadConfig, configPath } from './config.js'; import { color, clear, header, box } from './terminal.js'; import { createCommandRouter } from './commands.js'; import { configureTerminal, createTuiSession } from './tui.js'; const { Server } = ssh2; const config = loadConfig(); const sessions = new Set(); const notifications = new Set(); const hostKey = path.resolve(config.host.hostKeyPath || 'data/host.key'); if (!fs.existsSync(hostKey)) { fs.mkdirSync(path.dirname(hostKey), { recursive: true }); fs.writeFileSync(hostKey, crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }).privateKey.export({ type: 'pkcs1', format: 'pem' })); } function isAdmin(ctx) { return ctx.admin === true; } function count() { return sessions.size; } function sendNotification(message) { for (const socket of notifications) socket.write(JSON.stringify({ type: 'notification', message }) + '\n'); } function prompt(ctx) { return `${color(config.theme?.primary || 'cyan', 'portfolio')} ${isAdmin(ctx) ? color('yellow', '[admin] ') : ''}› `; } function renderHome(ctx) { const p = config.persona || {}; const width = Math.max(40, ctx.cols || 80); const title = `● ${p.name || 'Your Name'} — ${p.tagline || ''}`; const content = router.home(ctx).split('\n'); return `${clear}${header(title, `${count()} connected`, width, config.theme?.primary, config.theme?.muted)}\r\n${color(config.theme?.muted || 'brightBlack', p.location || '')}\r\n\r\n${box('Portfolio', content, width, config.theme)}\r\n\r\n`; } function renderPage(ctx, input, content = '') { const width = Math.max(40, ctx.cols || 80); const [command, argument] = input.split(/\s+/); const pageName = command === 'open' ? argument : command; const title = config.pages?.[pageName]?.title || (command ? command[0].toUpperCase() + command.slice(1) : 'Portfolio'); return `${clear}${header(`● ${config.persona?.name || 'Your Name'} — ${config.persona?.tagline || ''}`, `${count()} connected`, width, config.theme?.primary, config.theme?.muted)}\r\n${color(config.theme?.muted || 'brightBlack', config.persona?.location || '')}\r\n\r\n${box(title, String(content || '').split(/\r?\n/), width, config.theme)}\r\n\r\n`; } function refreshHeaders() { for (const s of sessions) s.tui?.updateHeader(header(`● ${config.persona?.name || 'Your Name'} — ${config.persona?.tagline || ''}`, `${count()} connected`, s.ctx.cols, config.theme?.primary, config.theme?.muted)); } const router = createCommandRouter({ config, isAdmin, ownerOnline: () => [...sessions].some(s => s.ctx.admin), sendNotification, reload: () => Object.assign(config, loadConfig()), broadcast: line => { for (const s of sessions) s.tui?.notify(color(config.theme?.accent || 'yellow', line)); } }); function handleSession(channel, ctx) { ctx.channel = channel; const active = { channel, ctx, tui: null }; sessions.add(active); refreshHeaders(); configureTerminal(channel, config.persona?.name || 'SSH Portfolio'); active.tui = createTuiSession({ channel, ctx, router, prompt, renderHome, renderPage, onClose: () => { sessions.delete(active); refreshHeaders(); } }); active.tui.showHome(); } const server = new Server({ hostKeys: [fs.readFileSync(hostKey)], ident: 'ssh-portfolio' }, client => { const ctx = { admin: false, channel: null, cols: 80, username: 'visitor' }; client.on('authentication', ctxAuth => { ctx.username = ctxAuth.username || 'visitor'; if (ctxAuth.method === 'none') { ctxAuth.accept(); return; } if (ctxAuth.method === 'publickey' && ctxAuth.key) { const key = ctxAuth.key.data.toString('base64'); const allowed = (config.adminPublicKeys || []).some(entry => String(entry).trim().split(/\s+/).includes(key)); if (allowed && (!ctxAuth.signature || ctxAuth.key.verify(ctxAuth.blob, ctxAuth.signature, ctxAuth.hashAlgo))) { ctx.admin = true; ctxAuth.accept(); return; } } ctxAuth.reject(['none', 'publickey']); }).on('ready', () => client.on('session', accept => { const sshSession = accept(); sshSession.on('pty', (acceptPty, rejectPty, info) => { ctx.cols = info.cols || 80; acceptPty(); sshSession.on('window-change', (acceptResize, rejectResize, resize) => { ctx.cols = resize.cols || ctx.cols; refreshHeaders(); }); }); sshSession.on('shell', (acceptShell) => handleSession(acceptShell(), ctx)); sshSession.on('exec', (acceptExec, rejectExec, info) => { const e = acceptExec(); e.write((router.execute(ctx, info.command) || '') + '\r\n'); e.exit(0); e.end(); }); })).on('error', () => {}); }); server.listen(config.host.port, '0.0.0.0', () => console.log(`SSH portfolio listening on port ${config.host.port}; config: ${configPath}`)); net.createServer(socket => { notifications.add(socket); socket.on('data', () => {}); socket.on('close', () => notifications.delete(socket)); }).listen(config.host.notificationPort, '127.0.0.1');