OverviewFilesCommitsIssuesDocsReleasesPeers

hyperident-shell / bin / hyperident-shell.js

174 lines · javascript ref:
#!/usr/bin/env node
import process from '#node:process'
import HypercoreId from 'hypercore-id-encoding'
import { loadDeviceKeyPair } from '../index.js'
import { serve } from '../lib/server.js'
import { connect } from '../lib/client.js'
import { addName, loadNames } from '../lib/names.js'
import { openRegistry, resolveViaRegistry, loadRegistryKey, saveRegistryKey } from '../lib/registry.js'
import { localStdio } from '../lib/stdio.js'

const USAGE = `hyperident-shell — P2P shell keyed by your hyperident device identity

Usage:
  hyperident-shell id                            Print your device pubkey (the shell address)
  hyperident-shell serve [--firewall <file>]     Serve a shell keyed by THIS device
                   [--disable-firewall]
  hyperident-shell <name|pubkey> [-- cmd ...]    Connect (resolves a name via the local
                                                 names store, else treats it as a pubkey)
  hyperident-shell add-name <name> <pubkey>      Map a name to a device pubkey
  hyperident-shell names                         List known names

The server/client key IS your hyperident device key; the server address equals
your device pubkey. Runs on node and bare.`

function keyPair () {
  const id = loadDeviceKeyPair()
  return { id, keyPair: { publicKey: id.publicKey, secretKey: id.secretKey } }
}

async function main () {
  const argv = process.argv.slice(2)
  const cmd = argv[0]

  if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
    console.log(USAGE)
    return
  }

  if (cmd === 'id') {
    const { id } = keyPair()
    console.log(id.pub)
    process.stderr.write('device pubkey (hex): ' + id.publicKeyHex + '\n')
    return
  }

  if (cmd === 'serve') {
    const { id, keyPair: kp } = keyPair()
    let firewall = null
    let disableFirewall = false
    for (let i = 1; i < argv.length; i++) {
      if (argv[i] === '--firewall') firewall = argv[++i]
      else if (argv[i] === '--disable-firewall') disableFirewall = true
    }
    await serve({
      keyPair: kp,
      firewall,
      disableFirewall,
      onConnection: (rpk, allow) =>
        process.stderr.write((allow ? 'allowed ' : 'DENIED  ') + HypercoreId.encode(rpk) + '\n')
    })
    console.log('hyperident-shell serving as ' + id.pub)
    console.log('connect with:  hyperident-shell ' + id.pub)
    // DHT server handles keep the process alive.
    return
  }

  if (cmd === 'add-name') {
    const name = argv[1]
    const pubkey = argv[2]
    if (!name || !pubkey) {
      process.stderr.write('usage: hyperident-shell add-name <name> <pubkey>\n')
      process.exit(1)
    }
    addName(name, pubkey)
    console.log('added: ' + name + ' -> ' + pubkey)
    return
  }

  if (cmd === 'names') {
    const map = loadNames()
    if (map.size === 0) console.log('(no names yet — add with: hyperident-shell add-name <name> <pubkey>)')
    for (const [n, k] of map) console.log(n.padEnd(20) + ' ' + k)
    return
  }

  if (cmd === 'registry') {
    await handleRegistry(argv.slice(1))
    return
  }

  // Otherwise: connect. Resolve cmd → server device pubkey.
  const { keyPair: kp } = keyPair()
  let serverPublicKey
  try {
    serverPublicKey = await resolveTarget(cmd)
  } catch (e) {
    process.stderr.write(e.message + '\n')
    process.exit(1)
  }
  connect({ serverPublicKey, keyPair: kp, command: argv.slice(1), stdio: localStdio() })
}

// Resolve a name-or-pubkey to a device pubkey Buffer:
//   1. a raw z-base32/hex pubkey  2. a local name  3. the P2P registry (if configured)
async function resolveTarget (nameOrKey) {
  try { return HypercoreId.decode(nameOrKey) } catch {}

  const names = loadNames()
  if (names.has(nameOrKey)) return HypercoreId.decode(names.get(nameOrKey))

  const registryKey = loadRegistryKey()
  if (registryKey) {
    process.stderr.write('resolving "' + nameOrKey + '" via P2P registry…\n')
    const entry = await resolveViaRegistry(nameOrKey, { registryKey })
    if (entry && entry.identity && entry.identity.devicePubkey) {
      return HypercoreId.decode(entry.identity.devicePubkey)
    }
  }

  throw new Error(
    'cannot resolve "' + nameOrKey + '" — not a pubkey, not a local name' +
    (registryKey ? ', not in the registry' : ', and no registry configured (hyperident-shell registry use <key>)')
  )
}

async function handleRegistry (args) {
  const sub = args[0]

  if (sub === 'use') {
    saveRegistryKey(args[1])
    console.log('registry set: ' + args[1])
    return
  }

  if (sub === 'key') {
    console.log(loadRegistryKey() || '(none)')
    return
  }

  if (sub === 'resolve') {
    const registryKey = loadRegistryKey()
    if (!registryKey) { process.stderr.write('no registry configured (hyperident-shell registry use <key>)\n'); process.exit(1) }
    const entry = await resolveViaRegistry(args[1], { registryKey })
    console.log(entry ? entry.identity.devicePubkey : '(not found)')
    process.exit(0)
  }

  if (sub === 'host') {
    let key = null
    const claims = []
    for (let i = 1; i < args.length; i++) {
      if (args[i] === '--key') key = args[++i]
      else if (args[i] === '--claim') { const [n, k] = String(args[++i]).split('='); claims.push([n, k]) }
    }
    const r = await openRegistry({ registryKey: key, host: true })
    for (const [n, k] of claims) {
      await r.reg.claim({ name: n, identity: { devicePubkey: k, identityRoot: k } })
      process.stderr.write('claimed ' + n + ' -> ' + k + '\n')
    }
    console.log('registry host up — key:')
    console.log(r.reg.keyHex)
    process.stderr.write('hosting over hyperswarm (keep this running)\n')
    return // the swarm keeps the process alive
  }

  process.stderr.write('usage: hyperident-shell registry host [--key <hex>] [--claim <name>=<pubkey>]... | use <key> | key | resolve <name>\n')
  process.exit(1)
}

main().catch((e) => {
  process.stderr.write('hyperident-shell: ' + e.message + '\n')
  process.exit(1)
})