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