Files
peardata/server/server.js
T
Raven Scott c3b4240e47
CI / test (push) Successful in 1m2s
Release rolling / release (push) Has been cancelled
Usage — total data dir, Corestore, memory rings, warm HyperDB sample
Hot memory — 15m → 24h (or custom points)
Warm memory — downsampled ring + samples-per-bucket
Disk history — auto-rotate by age (1 day → 1 year, forever, or custom days)
Soft disk budget + max warm points
Auto prune on a configurable hour interval
Dry-run prune / Prune now / Save to agent (admin role)
2026-07-19 12:55:13 -04:00

258 lines
6.7 KiB
JavaScript

/**
* 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 }