#!/usr/bin/env node import { spawnSync } from "node:child_process"; import "dotenv/config"; const args = process.argv.slice(2); function getArg(name) { const index = args.indexOf(name); return index >= 0 ? args[index + 1] : undefined; } function hasFlag(name) { return args.includes(name); } function usage() { console.log(`Usage: node scripts/provision-mqtt-device.mjs --device-id [--base-url http://127.0.0.1:3000] [--apply-local] Options: --device-id Device UUID from the platform. --base-url Running backend URL. Default: BASE_URL env or http://127.0.0.1:3000. --apply-local Run mosquitto_passwd locally. Use only on the broker host. Environment: ADMIN_TOKEN Admin bearer token. Default: admin-dev-token. MOSQUITTO_PASSWD_FILE Default: /etc/mosquitto/passwd. MOSQUITTO_ACL_FILE Default: /etc/mosquitto/acl. `); } const deviceId = getArg("--device-id"); const baseUrl = getArg("--base-url") || process.env.BASE_URL || "http://127.0.0.1:3000"; const adminToken = process.env.ADMIN_TOKEN || "admin-dev-token"; const passwdFile = process.env.MOSQUITTO_PASSWD_FILE || "/etc/mosquitto/passwd"; const aclFile = process.env.MOSQUITTO_ACL_FILE || "/etc/mosquitto/acl"; const applyLocal = hasFlag("--apply-local"); if (!deviceId || hasFlag("--help") || hasFlag("-h")) { usage(); process.exit(deviceId ? 0 : 1); } function shellQuote(value) { return `'${String(value).replace(/'/g, "'\\''")}'`; } async function rotateCredential() { const response = await fetch(`${baseUrl}/admin/devices/${deviceId}/credentials/rotate`, { method: "POST", headers: { Authorization: `Bearer ${adminToken}`, "Content-Type": "application/json" }, body: "{}" }); const payload = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(payload?.message || payload?.code || `rotate failed with status ${response.status}`); } return payload.data; } function applyMosquittoPassword(username, password) { const result = spawnSync("sudo", ["mosquitto_passwd", "-b", passwdFile, username, password], { stdio: "inherit" }); if (result.status !== 0) { throw new Error(`mosquitto_passwd failed with status ${result.status}`); } } const data = await rotateCredential(); const username = data.credential.mqtt_username; const password = data.credential.mqtt_password; if (applyLocal) { applyMosquittoPassword(username, password); } console.log( JSON.stringify( { device_id: data.device.id, mqtt_username: username, mqtt_password: password, one_time_secret: true, applied_local: applyLocal, mosquitto_commands: [ `sudo mosquitto_passwd -b ${shellQuote(passwdFile)} ${shellQuote(username)} ${shellQuote(password)}`, `node scripts/check-mqtt-acl.mjs --file ${shellQuote(aclFile)} || node scripts/check-mqtt-acl.mjs --print-template`, "sudo chown root:mosquitto /etc/mosquitto/passwd", "sudo chmod 640 /etc/mosquitto/passwd", "sudo systemctl reload mosquitto" ], topic_scope: [ `device publish/readwrite: devices/${username}/uplink/#`, `device subscribe/read: devices/${username}/downlink/#`, "backend read: devices/+/uplink/#", "backend write: devices/+/downlink/#" ] }, null, 2 ) );