OverviewFilesCommitsIssuesDocsReleasesPeers

hyperident-shell / lib / client.js

60 lines · javascript ref:
// Shell client: connect to a server pubkey with the device keyPair and drive a
// ShellClient over protomux. Retries transient pre-establishment failures (DHT
// hole-punching can abort on the first try — HOLEPUNCH_ABORTED — then succeed).
import DHT from 'hyperdht'
import Protomux from 'protomux'
import process from '#node:process'
import { ShellClient } from './shell.js'

// Codes worth retrying before the encrypted connection is established.
const TRANSIENT = new Set(['HOLEPUNCH_ABORTED', 'ETIMEDOUT', 'PEER_NOT_FOUND', 'ECONNRESET'])

export function connect ({ serverPublicKey, keyPair, command = [], retries = 5, onerror = null, stdio = null } = {}) {
  const state = { node: null, socket: null, shell: null }
  let attempt = 0

  function fail (err) {
    if (onerror) onerror(err)
    else process.stderr.write('connection failed: ' + (err.code || err.message) + '\n')
    process.exitCode = 1
  }

  function attemptConnect () {
    attempt++
    const node = new DHT()
    const socket = node.connect(serverPublicKey, { keyPair })
    state.node = node
    state.socket = socket
    let established = false

    socket.setKeepAlive(5000)

    // 'open' fires once the noise handshake completes (connection established).
    // Only then wire up the shell — so a retry never double-attaches stdin.
    socket.once('open', () => {
      established = true
      const mux = new Protomux(socket)
      state.shell = new ShellClient(command, { socket, mux, ...(stdio || {}) })
      state.shell.open()
      socket.once('close', () => node.destroy())
    })

    socket.on('error', (err) => {
      if (established) { fail(err); return }
      // Pre-establishment failure — retry (transient/hole-punch races) or give up.
      try { socket.destroy() } catch {}
      node.destroy().finally(() => {
        if (attempt <= retries && (TRANSIENT.has(err.code) || !err.code)) {
          if (attempt === 1) process.stderr.write('connecting…\n')
          setTimeout(attemptConnect, 400 * attempt) // linear backoff
        } else {
          fail(err)
        }
      })
    })
  }

  attemptConnect()
  return state
}