OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / scripts / start-homehive.js

197 lines · javascript ref:
#!/usr/bin/env node

/**
 * Start HomeHive — Private Mode Relay
 * ====================================
 * Runs HiveRelay in private mode: no DHT, no public discovery,
 * devices must be explicitly paired to connect.
 *
 * Usage:
 *   node scripts/start-homehive.js
 *   node scripts/start-homehive.js --pair          # enable pairing for 5 minutes
 *   node scripts/start-homehive.js --add-device <pubkey> <name>
 */

import { RelayNode } from '../core/relay-node/index.js'
import { mkdirSync } from 'fs'
import { join } from 'path'
import { networkInterfaces } from 'os'

const STORAGE_DIR = process.env.HOMEHIVE_STORAGE || join(process.env.HOME || '.', '.homehive')
const API_PORT = parseInt(process.env.HOMEHIVE_PORT || '9100', 10)
const args = process.argv.slice(2)

mkdirSync(STORAGE_DIR, { recursive: true })

function getLocalIPs () {
  const ifaces = networkInterfaces()
  const result = []
  for (const [name, addrs] of Object.entries(ifaces)) {
    for (const addr of addrs) {
      if (addr.internal) continue
      if (addr.family === 'IPv4' || addr.family === 4) {
        result.push({ interface: name, address: addr.address })
      }
    }
  }
  return result
}

async function main () {
  const localIPs = getLocalIPs()

  console.log('=================================================')
  console.log('  HomeHive — Private Relay')
  console.log('  Mode: private (LAN + WiFi, no public internet)')
  console.log(`  Storage: ${STORAGE_DIR}`)
  console.log('=================================================\n')

  console.log('  Network interfaces:')
  for (const ip of localIPs) {
    const isWifi = /^(wl|wi|en0|en1|wlan|Wi-Fi)/i.test(ip.interface)
    const label = isWifi ? '(wifi)' : '(wired)'
    console.log(`    ${ip.interface}: ${ip.address} ${label}`)
  }
  console.log()

  const node = new RelayNode({
    mode: 'private',
    storage: STORAGE_DIR,
    enableAPI: true,
    enableMetrics: true,
    apiPort: API_PORT,
    apiHost: '0.0.0.0', // Listen on all interfaces (LAN + wifi)
    discovery: {
      dht: false,
      announce: false,
      mdns: true,
      explicit: true
    },
    name: process.env.HOMEHIVE_NAME || 'homehive'
  })

  // Event logging
  node.on('started', ({ publicKey }) => {
    console.log(`  Public key: ${publicKey.toString('hex')}`)
    console.log(`  Paired devices: ${node.listDevices().length}`)
    console.log()

    console.log('  Reachable at:')
    for (const ip of localIPs) {
      console.log(`    http://${ip.address}:${API_PORT}/dashboard`)
    }
    console.log()

    if (node.listDevices().length === 0) {
      console.log('  No devices paired yet.')
      console.log('  Run with --pair to enable pairing mode.')
      console.log()
    }
  })

  node.on('device-paired', ({ pubkey, name }) => {
    console.log(`  + Device paired: ${name} (${pubkey.slice(0, 16)}...)`)
  })

  node.on('device-removed', ({ pubkey }) => {
    console.log(`  - Device removed: ${pubkey.slice(0, 16)}...`)
  })

  node.on('connection', ({ remotePubKey }) => {
    console.log(`  <- Device connected: ${remotePubKey.slice(0, 16)}...`)
  })

  node.on('connection-rejected', ({ remotePubKey, reason }) => {
    console.log(`  x Connection rejected: ${remotePubKey.slice(0, 16)}... (${reason})`)
  })

  node.on('connection-closed', () => {
    console.log('  -> Device disconnected')
  })

  node.on('pairing-success', ({ pubkey, name }) => {
    console.log(`  * Pairing successful: ${name} (${pubkey.slice(0, 16)}...)`)
  })

  node.on('pairing-rejected', ({ reason, pubkey }) => {
    console.log(`  x Pairing rejected: ${reason} (${pubkey ? pubkey.slice(0, 16) + '...' : 'unknown'})`)
  })

  node.on('lan-peer-discovered', ({ pubkey, host, port, name }) => {
    console.log(`  ~ LAN peer found: ${name} at ${host}:${port} (${pubkey.slice(0, 16)}...)`)
  })

  node.on('seeding', ({ appKey }) => {
    console.log(`  > Serving app: ${appKey.slice(0, 16)}...`)
  })

  await node.start()

  // Handle --pair flag
  if (args.includes('--pair')) {
    const timeoutMs = 5 * 60 * 1000
    const info = node.enablePairing({ timeoutMs })
    console.log('  PAIRING MODE ACTIVE (5 minutes)')
    console.log('  ─────────────────────────────────')
    console.log(`  Token: ${info.token}`)
    console.log(`  Relay pubkey: ${info.relayPubkey}`)
    console.log()
    console.log('  Reachable from any device on this network:')
    for (const ip of localIPs) {
      const isWifi = /^(wl|wi|en0|en1|wlan|Wi-Fi)/i.test(ip.interface)
      console.log(`    ${ip.address}:${API_PORT} ${isWifi ? '(wifi)' : '(wired)'}`)
    }
    console.log()
    console.log('  Share this with the device to pair:')
    const pairingPayload = {
      pubkey: info.relayPubkey,
      token: info.token,
      addresses: localIPs.map(ip => `${ip.address}:${API_PORT}`)
    }
    console.log(`  ${JSON.stringify(pairingPayload)}`)
    console.log()
  }

  // Handle --add-device flag
  const addIdx = args.indexOf('--add-device')
  if (addIdx !== -1) {
    const pubkey = args[addIdx + 1]
    const name = args[addIdx + 2] || 'manual'
    if (!pubkey || pubkey.length !== 64) {
      console.error('  Error: --add-device requires a 64-char hex pubkey')
      process.exit(1)
    }
    await node.addDevice(pubkey, name)
    console.log(`  Added device: ${name} (${pubkey.slice(0, 16)}...)`)
    console.log()
  }

  // List paired devices
  const devices = node.listDevices()
  if (devices.length > 0) {
    console.log('  Paired devices:')
    for (const d of devices) {
      console.log(`    ${d.name}: ${d.pubkey.slice(0, 16)}... (paired ${new Date(d.pairedAt).toLocaleDateString()})`)
    }
    console.log()
  }

  console.log('  HomeHive running. Press Ctrl+C to stop.\n')

  // Graceful shutdown
  const shutdown = async () => {
    console.log('\n  Shutting down HomeHive...')
    await node.stop()
    console.log('  Done.')
    process.exit(0)
  }

  process.on('SIGINT', shutdown)
  process.on('SIGTERM', shutdown)
}

main().catch(err => {
  console.error('Fatal:', err)
  process.exit(1)
})