A reliability, security, and accessibility hardening release — no breaking changes. It tightens RBAC on
write endpoints, patches the ws/qs advisories, makes the busy message path and graceful shutdown
crash-resistant, fixes bulk-message terminal status, finally honors LOG_LEVEL, adds audit-log and
webhook-job retention, and improves dashboard accessibility and load-error states.
⚠️ RBAC tightening (action may be required): write endpoints for groups, contacts, labels, channels,
catalog, and status now require theOPERATORrole. If you used aVIEWERkey for any of these writes,
switch it toOPERATOR(orADMIN). Everything else is backward-compatible.
Security
- Write endpoints for groups, contacts, labels, channels, catalog, and status now require the
OPERATORrole, closing an unintended privilege gap where aVIEWER-role API key could create/leave
groups, manage participants, block contacts, post statuses, send products, and mutate labels. Read
(GET) endpoints remain open to any valid key, matching the message/session controllers.⚠️ If you used a
VIEWERkey for any of these write operations, switch it toOPERATOR(orADMIN). - Patched a high-severity
wsadvisory (and a moderateqsDoS) on the live socket.io transport by
bumping in-range deps (ws→8.21.0,engine.io→6.6.9,qs→6.15.2, plus the incidental
re-resolutionsnpm audit fixpulled in) in both the API and dashboard. Lockfile-only — no
package.json/API change. The remaining advisories are build-only (sqlite3→node-gyp→tar)
and require a breakingsqlite3major, deferred.
Added
LOG_LEVELis now honored. It was read into config/compose but never applied (logging was hardcoded
toinfo); the level (error/warn/info/debug/verbose) is now set at bootstrap.- Automatic audit-log retention. Audit logs older than
AUDIT_RETENTION_DAYS(default 90;0disables)
are pruned daily and once at startup — the existingcleanup()was never scheduled, soaudit_logsgrew
without bound.
Fixed
- Bulk-message batch status is now correct on cancel and stop-on-error. A cancelled batch could be
silently reverted toPROCESSING(the final save overwrote theCANCELLEDstatus with the stale
in-memory one), and astopOnErrorabort was reported asCOMPLETEDwhenever at least one message had
already been sent. The terminal status is now re-derived (cancelled →CANCELLEDwith reconciled
counters; stop-on-error →FAILED; otherwiseCOMPLETED/FAILED). - Bulk-message item
typeis now validated against the allowed set (text/image/video/audio/document)
with@IsIn, so an invalid type is rejected up front instead of failing mid-send. - Graceful shutdown is now robust.
onModuleDestroyclears reconnect timers first and destroys engines
in parallel, each isolated and time-bounded — so one hung/throwing Chromium can no longer abort teardown
of the other sessions or stall shutdown. - A session that exhausts its reconnect attempts is now marked
FAILEDwith a reason (surfaced via
lastError) instead of sitting silentlyDISCONNECTEDforever. - BullMQ webhook jobs are auto-evicted (
removeOnComplete/removeOnFailretention) so completed/failed
job payloads no longer accumulate unbounded in Redis (audit M19). - Engine-event handlers no longer risk unhandled promise rejections. Webhook dispatch is now
self-contained (a failed webhook lookup is logged and swallowed, not rejected into the fire-and-forget
callers), theonMessage/onMessageCreatehook chains carry a.catch(), and a process-level
unhandledRejectionbackstop logs (instead of crashing) anything that still slips through. A transient
DB hiccup on the busy message path can no longer drop the event silently or take the process down. - Audit-log writes are best-effort. A failed audit insert is logged and swallowed instead of turning
an otherwise-successful operation (create/delete/start/stop session, etc.) into a500. - Dashboard accessibility: toast notifications are now an ARIA live region (
role="region"/aria-live,
withrole="alert"on error/warning toasts) so screen readers announce success/error feedback, and the
toast close button has an accessible name. The API-key visibility toggles on the Login and API Keys pages
now have state-reflectingaria-labels (show/hide). Newcommon.showApiKey/common.hideApiKeystrings
across all locales. - Dashboard no longer shows a misleading "nothing here" empty state when a list fetch fails. The
Webhooks, API Keys, and Logs pages discarded the query error and rendered the empty state on failure;
they now surface an accessible error banner (role="alert") so the user knows the data failed to load.