// Migrated 2026-05-11: @holepunchto/keet-core-hyperdb was retired upstream;
// the replacement is @holepunchto/keet-core. The new constructor requires
// opts.buildType (set in coreOpts below). API surface (KeetCore.from →
// { keet }, core.backendAPI, backend.api.core.*) is preserved. See
// hyper-fleeky/keet-audit-4.x/SEMANTIC-DIFF.md for the full mapping.
//
// All keet-stack dep versions are pinned EXACTLY to Keet AppImage 4.14.1's
// /resources/app/node_modules — see package.json `//pin-note`. Drift was
// the root cause of the 2026-05-12 fresh-storage boot hang (notably
// @hyperswarm/rpc 3.2.0 missing the Bare events import map that 3.4.1 has).
//
// Hypercore.destroyRequests polyfill — load-bearing on populated cores.
// Some path inside keet-core's close cascade for an identity that has real
// state (rooms, peers, active block requests) calls
// Hypercore.destroyRequests(activeRequests, err). Current hypercore 11.x
// exposes Hypercore.clearRequests but not destroyRequests under that name.
// Polyfill the static method here to back-port destroy-on-close semantics:
// walk activeRequests, call .destroy(err) on each, swallow per-request
// throws. Idempotent — won't overwrite if a future hypercore provides it.
//
// Was removed once on the theory the bundled hyperbee2 uses clearRequests,
// proven by tests on fresh /tmp identity-0 with no real state. Real users
// on populated identities hit a different code path that DOES call
// destroyRequests — the polyfill catches it. Keep it.
import Hypercore from 'hypercore'
if (!Hypercore.destroyRequests) {
Hypercore.destroyRequests = function destroyRequests (requests, err) {
if (!requests) return
const iter = Array.isArray(requests) ? requests : Object.values(requests)
for (const r of iter) {
try { r && r.destroy && r.destroy(err) } catch {}
}
}
}
import KeetCoreModule from '@holepunchto/keet-core'
import crypto from 'hypercore-crypto'
import z32 from 'z32'
import path from 'path'
import os from 'os'
import fs from 'fs'
import fsx from 'fs-native-extensions'
import { Router } from './lib/router.js'
import { UdsTransport } from './lib/uds-transport.js'
import { IdentityRegistry } from './lib/identity-registry.js'
import { setDebug } from './lib/debug.js'
export { Router } from './lib/router.js'
export { UdsTransport } from './lib/uds-transport.js'
export { IdentityRegistry } from './lib/identity-registry.js'
export { SecureStorage } from './lib/secure-storage.js'
export { MetadataBase } from './lib/metadata-base.js'
export async function startDaemon (opts = {}) {
// --debug flag enables verbose discovery logging
if (opts.debug) setDebug(true)
const baseDir = opts.storage || path.join(os.homedir(), '.config', 'keet-cli')
const pidFile = opts.pidFile || '/tmp/keet-daemon.pid'
// Identity registry — handles migration from default/ on first run
const registry = new IdentityRegistry(baseDir)
await registry.load()
const active = registry.getActive()
const storage = active ? active.dir : path.join(baseDir, 'identity-0')
console.log('[keet-daemon] Starting... storage:', storage)
console.log('[keet-daemon] Active identity:', active ? active.id : '(none)')
// Single-daemon-per-storage guard. keet-core takes an EXCLUSIVE RocksDB lock
// on `storage`; a SECOND daemon on the same storage does NOT fail cleanly — it
// wedges keet-core so EVERY room join throws "Autobase is closing" (the
// stortron --dedicated incident, 2026-06-24: a leftover --daemon-only instance
// from the day before contended identity-0). Take an advisory flock on a
// sidecar lock file BEFORE opening keet-core so a duplicate refuses to start
// with a clear message instead of corrupting the live daemon. flock releases on
// process death (incl. SIGKILL), so a crashed/killed daemon never leaves a
// stale guard. NOTE: covers the primary identity; an identity swap (swapCore)
// keeps this initial lock — the incident was concurrent daemons on one identity.
fs.mkdirSync(storage, { recursive: true })
const lockPath = path.join(storage, 'keet-daemon.lock')
const lockFd = fs.openSync(lockPath, 'a+') // create if absent; no truncate
if (!fsx.tryLock(lockFd)) {
let holder = ''
try { holder = fs.readFileSync(lockPath, 'utf8').trim() } catch {}
try { fs.closeSync(lockFd) } catch {}
console.error('[keet-daemon] storage already locked by another daemon%s — refusing to start. A second keet-core on the same storage wedges room joins ("Autobase is closing"). Stop the other daemon first. storage=%s',
holder ? ` (PID ${holder})` : '', storage)
throw new Error('KEET_DAEMON_ALREADY_RUNNING: ' + storage)
}
// Record our PID for diagnostics (a refused daemon reads it above). The flock —
// not this file's content — is the guarantee. Keep lockFd open for the whole
// process lifetime; do NOT close it (the OS releases the lock on exit).
try { fs.writeFileSync(lockPath, String(Bare.pid)) } catch {}
// Track broken rooms (KEET_CONTRACT_ABORT during open/sync)
// Maps roomId (z-base-32 discovery key) → error message
const brokenRooms = new Map()
// Core options — reused for hot-swap
const coreOpts = {
experimental: false,
devMirrors: false,
devUserRegistry: false,
swarming: true,
production: true,
// keet-core 4.14.4+ requires buildType; constructor throws
// INVALID_BUILD_TYPE() if absent. Valid values: 'nightly' |
// 'internal' | 'production'. Use 'production' for stable ABI.
buildType: 'production',
onerror: err => {
const msg = err.message || String(err)
if (err.details?.roomKey) {
try {
const dkey = crypto.discoveryKey(Buffer.from(err.details.roomKey))
const roomId = z32.encode(dkey)
if (!brokenRooms.has(roomId)) {
brokenRooms.set(roomId, msg)
console.error(`[keet-daemon] Broken room tracked: ${roomId.slice(0, 12)} — ${msg}`)
}
} catch {
console.error('[keet-daemon] Core error:', msg)
}
} else {
console.error('[keet-daemon] Core error:', msg)
if (err.stack) console.error('[keet-daemon] stack:', err.stack)
if (err.cause) console.error('[keet-daemon] cause:', err.cause)
}
}
}
// Init Keet core (holds exclusive RocksDB lock)
const KeetCore = KeetCoreModule.default || KeetCoreModule
let coreResult = KeetCore.from(storage, coreOpts)
let core = coreResult.keet
let backend = core.backendAPI
await backend.ready()
console.log('[keet-daemon] Backend ready')
// Hot-swap function: close current core, open new storage
async function swapCore (newStoragePath) {
console.log('[keet-daemon] Swapping core to:', newStoragePath)
// Tear down UDS subscriptions before closing core
if (uds) uds.destroyAllSubscriptions()
await core.close()
coreResult = KeetCore.from(newStoragePath, coreOpts)
core = coreResult.keet
backend = core.backendAPI
await backend.ready()
// Update router references
router.backend = backend
router.core = core
router._storage = newStoragePath
router._vault = router._createVault(newStoragePath)
// Swap MetadataBase to new identity storage
await router.swapMetadata(newStoragePath)
console.log('[keet-daemon] Core swapped, broadcasting event')
// Notify all UDS clients
if (uds) uds.broadcastEvent('core_swapped', { storage: newStoragePath })
}
// Router + UDS transport (no HTTP — security audit remediation)
const router = new Router(backend, core, {
storage, registry, swapCore, brokenRooms
})
const socketPath = opts.socketPath || '/tmp/keet-daemon.sock'
// opts.tcp = { host, port, token }: loopback TCP instead of the Unix socket, for
// Windows (see UdsTransport). The bound port is uds.port.
var uds = new UdsTransport(router, { socketPath, tcp: opts.tcp || null })
await uds.start()
// Write PID file
try {
fs.writeFileSync(pidFile, String(Bare.pid))
} catch (err) {
console.error('[keet-daemon] Warning: could not write PID file:', err.message)
}
console.log(`[keet-daemon] Ready — ${opts.tcp ? 'TCP 127.0.0.1:' + uds.port : 'UDS ' + socketPath}, PID ${Bare.pid}`)
// Initialize MetadataBase (Autobase smart contract + legacy migration)
router.initMetadata().catch(err => {
console.error('[keet-daemon] MetadataBase init failed:', err.message)
})
// Enrich migrated registry entries with live identity data. MOVED to
// after uds.start() because `getIdentity()` blocks on keet-core's
// onlineBoot.ready(), which never resolves on fresh storage (where
// `_loadIdentity` errors with "Identity is required"). Running this
// earlier prevented UDS from ever listening, making vault.setup
// unreachable to bootstrap clients. Now non-fatal in both senses:
// can't crash the daemon AND can't block its readiness.
if (active && active.username === '') {
Promise.resolve().then(async () => {
try {
const identity = await backend.api.core.getIdentity()
if (identity) {
const fields = {}
if (identity.displayName) fields.name = identity.displayName
if (identity.username) fields.username = identity.username
if (fields.name || fields.username) {
await registry.updateMeta(active.id, fields)
console.log('[keet-daemon] Registry enriched from live identity:', fields)
}
}
} catch (err) {
console.error('[keet-daemon] Registry enrichment failed (non-fatal):', err.message)
}
}).catch(() => {})
}
return { router, uds, core, registry, pidFile }
}