Production readiness hardening and ops tooling

This commit is contained in:
2026-05-29 10:10:12 +07:00
parent e0b8f9af9a
commit 648e77cee9
68 changed files with 12222 additions and 848 deletions

View File

@ -0,0 +1,108 @@
#!/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 <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
)
);