OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / verifier / bin / verify.js

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

/**
 * hive-verify — CLI for the cross-client verifier.
 *
 * Examples:
 *   hive-verify https://relay-a.example.com https://relay-b.example.com
 *     → compares capability docs + catalogs across both relays
 *
 *   hive-verify --drive abc123... https://relay-a https://relay-b https://relay-c
 *     → compares the three relays' views of a specific drive
 *
 *   hive-verify --json ...
 *     → emit raw JSON report instead of human-readable
 *
 * Exit codes:
 *   0  — all relays agree (or insufficient data — better report that
 *        than silently pass a single-source check)
 *   1  — divergence detected (the important case)
 *   2  — all relays failed to respond
 *   3  — argument or usage error
 */

import { verifyRelays, compareDrive } from '../index.js'

function usage () {
  console.error('Usage:')
  console.error('  hive-verify <relay-url-1> <relay-url-2> [<relay-url-3> ...]')
  console.error('  hive-verify --drive <drive-key-hex> <relay-url-1> <relay-url-2> [...]')
  console.error('')
  console.error('Options:')
  console.error('  --json        emit raw JSON instead of human-readable')
  console.error('  --timeout N   per-fetch timeout in ms (default 10000)')
  console.error('  --help        show this message')
}

async function main () {
  const args = process.argv.slice(2)
  if (args.includes('--help') || args.length === 0) {
    usage()
    process.exit(args.length === 0 ? 3 : 0)
  }

  const jsonMode = args.includes('--json')
  let driveKey = null
  let timeoutMs = 10_000
  const relayUrls = []

  for (let i = 0; i < args.length; i++) {
    const a = args[i]
    if (a === '--json') continue
    if (a === '--drive') {
      driveKey = args[++i]
      if (!driveKey) { usage(); process.exit(3) }
      continue
    }
    if (a === '--timeout') {
      timeoutMs = parseInt(args[++i]) || 10_000
      continue
    }
    if (a.startsWith('--')) {
      console.error('Unknown option: ' + a)
      usage()
      process.exit(3)
    }
    relayUrls.push(a)
  }

  if (relayUrls.length < 2) {
    console.error('Error: need at least 2 relay URLs to compare.')
    usage()
    process.exit(3)
  }

  try {
    if (driveKey) {
      const report = await compareDrive(driveKey, relayUrls, { timeoutMs })
      if (jsonMode) {
        console.log(JSON.stringify(report, null, 2))
      } else {
        printDriveReport(report)
      }
      process.exit(report.agreement === 'agree' ? 0 : 1)
    } else {
      const report = await verifyRelays(relayUrls, { timeoutMs })
      if (jsonMode) {
        console.log(JSON.stringify(report, null, 2))
      } else {
        printRelayReport(report)
      }
      if (report.fetchErrors.length === relayUrls.length * 2) {
        process.exit(2) // everything failed
      }
      process.exit(report.verdict === 'agree' ? 0 : 1)
    }
  } catch (err) {
    console.error('Error: ' + err.message)
    process.exit(3)
  }
}

function printRelayReport (r) {
  console.log('Relays checked:')
  for (const url of r.checkedRelays) console.log('  ' + url)
  console.log('')
  console.log('Capabilities OK: ' + (r.capabilitiesOK ? '✓' : '✗ (see errors below)'))
  console.log('Catalogs OK:     ' + (r.catalogsOK ? '✓' : '✗ (see errors below)'))
  console.log('')
  if (r.fetchErrors.length > 0) {
    console.log('Fetch errors:')
    for (const e of r.fetchErrors) {
      console.log('  ' + e.relay + ' (' + e.endpoint + '): ' + e.error)
    }
    console.log('')
  }
  console.log('Divergences: ' + r.divergenceCount)
  for (const d of r.divergences) {
    if (d.category === 'capability') {
      console.log('  [capability] ' + d.field + ' differs between ' + d.relayA + ' and ' + d.relayB)
      console.log('    A: ' + JSON.stringify(d.valueA))
      console.log('    B: ' + JSON.stringify(d.valueB))
    } else if (d.category === 'catalog-entry') {
      console.log('  [catalog] ' + d.appKey + ' differs on: ' + d.divergentFields.join(', '))
      console.log('    A (' + d.relayA + '): ' + JSON.stringify(d.entryA))
      console.log('    B (' + d.relayB + '): ' + JSON.stringify(d.entryB))
    }
  }
  console.log('')
  console.log('Verdict: ' + r.verdict.toUpperCase())
}

function printDriveReport (r) {
  console.log('Drive: ' + r.drive)
  console.log('')
  console.log('Views:')
  for (const v of r.views) {
    if (v.ok) {
      console.log('  ' + v.relay + ' → length=' + (v.info?.length ?? '?') + ' version=' + (v.info?.version ?? '?'))
    } else {
      console.log('  ' + v.relay + ' → ERROR: ' + v.error)
    }
  }
  console.log('')
  if (r.agreement === 'insufficient-data') {
    console.log('Verdict: INSUFFICIENT DATA (need at least 2 successful responses)')
  } else if (r.agreement === 'agree') {
    console.log('Verdict: AGREE (all responding relays serve the same drive state)')
  } else {
    console.log('Verdict: DIVERGE')
    for (const d of r.divergentFrom) {
      console.log('  ' + d.relay + ' disagrees with ' + d.vs + ' on: ' + d.fields.join(', '))
    }
  }
}

main().catch(err => {
  console.error('Unexpected error: ' + err.stack)
  process.exit(3)
})