/** * Bare/Node control plane for local Holesail client sessions. * Runs in the pear-electron Bare entry (index.js), where require.addon works. * The UI renderer talks to this over http://127.0.0.1:. * * Under Bare: require ONLY bare-* packages (never Node builtin names like * 'process' / 'crypto' — bare-module has no Node core builtins). * Under Node: require Node core (bare-* native addons need Bare's require.addon). */ 'use strict' const isBare = typeof globalThis.Bare !== 'undefined' // Strict split — no cross-fallback (Bare cannot load 'process', Node cannot load bare-abort addons). const http = isBare ? require('bare-http1') : require('http') const fs = isBare ? require('bare-fs') : require('fs') const path = isBare ? require('bare-path') : require('path') const crypto = isBare ? require('bare-crypto') : require('crypto') const net = isBare ? require('bare-net') : require('net') const b4a = require('b4a') const Holesail = require('holesail') // Avoid bare-process / Node 'process' entirely function getPid() { try { if (typeof globalThis.process?.pid === 'number') return globalThis.process.pid } catch { // ignore } try { if (typeof globalThis.Bare?.pid === 'number') return globalThis.Bare.pid } catch { // ignore } return 0 } /** @type {Map} */ const localClients = new Map() function findFreePort(host = '127.0.0.1') { return new Promise((resolve, reject) => { const s = net.createServer() s.listen(0, host, () => { const addr = s.address() const port = typeof addr === 'object' && addr ? addr.port : 0 s.close((err) => (err ? reject(err) : resolve(port))) }) s.on('error', reject) }) } /** * @param {string} urlOrKey * @param {{ localPort?: number, host?: string }} [opts] */ async function connect(urlOrKey, opts = {}) { const url = String(urlOrKey || '').trim() if (!url) throw new Error('Holesail URL required') if (localClients.has(url)) { const e = localClients.get(url) return summarize(e) } const host = opts.host || '127.0.0.1' const localPort = opts.localPort || (await findFreePort(host)) const instance = new Holesail({ client: true, key: url, port: localPort, host, log: false, }) const readyMs = Number(opts.readyTimeoutMs) || 45000 await Promise.race([ instance.ready(), new Promise((_, reject) => setTimeout( () => reject( new Error( `Holesail client ready() timed out after ${readyMs}ms (DHT / peer discovery)` ) ), readyMs ) ), ]) const info = instance.info || {} const entry = { instance, info, localPort: info.port || localPort, host: info.host || host, url, } if (entry.host === '0.0.0.0' || entry.host === '::') entry.host = '127.0.0.1' localClients.set(url, entry) return summarize(entry) } function summarize(e) { return { url: e.url, localPort: e.localPort, host: e.host, state: e.info?.state || 'listening', secure: e.info?.secure, protocol: e.info?.protocol, } } async function disconnect(urlOrKey) { const url = String(urlOrKey || '').trim() const entry = localClients.get(url) if (!entry) return false localClients.delete(url) try { await entry.instance.close() } catch { // ignore } return true } function list() { return [...localClients.values()].map(summarize) } function readJson(req) { return new Promise((resolve, reject) => { const chunks = [] req.on('data', (c) => chunks.push(typeof c === 'string' ? b4a.from(c) : c)) req.on('end', () => { if (!chunks.length) return resolve({}) try { const raw = b4a.toString(chunks.length === 1 ? chunks[0] : b4a.concat(chunks)) resolve(JSON.parse(raw || '{}')) } catch { reject(new Error('Invalid JSON body')) } }) req.on('error', reject) }) } function sendJson(res, status, body) { const raw = JSON.stringify(body) res.statusCode = status res.setHeader('Content-Type', 'application/json; charset=utf-8') res.setHeader('Access-Control-Allow-Origin', '*') res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Peardock-Token') res.end(raw) } function makeToken() { // bare-crypto and Node crypto both expose randomBytes const bytes = crypto.randomBytes(16) return b4a.toString(bytes, 'hex') } /** * @param {{ * statePath?: string, * token?: string, * host?: string, * port?: number, * }} [opts] */ async function start(opts = {}) { const token = opts.token || makeToken() const host = opts.host || '127.0.0.1' const pid = getPid() const server = http.createServer(async (req, res) => { if (req.method === 'OPTIONS') { res.statusCode = 204 res.setHeader('Access-Control-Allow-Origin', '*') res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Peardock-Token') res.end() return } try { const url = new URL(req.url || '/', `http://${host}`) const pathname = url.pathname if (pathname === '/health' && req.method === 'GET') { sendJson(res, 200, { ok: true, service: 'holesail-local', clients: localClients.size, bare: isBare, }) return } if (pathname !== '/health') { const hdr = req.headers['x-peardock-token'] const q = url.searchParams.get('token') if (hdr !== token && q !== token) { sendJson(res, 401, { ok: false, error: 'Unauthorized' }) return } } if (pathname === '/ping' && req.method === 'GET') { sendJson(res, 200, { ok: true, pong: true, clients: localClients.size }) return } if (pathname === '/list' && req.method === 'GET') { sendJson(res, 200, { ok: true, result: list() }) return } if (pathname === '/connect' && req.method === 'POST') { const body = await readJson(req) const result = await connect(body.url || body.key, { localPort: body.localPort, host: body.host, readyTimeoutMs: body.readyTimeoutMs, }) sendJson(res, 200, { ok: true, result }) return } if (pathname === '/disconnect' && req.method === 'POST') { const body = await readJson(req) const result = await disconnect(body.url || body.key) sendJson(res, 200, { ok: true, result }) return } sendJson(res, 404, { ok: false, error: 'Not found' }) } catch (err) { sendJson(res, 500, { ok: false, error: err?.message || String(err) }) } }) await new Promise((resolve, reject) => { server.once('error', reject) server.listen(opts.port || 0, host, resolve) }) const addr = server.address() const port = typeof addr === 'object' && addr ? addr.port : 0 const baseUrl = `http://${host}:${port}` const meta = { port, host, token, baseUrl, pid, bare: isBare } if (opts.statePath) { try { fs.mkdirSync(path.dirname(opts.statePath), { recursive: true }) fs.writeFileSync(opts.statePath, JSON.stringify(meta, null, 2), { mode: 0o600 }) } catch (err) { console.error('[holesail-control] failed to write state file', err) } } console.log(`[holesail-control] listening on ${baseUrl} (bare=${isBare})`) return { ...meta, server, async close() { for (const u of [...localClients.keys()]) { try { await disconnect(u) } catch { // ignore } } await new Promise((resolve) => server.close(() => resolve())) if (opts.statePath) { try { fs.unlinkSync(opts.statePath) } catch { // ignore } } }, } } module.exports = { start, connect, disconnect, list }