Files
mesh-catalog/modules/lavinmq/provisioner/index.ts
T
jschoubben 97707480d6 Add lavinmq amqp provider and amqp-ping consumer
lavinmq becomes a provider of a user-facing amqp interface: a consumer
that requires a message queue is given its OWN broker — a scoped vhost
and user on a lavinmq provider — not an account on the mesh's own
control-plane broker (ADR 0048). Vhost-per-login is the isolation model,
the exact analog of postgres's database-per-login: the provider names a
vhost after the consumer's login and a user with full rights on that
vhost and none elsewhere, so a login is a broker the consumer alone can
reach.

The provider drives lavinmq through its HTTP management API (client.ts,
the module's one impure seam), with a run-once bootstrap that computes
the RabbitMQ-compatible password hash lavinmq's config wants from the
plain admin secret the mesh mints — the value no ${secret:...}
placeholder can produce and the reason the bootstrap exists (ADR 0052).
serves.amqp carries the port so consumers reference ${bound:amqp:port}.

amqp-ping is a demo consumer: it contributes nothing (the vhost is the
login), reads its grant from an env-file the mesh fills, and uses
${bound:amqp:as} for BOTH its username and its vhost — the db-name
lesson applied to AMQP. It speaks AMQP 0-9-1 over a raw socket with no
npm dependency (the way redis speaks RESP) and round-trips one message.
It carries a slug so its identity fits the 20-char backend bound
(ADR 0049).

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-06 15:50:19 +02:00

52 lines
2.5 KiB
TypeScript

// lavinmq's provisioner — the adapter that makes lavinmq a provider of the mesh `amqp` 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 lavinmq creates and removes a consumer's own
// broker (novox/hq ADR 0039/0040/0048).
//
// The `amqp` interface: a consumer connects as `as` with the password the mesh minted, to a vhost
// named for that same login — its own message broker, isolated from every other consumer's by the
// vhost boundary. It is a broker of its own, not a shared account on the mesh's control-plane broker.
//
// **The login and password are the mesh's, not the provisioner's (novox/hq 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. lavinmq creates exactly that user with exactly that password — a name or password the
// provisioner invented is one the consumer could never present.
//
// Vhost-per-login is the isolation model, the exact analog of postgres's database-per-login: the
// consumer owns one vhost, named for its login, and a user with full rights on that vhost and no
// rights anywhere else. lavinmq enforces it — a user with no permission on `/` is refused the moment
// it opens that vhost (`NOT_ALLOWED`), so a login is a broker the consumer alone can reach.
import { runProvisioner, type Provision } from "@novox/mesh-sdk/provisioner";
import { emit } from "@novox/mesh-sdk/events";
import { LavinmqClient } from "../client.js";
const lavinmq = LavinmqClient.fromEnv();
/** Emit a lifecycle event without letting a broker hiccup fail the provisioning itself. */
async function announce(type: string, body: Record<string, string>): Promise<void> {
try {
await emit(type, body);
} catch (err) {
console.error(`[provisioner:amqp] emit ${type} failed: ${err}`);
}
}
runProvisioner("amqp", {
async create(p: Provision): Promise<void> {
// The vhost and the user share the consumer's login, so one cannot reach another's broker.
await lavinmq.waitReady();
await lavinmq.createConsumer(p.as, p.password);
await announce("module.lavinmq.amqp.provisioned", {
consumer: p.consumer ?? "",
user: p.as,
vhost: p.as,
});
},
async remove(p: { as: string }): Promise<void> {
await lavinmq.removeConsumer(p.as);
await announce("module.lavinmq.amqp.deprovisioned", { user: p.as, vhost: p.as });
},
});