OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / core / cli / setup.js

499 lines · javascript ref:
/**
 * HiveRelay Interactive Setup Wizard (TUI)
 *
 * Guides the operator through configuring a relay node:
 *   - Product profile (relay-core / custody-relay / service-operator)
 *   - Resource limits (memory, storage, bandwidth, connections)
 *   - Optional service plugins
 *   - Transports (holesail, tor, websocket)
 *   - Network (API port, regions, bootstrap)
 *   - Seeding & registry settings
 *
 * Writes to ~/.hiverelay/config.json and prints a summary.
 */

import { saveConfig, ensureDirs } from '../config/loader.js'
import { homedir } from 'os'
import { join } from 'path'

let select
let confirm
let input
let checkbox
let number

async function loadPrompts () {
  if (select) return
  ({ select, confirm, input, checkbox, number } = await import('@inquirer/prompts'))
}

// ─── Node Profiles ──────────────────────────────────────────────────

const PROFILES = {
  'relay-core': {
    label: 'Relay Core — availability + custody kernel (recommended)',
    maxStorageBytes: 50 * 1024 * 1024 * 1024, // 50 GB
    maxConnections: 256,
    maxRelayBandwidthMbps: 100,
    services: [],
    config: {
      mode: 'relay-core',
      enableServices: false
    }
  },
  'custody-relay': {
    label: 'Custody Relay — blind atomic custody focus',
    maxStorageBytes: 50 * 1024 * 1024 * 1024, // 50 GB
    maxConnections: 256,
    maxRelayBandwidthMbps: 100,
    services: [],
    config: {
      mode: 'custody-relay',
      enableServices: false,
      strictSeedingPrivacy: true,
      custodyExpiryInterval: 60_000
    }
  },
  homehive: {
    label: 'HomeHive — private/local relay profile',
    maxStorageBytes: 10 * 1024 * 1024 * 1024, // 10 GB
    maxConnections: 32,
    maxRelayBandwidthMbps: 25,
    services: [],
    config: {
      mode: 'homehive',
      enableServices: false,
      discovery: { dht: true, announce: false, mdns: true },
      access: { open: false },
      pairing: { enabled: true },
      acceptMode: 'allowlist'
    }
  },
  'service-operator': {
    label: 'Service Operator — opt-in app services on top of core',
    maxStorageBytes: 50 * 1024 * 1024 * 1024, // 50 GB
    maxConnections: 256,
    maxRelayBandwidthMbps: 100,
    services: ['identity', 'storage', 'schema', 'vrf'],
    config: {
      mode: 'service-operator',
      enableServices: true
    }
  },
  'experimental-lab': {
    label: 'Experimental Lab — AI/ZK/SLA/arbitration plugin playground',
    maxStorageBytes: 200 * 1024 * 1024 * 1024, // 200 GB
    maxConnections: 1024,
    maxRelayBandwidthMbps: 500,
    services: ['identity', 'storage', 'schema', 'vrf', 'ai', 'zk', 'sla', 'arbitration'],
    config: {
      mode: 'experimental-lab',
      enableServices: true
    }
  },
  custom: {
    label: 'Custom — Configure everything manually'
  }
}

// All available services with descriptions
const ALL_SERVICES = [
  { name: 'identity  — plugin: identity and relay-local signing helpers', value: 'identity' },
  { name: 'storage   — plugin: service RPC storage helpers', value: 'storage' },
  { name: 'schema    — plugin: schema validation', value: 'schema' },
  { name: 'vrf       — plugin: verifiable random functions (RFC 9381) + randomness beacon', value: 'vrf' },
  { name: 'ai        — experimental plugin: AI/ML inference', value: 'ai' },
  { name: 'zk        — experimental plugin: zero-knowledge proofs', value: 'zk' },
  { name: 'sla       — experimental plugin: service-level agreements', value: 'sla' },
  { name: 'arbitration — experimental plugin: dispute resolution', value: 'arbitration' },
  { name: 'poker     — app plugin: card-blind signed-log substrate for turn-based games', value: 'poker' }
]

// ─── Helpers ────────────────────────────────────────────────────────

function formatBytes (bytes) {
  if (bytes >= 1024 ** 3) return `${(bytes / (1024 ** 3)).toFixed(0)} GB`
  if (bytes >= 1024 ** 2) return `${(bytes / (1024 ** 2)).toFixed(0)} MB`
  return `${bytes} bytes`
}

function parseStorageInput (val) {
  const str = val.toString().trim().toUpperCase()
  const num = parseFloat(str)
  if (str.endsWith('TB')) return num * 1024 * 1024 * 1024 * 1024
  if (str.endsWith('GB')) return num * 1024 * 1024 * 1024
  if (str.endsWith('MB')) return num * 1024 * 1024
  return num * 1024 * 1024 * 1024 // Default to GB
}

// ─── Main Setup Flow ────────────────────────────────────────────────

export async function runSetup () {
  await loadPrompts()

  console.log()
  console.log('  ╔════════════════════════════════════════╗')
  console.log('  ║     HiveRelay Node Setup Wizard        ║')
  console.log('  ║     Configure your relay node          ║')
  console.log('  ╚════════════════════════════════════════╝')
  console.log()

  const config = {}

  // ─── Step 1: Node Profile ────────────────────────────────────────

  const profile = await select({
    message: 'Select a node profile:',
    choices: Object.entries(PROFILES).map(([key, p]) => ({
      name: p.label,
      value: key
    }))
  })

  if (profile !== 'custom') {
    const p = PROFILES[profile]
    config.maxStorageBytes = p.maxStorageBytes
    config.maxConnections = p.maxConnections
    config.maxRelayBandwidthMbps = p.maxRelayBandwidthMbps
    config._selectedServices = p.services
    Object.assign(config, p.config || {})
  }

  // ─── Step 2: Storage Path ────────────────────────────────────────

  const defaultStorage = join(homedir(), '.hiverelay', 'storage')
  const storagePath = await input({
    message: 'Storage directory:',
    default: defaultStorage
  })
  config.storage = storagePath

  // ─── Step 3: Resource Limits (custom or confirm) ─────────────────

  if (profile === 'custom') {
    const storageStr = await input({
      message: 'Max storage (e.g. 50GB, 200GB, 1TB):',
      default: '50GB',
      validate: (v) => {
        const n = parseStorageInput(v)
        return n > 0 ? true : 'Enter a valid size (e.g. 50GB)'
      }
    })
    config.maxStorageBytes = parseStorageInput(storageStr)

    config.maxConnections = await number({
      message: 'Max peer connections:',
      default: 256,
      min: 16,
      max: 4096
    })

    config.maxRelayBandwidthMbps = await number({
      message: 'Max relay bandwidth (Mbps):',
      default: 100,
      min: 10,
      max: 10000
    })
  } else {
    console.log()
    console.log(`  Profile: ${profile}`)
    console.log(`  Storage: ${formatBytes(config.maxStorageBytes)}`)
    console.log(`  Connections: ${config.maxConnections}`)
    console.log(`  Bandwidth: ${config.maxRelayBandwidthMbps} Mbps`)
    console.log()

    const tweakResources = await confirm({
      message: 'Adjust resource limits?',
      default: false
    })

    if (tweakResources) {
      const storageStr = await input({
        message: 'Max storage (e.g. 50GB, 200GB, 1TB):',
        default: formatBytes(config.maxStorageBytes),
        validate: (v) => {
          const n = parseStorageInput(v)
          return n > 0 ? true : 'Enter a valid size (e.g. 50GB)'
        }
      })
      config.maxStorageBytes = parseStorageInput(storageStr)

      config.maxConnections = await number({
        message: 'Max peer connections:',
        default: config.maxConnections,
        min: 16,
        max: 4096
      })

      config.maxRelayBandwidthMbps = await number({
        message: 'Max relay bandwidth (Mbps):',
        default: config.maxRelayBandwidthMbps,
        min: 10,
        max: 10000
      })
    }
  }

  // ─── Step 4: Services ────────────────────────────────────────────

  console.log()
  const profileServices = config._selectedServices || []

  const services = await checkbox({
    message: 'Enable optional service plugins:',
    choices: ALL_SERVICES.map(s => ({
      ...s,
      checked: profileServices.includes(s.value)
    }))
  })
  config.services = services
  config.plugins = services
  config.enableServices = services.length > 0
  delete config._selectedServices

  // ─── Step 5: Core Features ───────────────────────────────────────

  console.log()
  config.enableRelay = await confirm({
    message: 'Enable circuit relay (forward traffic for peers)?',
    default: true
  })

  config.enableSeeding = await confirm({
    message: 'Enable app seeding (store and serve Pear apps)?',
    default: true
  })

  if (config.enableSeeding) {
    config.registryAutoAccept = await confirm({
      message: 'Auto-accept seed requests from the registry?',
      default: true
    })
  }

  // ─── Step 6: API & Network ───────────────────────────────────────

  console.log()
  config.enableAPI = await confirm({
    message: 'Enable HTTP API?',
    default: true
  })

  if (config.enableAPI) {
    config.apiPort = await number({
      message: 'API port:',
      default: 9100,
      min: 1024,
      max: 65535
    })
  }

  const setRegion = await confirm({
    message: 'Set a geographic region filter?',
    default: false
  })
  if (setRegion) {
    const regionInput = await input({
      message: 'Region codes (comma-separated, e.g. US,EU,ASIA):',
      default: ''
    })
    config.regions = regionInput.split(',').map(r => r.trim()).filter(Boolean)
  }

  // ─── Step 7: Transports ──────────────────────────────────────────

  console.log()
  const transports = { udp: true }

  transports.holesail = await confirm({
    message: 'Enable Holesail tunnel (required if behind NAT)?',
    default: true
  })

  transports.tor = await confirm({
    message: 'Enable Tor hidden service?',
    default: false
  })

  if (transports.tor) {
    config.tor = {}
    config.tor.socksPort = await number({
      message: 'Tor SOCKS port:',
      default: 9050,
      min: 1024,
      max: 65535
    })
    config.tor.controlPort = await number({
      message: 'Tor control port:',
      default: 9051,
      min: 1024,
      max: 65535
    })
    const torPassword = await input({
      message: 'Tor control password (leave empty for cookie auth):',
      default: ''
    })
    if (torPassword) config.tor.controlPassword = torPassword
  }

  transports.websocket = await confirm({
    message: 'Enable WebSocket transport?',
    default: false
  })

  if (transports.websocket) {
    config.wsPort = await number({
      message: 'WebSocket port:',
      default: 8765,
      min: 1024,
      max: 65535
    })
  }

  config.transports = transports

  // ─── Step 8: Lightning Payments ──────────────────────────────────

  console.log()
  const enableLightning = await confirm({
    message: 'Enable experimental Lightning Network payments?',
    default: false
  })

  if (enableLightning) {
    config.lightning = { enabled: true }
    config.lightning.rpcUrl = await input({
      message: 'LND gRPC URL:',
      default: 'localhost:10009'
    })
    config.lightning.network = await select({
      message: 'Lightning network:',
      choices: [
        { name: 'Mainnet', value: 'mainnet' },
        { name: 'Testnet', value: 'testnet' },
        { name: 'Regtest', value: 'regtest' }
      ]
    })
    config.payment = {
      enabled: true,
      settlementInterval: 24 * 60 * 60 * 1000,
      minSettlementSats: 1000
    }
  }

  // ─── Step 9: Advanced ────────────────────────────────────────────

  const showAdvanced = await confirm({
    message: 'Configure advanced settings?',
    default: false
  })

  if (showAdvanced) {
    console.log()

    config.maxCircuitDuration = (await number({
      message: 'Max circuit duration (minutes):',
      default: 10,
      min: 1,
      max: 60
    })) * 60 * 1000

    config.maxCircuitBytes = (await number({
      message: 'Max circuit size (MB):',
      default: 64,
      min: 1,
      max: 1024
    })) * 1024 * 1024

    config.maxCircuitsPerPeer = await number({
      message: 'Max circuits per peer:',
      default: 5,
      min: 1,
      max: 50
    })

    config.proofMaxLatencyMs = await number({
      message: 'Proof-of-relay max latency (ms):',
      default: 5000,
      min: 500,
      max: 30000
    })

    config.shutdownTimeoutMs = (await number({
      message: 'Graceful shutdown timeout (seconds):',
      default: 10,
      min: 1,
      max: 60
    })) * 1000

    const customBootstrap = await confirm({
      message: 'Use custom DHT bootstrap nodes?',
      default: false
    })
    if (customBootstrap) {
      const bsInput = await input({
        message: 'Bootstrap nodes (host:port, comma-separated):',
        default: ''
      })
      config.bootstrapNodes = bsInput.split(',').map(s => {
        const [host, port] = s.trim().split(':')
        return { host, port: parseInt(port) || 49737 }
      }).filter(b => b.host)
    }
  }

  // ─── Summary ─────────────────────────────────────────────────────

  console.log()
  console.log('  ╔════════════════════════════════════════╗')
  console.log('  ║         Configuration Summary          ║')
  console.log('  ╚════════════════════════════════════════╝')
  console.log()
  console.log(`  Profile:       ${profile}`)
  console.log(`  Storage path:  ${config.storage}`)
  console.log(`  Max storage:   ${formatBytes(config.maxStorageBytes)}`)
  console.log(`  Connections:   ${config.maxConnections}`)
  console.log(`  Bandwidth:     ${config.maxRelayBandwidthMbps} Mbps`)
  console.log(`  Plugins:       ${services.length ? services.join(', ') : 'none — core relay only'}`)
  console.log(`  Relay:         ${config.enableRelay ? 'enabled' : 'disabled'}`)
  console.log(`  Seeding:       ${config.enableSeeding ? 'enabled' : 'disabled'}`)
  console.log(`  API:           ${config.enableAPI ? 'http://127.0.0.1:' + (config.apiPort || 9100) : 'disabled'}`)
  console.log(`  Transports:    ${Object.keys(transports).filter(t => transports[t]).join(', ')}`)
  if (config.regions && config.regions.length) {
    console.log(`  Regions:       ${config.regions.join(', ')}`)
  }
  if (enableLightning) {
    console.log(`  Lightning:     enabled (${config.lightning.network})`)
  }
  console.log()

  const doSave = await confirm({
    message: 'Save this configuration?',
    default: true
  })

  if (!doSave) {
    console.log('  Setup cancelled. No changes saved.')
    return null
  }

  // ─── Save ────────────────────────────────────────────────────────

  ensureDirs()
  const configPath = saveConfig(config)

  console.log()
  console.log(`  Config saved to: ${configPath}`)
  console.log()
  console.log('  Start your node:')
  console.log('    hiverelay start')
  console.log()

  const startNow = await confirm({
    message: 'Start the relay node now?',
    default: true
  })

  return { config, startNow }
}