New clients join a shared 'open' role (pub/sub on #, $CONTROL stays protected)
instead of per-client topic-scoped roles. All passwords are now stored encrypted
with admin reveal + reset endpoints so one login can be reused across device
batches. Username is the name slug (no random suffix). Legacy scoped clients
can be moved onto the open role.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A single client sometimes legitimately needs several unrelated topic trees
(e.g. hvac-backend needs both shellies/# for TRVs and hvac/mhi/# for MHI
aircon) — the previous model only supported one topic_scope per client,
forcing either an overbroad '#' grant or multiple broker logins for one app.
topic_scope is now newline/comma-parsed into a list; each pattern gets its
own ACL grant on the client's role (createDynsecClient loops per topic).
Stored newline-joined (still a single TEXT column, no migration). Added
PATCH /mqtt-clients/:id (additive: add_topic_scope) to grant a client more
topics later without revoking/recreating it — backed by a new
addTopicsToRole() in mqtt-dynsec.js.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MQTT Broker Clients gained a 'service consumer' flag. Device clients stay
show-once (a human copies the password into hardware). Service clients (a
stack app like hvac-backend that must re-read its own broker login on every
restart) get a STABLE username (= name slug, no random suffix) and their
password stored AES-256-GCM encrypted (crypto.js, same as integration
secrets), served at runtime via:
GET /settings/api/internal/mqtt-client/:username (bearer SETTINGS_SECRET)
This is the credential path the hvac plan always intended — show-once
genuinely can't feed a machine consumer. Revoke/delete drop the stored
secret; the internal endpoint refuses revoked clients; secret_enc is never
returned to the admin UI. Reject duplicate usernames up front with 409.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New /settings/api/mqtt-clients routes (list/create/revoke/delete) manage
per-consumer dynamic-security identities on the shared broker (LXC 104) —
name + topic scope + publish/subscribe flags in, generated username/password
out (shown once, never stored). Replaces manually running mosquitto_ctrl
over SSH by hand for every new device or app that needs broker access.
Implementation SSHs into the broker LXC and runs mosquitto_ctrl inside a
throwaway container on its Docker network (src/lib/ssh.js + mqtt-dynsec.js)
rather than reimplementing the dynamic-security plugin's JSON wire protocol
from scratch — reuses the exact commands verified by hand while wiring up
the water-softener and mqtt-inspector clients this session. Needs the
shared deploy SSH key mounted (Dockerfile/compose changes) and
MQTT_ADMIN_USER/PASS threaded in via stack-init.
mqtt_clients table is bookkeeping only (name/scope/active) — the broker's
own dynamic-security.json remains the source of truth for auth.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>