OverviewFilesCommitsIssuesDocsReleasesPeers

hyperident-shell / lib / registry.js

111 lines · javascript ref:
// P2P handle-registry integration for hyperident-shell.
//
// A shared HandleRegistry (Autobase) is replicated over Hyperswarm on its
// discovery topic. A host (writer) claims handles (e.g. celltron -> device
// pubkey); any peer opens a read replica, syncs, and resolves a name to the
// device pubkey to connect to. This replaces the local names-file stand-in for
// global resolution. Dual-runtime (node + bare): corestore/hyperswarm/autobase
// all run on both.
import Corestore from 'corestore'
import Hyperswarm from 'hyperswarm'
import fs from '#node:fs'
import os from '#node:os'
import path from '#node:path'
import process from '#node:process'
import b4a from 'b4a'
import { HandleRegistry } from 'hyper-identity/handle-registry'

function homedir () {
  return (os.homedir && os.homedir()) || process.env.HOME || process.env.USERPROFILE || '.'
}

function defaultStoreDir () {
  return path.join(homedir(), '.hyperident-shell', 'registry-store')
}

function hostStoreDir () {
  return path.join(homedir(), '.hyperident-shell', 'registry-host-store')
}

function configFile () {
  return path.join(homedir(), '.hyperident-shell', 'config.json')
}

// The configured registry key (~/.hyperident-shell/config.json), overridable via
// the HYPERIDENT_SHELL_REGISTRY env var.
export function loadRegistryKey () {
  if (process.env.HYPERIDENT_SHELL_REGISTRY) return process.env.HYPERIDENT_SHELL_REGISTRY
  try {
    return JSON.parse(fs.readFileSync(configFile(), 'utf8')).registryKey || null
  } catch {
    return null
  }
}

export function saveRegistryKey (key) {
  if (!isRegistryKey(key)) throw new Error('registry key must be 64 hex chars')
  fs.mkdirSync(path.dirname(configFile()), { recursive: true })
  fs.writeFileSync(configFile(), JSON.stringify({ registryKey: key }, null, 2) + '\n', { mode: 0o600 })
}

/**
 * Open a HandleRegistry and replicate it over Hyperswarm.
 * @param {object} opts
 * @param {string} [opts.registryKey] — bootstrap key (hex). Omit + host:true to CREATE.
 * @param {boolean} [opts.host=false] — announce on the swarm (registry server).
 * @param {string} [opts.storeDir] — corestore dir.
 * @returns {Promise<{ reg, swarm, store, close }>}
 */
export async function openRegistry ({ registryKey = null, host = false, storeDir = null } = {}) {
  storeDir = storeDir || (host ? hostStoreDir() : defaultStoreDir())
  const store = new Corestore(storeDir)
  const reg = new HandleRegistry(store, registryKey ? { bootstrap: registryKey } : {})
  await reg.ready()

  const swarm = new Hyperswarm()
  swarm.on('connection', (conn) => reg.replicate(conn))

  const topic = reg.discoveryKey
  swarm.join(topic, { server: host, client: true })
  await swarm.flush()

  return {
    reg,
    swarm,
    store,
    async close () {
      try { await swarm.destroy() } catch {}
      try { await reg.close() } catch {}
      try { await store.close() } catch {}
    }
  }
}

/**
 * Resolve a name via the P2P registry. Joins the swarm, syncs, and polls until
 * the name resolves or the timeout elapses.
 * @returns {Promise<object|null>} the handle entry, or null if unresolved.
 */
export async function resolveViaRegistry (name, { registryKey, storeDir, timeoutMs = 8000 } = {}) {
  if (!registryKey) return null
  const r = await openRegistry({ registryKey, host: false, storeDir })
  try {
    const deadline = Date.now() + timeoutMs
    let entry = null
    while (!entry && Date.now() < deadline) {
      await r.reg.update()
      entry = await r.reg.resolve(name)
      if (!entry) await new Promise((res) => setTimeout(res, 200))
    }
    return entry
  } finally {
    await r.close()
  }
}

export function isRegistryKey (s) {
  return typeof s === 'string' && /^[0-9a-fA-F]{64}$/.test(s)
}

export { defaultStoreDir }