Live Config, Audit Closures & Clean-Cut Plugin API
v0.6.0 retires the leak-prone .load / .unload / .reload plugin lifecycle in favour of a three-scope live settings registry (core, plugin:<id>, chanset), folds .helpset into a unified help corpus, lands the spotify-radio plugin, and closes a 2026-05-10 stability audit (3 CRITICAL + 89 WARNING + 55 INFO) and security audit (1 CRITICAL + 28 WARNING).
Breaking
- `.load` / `.unload` / `.reload` removed — plugin enable/disable now flows through
.set core plugins.<id>.enabled true|false;.restartis the canonical path for picking up code edits. The cache-busting import path that powered.reloadis deleted — Node ESM has no eviction API, and every cache-busted re-import minted a permanent module-graph entry. - `database`, `pluginDir`, `owner.handle`, `owner.hostmask` removed from `bot.json` — they are bootstrap env vars now:
HEX_DB_PATH,HEX_PLUGIN_DIR,HEX_OWNER_HANDLE,HEX_OWNER_HOSTMASK. The strict-object schema rejects legacy files with a hint pointing at the env var to set. - `api.config` removed from PluginAPI — every shipped plugin migrated to
api.settings— typed defs viaapi.settings.register([...])and reads viagetString/getInt/getFlag. Deeply-nested config falls back toapi.settings.bootConfig. - `.helpset` removed — folded into
.help set <scope> [<key>]against a unifiedHelpRegistrycorpus indexed across core commands, plugin commands, and all three settings scopes. - `plugin-load` / `plugin-unload` / `plugin-reload` mod_log action strings retired — historical rows remain queryable; new rows use
coreset-set/pluginset-set/chanset-set/rehash/restart.
Added
pnpm run spotify:auth for the one-time OAuth flow.chanset pattern to core (bot-wide live config), plugin:<id>, and chanset. KV-canonical-after-first-boot: bot.json / plugins.json are first-run seeds, operator .set / .unset / .rehash writes win.auditActor(ctx) so REPL / IRC / DCC / botlink-relay all converge on the same mod_log shape.@reload:live|reload|restart; .set echoes the class as a hint — (applied live) / (applied; subsystem reloaded) / (stored; takes effect after .restart).api.settings.bootConfig exposes a frozen merged JSON snapshot for config that doesn't flatten to typed settings.!help ban lookups. .help index is permission-filtered for unprivileged DCC/IRC users; REPL and botlink remain unfiltered.bindsByPlugin tally in the dispatcher./tmp/.hexbot-alive) and readiness (/tmp/.hexbot-connected) sentinels distinguish process-wedged from IRC-unreachable.Changed
- `src/bot.ts` split into focused modules — -26% LOC with no behavior change; core settings defs co-locate their
onChangehandlers, kv-maintenance and audit-fallback become small classes,connect()/start()/shutdown()reshaped into named phases. - Public `types/` `.d.ts` synced with runtime API — plugin authors relying on public declarations for IDE autocomplete were missing
audit,util,settings,coreSettings,banStoreand half the recent additions toPluginAPI/HandlerContext/ChannelState. - Plugin example configs minimised — every key whose value the plugin's own schema already supplies is removed; only operator-required and security-relevant keys remain. ai-chat and rss flip to
enabled: falseso the example doesn't auto-post on first boot.
Fixed
- 2026-05-10 stability audit — 3 CRITICAL + 89 WARNING + 55 INFO closed. Every mutating IRC verb now flows through the message queue (chanmod recovery storms can't trip Excess Flood); fatal SQLite errors hand control to
bot.shutdown()instead of bypassing teardown viaprocess.exit(2);ensureChannelstops growing unboundedly under stray TOPIC / RPL_CHANNELMODEIS. - STS upgrade plaintext-leak window —
messageQueue.clear()now fires BEFOREclient.quit()on STS upgrade so the close-timeflushWithDeadline(100)can't drain queued PRIVMSGs (potentially containing.adduserpassword material or plugin tokens) over plaintext between upgrade decision and TLS reconnect. - 2026-05-10 security audit — 1 CRITICAL + 28 WARNING closed. Deep-freeze
identity.require_acc_forandbootConfigso a plugin can't disable NickServ verification bot-wide;api.say/notice/actionroute throughsplitMessage+ target sanitize; STS rejects duration-only directives over plaintext;.ban/.unbanthreadauditActorso exactly onemod_logrow attributes to the caller. - `importWithCacheBust` ESM-cache leak resolved by deletion —
load()uses a plainawait import(pathToFileURL(absPath).href);importedOnce,reload(name), and theplugin:reloaded/plugin:reload_failedevents are gone. - `kv` VACUUM spam from 32-bit `setInterval` overflow — the 30-day delay (2.59 × 10⁹ ms) exceeded Node's
TIMEOUT_MAX(~24.8 days), which clamps to 1 ms and fires continuously on startup. VACUUM folds into the daily maintenance handler with an elapsed-time check. - First-boot plugin double-load race — seeding
core.plugins.<id>.enabledinto KV pre-load fired theonChangelistener, which fire-and-forgetsapplyPluginEnabled()and raced the awaited main load loop. Seed moves to after the loop whereapplyPluginEnabledis idempotent. - Memleak audit closure — caps on
pendingHandshakes(4096),SharedBanListdistinct channels (1024),RelayOrchestratorvirtual sessions (64/leaf), and floodchannelActionRate(1024 keys, oldest-by-insertion eviction); timers stored in fields and cleared on teardown; DCC /IRCBridge.attach()/Logger.addSinkByOwnerlistener lifecycles made symmetric.
Removed
- Closed audit documents —
docs/audits/stability-all-2026-05-10.mdand friends deleted after every finding was resolved; commit history retains the reasoning.
See CHANGELOG.md for the full list of changes.