// lavinmq's run-once bootstrap — lavinmq's own code (novox/hq ADR 0039), run once before the broker // first starts (ADR 0052). lavinmq's default admin is set at first boot from a config file's // `default_password_hash`, and that value is a HASH of the mesh-minted admin password, not the // password itself — a form the mesh's plain-secret delivery cannot produce and no `${secret:...}` // placeholder can compute. So this step computes it: it reads the admin password the mesh minted and // the host unsealed, hashes it the way lavinmq expects (see client.rabbitHash), and writes the config // file the broker container reads with `--config`. The host runs it to completion and only then // starts the broker the manifest places after it — so the broker's first boot finds a config with an // admin it can authenticate, and the provisioner (which reaches the management API as that admin) // can do its work. // // It runs in the module's own runtime image, under the module's own account, as `mesh-tools run` // imports it — no broker connection, because writing a config file is an offline operation and there // is no broker to reach yet. // // lavinmq consults `default_user`/`default_password_hash` only on a first boot with an empty data // dir; a later boot uses the persisted user database and ignores them. So this seeds the admin once, // and a rotation of the admin secret does not re-key an already-initialised broker — the same // first-boot-only shape the RabbitMQ-compatible default user has always had. import { writeFileSync } from "node:fs"; import { readFileSync } from "node:fs"; import { rabbitHash } from "../client.js"; const adminUser = process.env.MESH_PROVISION_ADMIN_USER ?? process.env.MESH_LAVINMQ_ADMIN_USER ?? "mesh-admin"; const passwordFile = process.env.MESH_PROVISION_PASSWORD_FILE ?? process.env.MESH_LAVINMQ_ADMIN_PASSWORD_FILE ?? "/run/secrets/default"; const configOut = process.env.MESH_LAVINMQ_CONFIG_OUT ?? "/var/lib/lavinmq-module/lavinmq.ini"; const dataDir = process.env.MESH_LAVINMQ_DATA_DIR ?? "/var/lib/lavinmq"; const password = readFileSync(passwordFile, "utf8").replace(/\n$/, ""); if (!password) { throw new Error(`[lavinmq:bootstrap] admin password file ${passwordFile} is empty — cannot seed the admin`); } // The broker reads only what it needs to authenticate its admin on first boot; bind/ports come from // the container's entrypoint (`-b 0.0.0.0`), so this file names the admin and nothing else about the // network. const ini = "[main]\n" + `data_dir = ${dataDir}\n` + `default_user = ${adminUser}\n` + `default_password_hash = ${rabbitHash(password)}\n`; writeFileSync(configOut, ini, { mode: 0o600 }); console.log(`[lavinmq:bootstrap] wrote ${configOut} with admin '${adminUser}' (password hashed for lavinmq)`);