// mosquitto's provisioner — the adapter that makes mosquitto a provider of the mesh `mqtt-topic` // interface. The reconcile loop, the contributions file, and reading the mesh's minted password are // the sdk harness's; this writes only the per-service half: how mosquitto creates and removes a // per-consumer MQTT client (novox/hq ADR 0039/0040/0048). // // The `mqtt-topic` interface: a consumer connects as `as` with the password the mesh minted, and // publishes and subscribes under `/#`, isolated from every other consumer by a Dynamic Security // role scoped to exactly that subtree. // // **The login and password are the mesh's, not the provisioner's (ADR 0048).** The mesh derives the // login and hands it to both ends so they agree, and mints the password and delivers a copy to each. // mosquitto creates exactly that client with exactly that password — a name or password the // provisioner invented is one the consumer could never present. import { runProvisioner, type Provision } from "@novox/mesh-sdk/provisioner"; import { emit } from "@novox/mesh-sdk/events"; import { MosquittoClient } from "../client.js"; const mosquitto = MosquittoClient.fromEnv(); /** Emit a lifecycle event without letting a broker hiccup fail the provisioning itself. */ async function announce(type: string, body: Record): Promise { try { await emit(type, body); } catch (err) { console.error(`[provisioner:mqtt-topic] emit ${type} failed: ${err}`); } } runProvisioner("mqtt-topic", { async create(p: Provision): Promise { // The topic subtree is scoped to the consumer's own login, so one cannot read another's topics. const topicPrefix = p.as; await mosquitto.createScopedClient(p.as, p.password, topicPrefix); await announce("topic.provisioned", { consumer: p.consumer ?? "", username: p.as, topicPrefix, }); }, async remove(p: { as: string }): Promise { await mosquitto.deleteScopedClient(p.as); await announce("topic.deprovisioned", { username: p.as }); }, // Asked every minute by the harness: whether the backend still holds this consumer exactly as // the mesh gave it, so a login lost behind the provisioner's back is made again (novox/hq issue 120). async holds(p: Provision): Promise { return mosquitto.holdsClient(p.as, p.password); }, });