OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / core / transports / holesail / index.js

91 lines · javascript ref:
/**
 * Holesail API Tunnel Transport
 *
 * Exposes the relay's HTTP API through the Hyperswarm DHT via Holesail,
 * making the API reachable without port forwarding. Designed for
 * home-network relay operators behind NAT.
 *
 * The transport creates a HolesailServer that tunnels the local API port
 * (default 9100) through the DHT. Remote nodes connect using the
 * holesail connection key via HolesailClient.
 *
 * Key persistence: The holesail seed is derived deterministically from
 * the relay's Hyperswarm keypair, so the connection key survives restarts.
 */

import { EventEmitter } from 'events'
import { createRequire } from 'module'

const require = createRequire(import.meta.url)

export class HolesailTransport extends EventEmitter {
  constructor (opts = {}) {
    super()
    this.apiPort = opts.apiPort || 9100
    this.apiHost = opts.host || '127.0.0.1'
    this.seed = opts.seed || null // 32-byte hex string for deterministic keys
    this.connectionKey = null
    this.running = false
    this._server = null
  }

  async start () {
    if (this.running) return

    const HolesailServer = require('holesail-server')

    this._server = new HolesailServer()

    // Generate deterministic keypair from seed
    if (this.seed) {
      this._server.generateKeyPair(this.seed)
    }

    await new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        reject(new Error('Holesail server start timed out after 30s'))
      }, 30000)

      this._server.start(
        {
          port: this.apiPort,
          host: this.apiHost,
          seed: this.seed
        },
        () => {
          clearTimeout(timer)
          resolve()
        }
      )
    })

    this.connectionKey = this._server.key
    this.running = true

    this.emit('started', { connectionKey: this.connectionKey })
  }

  async stop () {
    if (!this.running) return
    this.running = false

    if (this._server) {
      try {
        await this._server.destroy()
      } catch {}
      this._server = null
    }

    this.connectionKey = null
    this.emit('stopped')
  }

  getInfo () {
    return {
      running: this.running,
      connectionKey: this.connectionKey,
      apiPort: this.apiPort
    }
  }
}