// 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>')
}