// Shell server: hyperdht server keyed by the device keyPair, one protomux
// ShellServer per connection, identity-based firewall. Dual-runtime.
import DHT from 'hyperdht'
import Protomux from 'protomux'
import HypercoreId from 'hypercore-id-encoding'
import fs from '#node:fs'
import { ShellServer } from './shell.js'
// Read allowed pubkeys from a firewall file (one z-base32/hex key per line,
// '#' comments). Missing file => empty allow-list (deny all). bare-fs safe.
export function readAllowed (firewallPath) {
if (!firewallPath) return []
let txt
try {
txt = fs.readFileSync(firewallPath, 'utf8')
} catch (e) {
if (e.code === 'ENOENT') return []
throw e
}
return txt
.split('\n')
.map((l) => l.trim())
.filter((l) => l && !l.startsWith('#'))
.map((k) => HypercoreId.decode(k))
}
export async function serve ({ keyPair, firewall = null, disableFirewall = false, onConnection = null } = {}) {
const allowed = disableFirewall ? true : readAllowed(firewall)
const node = new DHT()
const server = node.createServer({ firewall: onFirewall })
server.on('connection', (socket) => {
socket.on('error', () => {}) // ignore transport resets
const mux = new Protomux(socket)
mux.pair({ protocol: 'hypershell' }, () => {
const shell = new ShellServer({ mux })
if (shell.channel) shell.open()
})
})
await server.listen(keyPair)
return {
node,
server,
publicKey: keyPair.publicKey,
async close () {
await server.close()
await node.destroy()
}
}
function onFirewall (remotePublicKey) {
const allow = allowed === true || allowed.some((k) => remotePublicKey.equals(k))
if (onConnection) onConnection(remotePublicKey, allow)
return !allow // protomux firewall: return true = DENY
}
}