OverviewFilesCommitsIssuesDocsReleasesPeers

hypermosh-shell / lib / names.mjs

76 lines · javascript ref:
// Optional name resolution. A raw public key ALWAYS works; a name is looked up
// in a local store first. Same known_hosts-style format as hyperident-shell, so
// a `~/.hypermosh/names` line is `<name> <z-base32-pubkey>`.
//
// Two layers (mirrors hyperident-shell):
//   1. local names store (~/.hypermosh/names) — immediate, offline, per-machine.
//   2. (future) the hyperident HandleRegistry — a P2P name -> identity registry;
//      resolve() has the hook where that lookup slots in, with the local store
//      acting as cache/override. Call sites are unchanged when it lands.
import fs from '#node:fs'
import os from '#node:os'
import path from '#node:path'
import process from '#node:process'
import HypercoreId from 'hypercore-id-encoding'

function homedir () { return (os.homedir && os.homedir()) || process.env.HOME || process.env.USERPROFILE || '.' }
function namesFile () { return path.join(homedir(), '.hypermosh', 'names') }

export function loadNames (file = namesFile()) {
  const map = new Map()
  let txt
  try { txt = fs.readFileSync(file, 'utf8') } catch (e) { if (e.code === 'ENOENT') return map; throw e }
  for (const line of txt.split('\n')) {
    const s = line.trim()
    if (!s || s.startsWith('#')) continue
    const sp = s.indexOf(' ')
    const name = sp === -1 ? s : s.slice(0, sp).trim()
    const key = sp === -1 ? '' : s.slice(sp + 1).trim()
    if (name && key) map.set(name, key)
  }
  return map
}

export function addName (name, pubkey, file = namesFile()) {
  HypercoreId.decode(pubkey) // validate (throws if not a valid key)
  fs.mkdirSync(path.dirname(file), { recursive: true })
  const map = loadNames(file)
  map.set(name, pubkey)
  fs.writeFileSync(file, [...map.entries()].map(([n, k]) => n + ' ' + k).join('\n') + '\n', { mode: 0o600 })
}

// Resolve a name-or-pubkey to a 32-byte public key Buffer.
//   1. local names store (name -> pubkey)
//   2. treat the input as a raw z-base32/hex pubkey  (← pubkeys always work)
//   3. TODO: query the hyperident HandleRegistry and cache into the local store.
export function resolve (nameOrKey, file = namesFile()) {
  const map = loadNames(file)
  if (map.has(nameOrKey)) return HypercoreId.decode(map.get(nameOrKey))
  try { return HypercoreId.decode(nameOrKey) } catch {}
  throw new Error('cannot resolve "' + nameOrKey + '" — not a known name or a valid pubkey.\n' +
    '  add it with:  hypermosh add-name ' + nameOrKey + ' <pubkey>')
}

// Async resolve: local store / raw pubkey (offline, instant) → the shared
// hyperident HandleRegistry (reads metadata.moshKey), caching a hit into the
// local names store. The registry deps are lazy-imported so `id`/`names`/offline
// resolves never pay for corestore/hyperswarm.
export async function resolveAsync (nameOrKey, file = namesFile()) {
  const map = loadNames(file)
  if (map.has(nameOrKey)) return HypercoreId.decode(map.get(nameOrKey))
  try { return HypercoreId.decode(nameOrKey) } catch {}
  let keyStr = null
  try {
    const { resolveMoshKey } = await import('./registry.mjs')
    keyStr = await resolveMoshKey(nameOrKey)
  } catch (e) { /* registry unreachable → fall through to the helpful error */ }
  if (keyStr) {
    let key
    try { key = HypercoreId.decode(keyStr) } catch { throw new Error('shared registry returned an invalid key for "' + nameOrKey + '": ' + keyStr) }
    try { addName(nameOrKey, HypercoreId.encode(key), file) } catch {} // cache normalized
    return key
  }
  throw new Error('cannot resolve "' + nameOrKey + '" — not a known name, a valid pubkey, or in the shared registry.\n' +
    '  add it with:  hypermosh add-name ' + nameOrKey + ' <pubkey>')
}