/** * peardock server entry point. * * HyperDHT listener + protomux-rpc API for remote Docker management. * Clients connect with the server public key printed at startup. */ import DHT from 'hyperdht' import b4a from 'b4a' import gracefulGoodbye from 'graceful-goodbye' import { loadOrCreateKeyPair } from './core/keys.js' import { initAuthKeys } from './core/auth-keys.js' import { peers } from './core/peer-registry.js' import { PeerSession } from './rpc/session.js' import { registerAllHandlers, cleanupSession } from './rpc/register.js' import { startDockerEventStream, stopDockerEventStream } from './services/events.js' import { startStatsBroadcast, stopStatsBroadcast } from './services/stats.js' import { isPeerRevoked } from './core/peer-policy.js' import { recordPeerConnect, recordPeerDisconnect } from './services/metrics.js' import { isInsecureOpenAdmin } from '../shared/crypto-auth.js' import { closeConnectionInvites } from './core/connection-invites.js' import { closeAllTunnels, isHolesailEnabled, holesailStatus, restoreTunnelsFromDisk, } from './services/holesail-tunnels.js' import { restoreSchedules } from './services/schedules.js' import { startAlerts, stopAlerts } from './services/alerts.js' import { docker } from './services/docker.js' import logger from './utils/logger.js' import { isSwarmEnabled } from './handlers/swarm.js' import { isPluginsEnabled } from './handlers/plugins.js' const bootStarted = Date.now() const log = logger.child('server') /** @type {{ keyPair: any, publicKeyHex: string, seedHex: string }} */ let keyPair let publicKeyHex let seedHex try { ;({ keyPair, publicKeyHex, seedHex } = loadOrCreateKeyPair()) initAuthKeys({ seedHex, publicKeyHex }) } catch (err) { log.error('Failed to load server keypair', { error: err.message }) process.exit(1) } if (isInsecureOpenAdmin()) { log.warn( 'PEARDOCK_INSECURE_OPEN_ADMIN=1 — every peer is admin. Disable for production multi-operator use.' ) } const dht = new DHT() const server = dht.createServer() server.on('connection', (socket) => { const peerId = socket.remotePublicKey ? b4a.toString(socket.remotePublicKey, 'hex') : null if (peerId && isPeerRevoked(peerId)) { log.warn('Rejected revoked peer', { peerId: peerId.slice(0, 12) }) try { socket.destroy() } catch { // ignore } return } const session = new PeerSession(socket, { serverPublicKey: keyPair.publicKey, onClose: (s) => { cleanupSession(s) peers.remove(s.id) recordPeerDisconnect() log.info('Peer disconnected', { peerId: s.id.slice(0, 12), role: s.role, peers: peers.size, }) }, }) registerAllHandlers(session) peers.add(session) recordPeerConnect() log.info('Peer connected', { peerId: session.id.slice(0, 12), role: session.role, peers: peers.size, }) }) server.on('error', (err) => { log.error('HyperDHT server error', { error: err.message, code: err.code }) }) /** * Probe Docker socket for the boot banner. * @returns {Promise<{ ok: boolean, apiVersion?: string, os?: string, error?: string }>} */ async function probeDocker() { try { const version = await docker.version() return { ok: true, apiVersion: version.ApiVersion || version.apiVersion || undefined, os: version.Os || version.os || undefined, } } catch (err) { return { ok: false, error: err.message || String(err) } } } await server.listen(keyPair) const dockerStatus = await probeDocker() const hs = isHolesailEnabled() ? holesailStatus() : null const bootMs = Date.now() - bootStarted logger.banner([ 'peardock server ready', '', 'Public key (viewer / read-only by default):', publicKeyHex, '', 'Admin: paste public key + SERVER_SEED in the client.', 'Operators: create a pd1. invite from Access (never share SERVER_SEED).', '', `Docker: ${dockerStatus.ok ? `ok · API ${dockerStatus.apiVersion || '?'} · ${dockerStatus.os || '?'}` : `unavailable · ${dockerStatus.error || 'socket error'}`}`, `Holesail: ${ !isHolesailEnabled() ? 'off (ENABLE_HOLESAIL=0)' : hs?.available ? `on · max ${hs.max} tunnels` : `on but package missing${hs?.loadError ? ` · ${String(hs.loadError).slice(0, 80)}` : ''}` }`, `Auth: HMAC capabilities · default role viewer${isInsecureOpenAdmin() ? ' · INSECURE OPEN ADMIN' : ''}`, `Swarm RPC: ${isSwarmEnabled() ? 'on' : 'off'} · Plugins: ${isPluginsEnabled() ? 'on' : 'off'}`, `Log: ${logger.format} · level ${['error', 'warn', 'info', 'debug'][logger.level] || 'info'}`, `Boot ${bootMs}ms · pid ${process.pid} · Node ${process.version}`, ]) log.info('Listening on HyperDHT', { publicKey: publicKeyHex.slice(0, 16) + '…', docker: dockerStatus.ok, bootMs, }) if (!dockerStatus.ok) { log.warn('Docker socket not reachable — RPC that needs Docker will fail until dockerd is up', { error: dockerStatus.error, }) } startDockerEventStream() startStatsBroadcast() try { startAlerts() } catch (err) { log.warn('Alerts engine failed to start', { error: err.message }) } if (isHolesailEnabled()) { restoreTunnelsFromDisk() .then((r) => { if (r?.restored || r?.failed) { log.info('Restored Holesail tunnels', r) } }) .catch((err) => { log.warn('Tunnel restore failed', { error: err.message }) }) } try { restoreSchedules() log.debug('Schedules restored') } catch (err) { log.warn('Schedule restore failed', { error: err.message }) } let shuttingDown = false async function shutdown(signal = 'shutdown') { if (shuttingDown) return shuttingDown = true log.info('Shutting down', { signal, peers: peers.size, uptimeSec: Math.round(process.uptime()) }) stopStatsBroadcast() stopDockerEventStream() try { stopAlerts() } catch { // ignore } try { await closeAllTunnels() } catch (err) { log.warn('Error closing tunnels', { error: err.message }) } peers.clear() try { await closeConnectionInvites() } catch (err) { log.debug('invites.close', { error: err.message }) } try { await server.close() } catch (err) { log.debug('server.close', { error: err.message }) } try { await dht.destroy() } catch (err) { log.debug('dht.destroy', { error: err.message }) } log.info('Bye') // Allow log flush setTimeout(() => process.exit(0), 50).unref?.() } gracefulGoodbye(() => shutdown('goodbye')) process.on('SIGINT', () => shutdown('SIGINT')) process.on('SIGTERM', () => shutdown('SIGTERM')) process.on('uncaughtException', (err) => { log.error('Uncaught exception', { error: err.message, stack: err.stack }) shutdown('uncaughtException') }) process.on('unhandledRejection', (reason) => { const msg = reason instanceof Error ? reason.message : String(reason) const stack = reason instanceof Error ? reason.stack : undefined log.error('Unhandled rejection', { error: msg, stack }) })