/** * PearData / PearMonitor agent entry point. * * - HyperDHT listener + protomux-rpc (P2P control + metric streams) * - HyperDB (Corestore) for warm history + metadata * - Optional Hyperswarm replication (PEARDATA_SWARM=1) * - Optional agent-compatible REST API (default :18888) */ import os from 'os' 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 { isPeerRevoked, loadPeerPolicy } from './core/peer-policy.js' import { isInsecureOpenAdmin } from '../shared/crypto-auth.js' import { APP_NAME, APP_VERSION } from '../shared/protocol.js' import { openDb, closeDb, getDb } from './db/index.js' import { startReplication, stopReplication } from './db/replicate.js' import { startPipeline } from './pipeline.js' import { startRestServer } from './rest/http-server.js' import { getCollector } from './services/collector.js' import { flushWarmPending } from './services/warm-flush.js' import { loadRetentionConfig, startAutoPrune, stopAutoPrune } from './services/retention.js' import logger from './utils/logger.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 }) loadPeerPolicy() } catch (err) { log.error('Failed to load server keypair', { error: err.message }) process.exit(1) } if (isInsecureOpenAdmin()) { log.warn( 'PEARDATA_INSECURE_OPEN_ADMIN=1 — every peer is admin. Disable for production multi-operator use.' ) } const db = await openDb() if (db) { try { await db.putNode({ nodeId: publicKeyHex.slice(0, 16), hostname: os.hostname(), publicKeyHex, platform: os.platform(), arch: os.arch(), cpus: os.cpus().length, totalMemMiB: Math.floor(os.totalmem() / (1024 * 1024)), agentVersion: APP_VERSION, updatedAt: Date.now(), }) } catch (err) { log.warn('Failed to upsert local node record', { error: err.message }) } } await startReplication() loadRetentionConfig() startPipeline() startAutoPrune() const restServer = startRestServer() let restTunnelInfo = null try { const { startRestTunnel } = await import('./services/rest-tunnel.js') restTunnelInfo = await startRestTunnel() } catch (err) { log.warn('REST tunnel failed to start', { error: err.message }) } 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) log.info('Peer disconnected', { peerId: s.id.slice(0, 12), role: s.role, }) }, }) registerAllHandlers(session) peers.add(session) log.info('Peer connected', { peerId: session.id.slice(0, 12), role: session.role, connected: peers.size(), }) }) await server.listen(keyPair) const bootMs = Date.now() - bootStarted const restPort = Number(process.env.PEARDATA_REST_PORT) || 18888 const restHost = process.env.PEARDATA_REST_HOST || '127.0.0.1' const model = getDb() logger.banner({ title: `${APP_NAME} agent v${APP_VERSION}`, publicKey: publicKeyHex, connect: `Client → dial ${publicKeyHex}`, admin: 'Use SERVER_SEED as admin proof, or mint pd1 invites', rest: restServer ? `http://${restHost}:${restPort}/api/v3/info` : 'disabled', restTunnel: restTunnelInfo?.publicKeyHex ? `HyperDHT ${restTunnelInfo.publicKeyHex}` : 'off', hyperdb: model ? model.publicKeyHex : 'disabled', swarm: process.env.PEARDATA_SWARM === '1' ? 'on' : 'off', insecure: isInsecureOpenAdmin() ? 'OPEN ADMIN (dev)' : 'secure defaults', bootMs: `${bootMs}ms`, }) log.info('HyperDHT agent listening', { publicKeyHex }) async function shutdown() { log.info('Shutting down…') try { getCollector().stop() } catch { // ignore } try { const { getDockerCollector, isDockerCollectorEnabled } = await import( './services/collectors/docker.js' ) if (isDockerCollectorEnabled()) getDockerCollector().stop() } catch { // ignore } try { const { getProcessCollector, isProcessCollectorEnabled } = await import( './services/collectors/processes.js' ) if (isProcessCollectorEnabled()) getProcessCollector().stop() } catch { // ignore } try { const { getParentCollector, isParentEnabled } = await import( './services/collectors/parent.js' ) if (isParentEnabled()) getParentCollector().stop() } catch { // ignore } try { const { getNginxCollector, isNginxEnabled } = await import( './services/collectors/nginx.js' ) if (isNginxEnabled()) getNginxCollector().stop() } catch { // ignore } try { const { getRedisCollector, isRedisEnabled } = await import( './services/collectors/redis.js' ) if (isRedisEnabled()) getRedisCollector().stop() } catch { // ignore } try { const { getPostgresCollector, isPostgresEnabled } = await import( './services/collectors/postgres.js' ) if (isPostgresEnabled()) getPostgresCollector().stop() } catch { // ignore } try { const { getPearDockCollector, isPearDockEnabled } = await import( './services/collectors/peardock.js' ) if (isPearDockEnabled()) getPearDockCollector().stop() } catch { // ignore } try { stopAutoPrune() } catch { // ignore } try { await flushWarmPending() } catch { // ignore } for (const s of peers.list()) { try { s.destroy() } catch { // ignore } } if (restServer) { await new Promise((resolve) => restServer.close(() => resolve())) } try { const { stopRestTunnel } = await import('./services/rest-tunnel.js') await stopRestTunnel() } catch { // ignore } try { await server.close() } catch { // ignore } try { await dht.destroy() } catch { // ignore } await stopReplication() await closeDb() process.exit(0) } gracefulGoodbye(shutdown) process.on('SIGINT', shutdown) process.on('SIGTERM', shutdown) export { publicKeyHex, dht, server, restServer }