smart-home-engine 1.16.1 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import{m as O}from"./monaco-langs-Dt8a9eeo.js";import{t as I}from"./index-BIE09pOh.js";/*!-----------------------------------------------------------------------------
1
+ import{m as O}from"./monaco-langs-Dt8a9eeo.js";import{t as I}from"./index-CvkGKRP_.js";/*!-----------------------------------------------------------------------------
2
2
  * Copyright (c) Microsoft Corporation. All rights reserved.
3
3
  * Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
4
4
  * Released under the MIT license
@@ -172,10 +172,10 @@
172
172
  }
173
173
  })();
174
174
  </script>
175
- <script type="module" crossorigin src="/assets/index-BIE09pOh.js"></script>
175
+ <script type="module" crossorigin src="/assets/index-CvkGKRP_.js"></script>
176
176
  <link rel="modulepreload" crossorigin href="/assets/monaco-langs-Dt8a9eeo.js">
177
177
  <link rel="stylesheet" crossorigin href="/assets/monaco-langs-DyX1CsEw.css">
178
- <link rel="stylesheet" crossorigin href="/assets/index-B3w0pv6a.css">
178
+ <link rel="stylesheet" crossorigin href="/assets/index-BvKmwc2a.css">
179
179
  </head>
180
180
  <body>
181
181
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-home-engine",
3
- "version": "1.16.1",
3
+ "version": "1.17.1",
4
4
  "description": "Node.js based script runner for use in MQTT based Smart Home environments",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -950,6 +950,150 @@ router.get('/logs', (req, res) => {
950
950
  res.json(limit >= all.length ? all : all.slice(-limit));
951
951
  });
952
952
 
953
+ // ── Password file management ──────────────────────────────────────────────────
954
+
955
+ /** POSIX shell-safe single-quote escaping for SSH commands */
956
+ function shellArg(s) {
957
+ return "'" + String(s).replace(/'/g, "'\\''") + "'";
958
+ }
959
+
960
+ /**
961
+ * GET /she/broker/passwd?file=<path>
962
+ * List usernames from a mosquitto password file.
963
+ */
964
+ router.get('/passwd', async (req, res) => {
965
+ try {
966
+ const bc = getBrokerConfig(req);
967
+ const file = String(req.query.file || '').trim();
968
+ if (!file || !file.startsWith('/')) return res.status(400).json({ error: 'absolute file path required' });
969
+
970
+ let text = '';
971
+ if (bc.ssh && bc.ssh.host) {
972
+ const result = await sshDeploy.runCommand(bc.ssh, `sudo cat -- ${shellArg(file)} 2>/dev/null || true`);
973
+ text = result.stdout;
974
+ } else {
975
+ try {
976
+ text = fs.readFileSync(file, 'utf8');
977
+ } catch (e) {
978
+ if (e.code !== 'ENOENT') throw e;
979
+ }
980
+ }
981
+ const users = text
982
+ .split('\n')
983
+ .filter((l) => l.includes(':'))
984
+ .map((l) => l.split(':')[0])
985
+ .filter(Boolean);
986
+ res.json({ users });
987
+ } catch (err) {
988
+ handleError(res, err);
989
+ }
990
+ });
991
+
992
+ /**
993
+ * POST /she/broker/passwd
994
+ * Add or update a user in a mosquitto password file.
995
+ * Body: { file, username, password }
996
+ */
997
+ router.post('/passwd', async (req, res) => {
998
+ try {
999
+ const bc = getBrokerConfig(req);
1000
+ const { file, username, password } = req.body;
1001
+ if (!file || typeof file !== 'string' || !file.startsWith('/')) return res.status(400).json({ error: 'absolute file path required' });
1002
+ if (!username || typeof username !== 'string') return res.status(400).json({ error: 'username required' });
1003
+ if (!password || typeof password !== 'string') return res.status(400).json({ error: 'password required' });
1004
+ if (/[:\n\r]/.test(username)) return res.status(400).json({ error: 'invalid username' });
1005
+
1006
+ if (bc.ssh && bc.ssh.host) {
1007
+ const cmd = `sudo mosquitto_passwd -b ${shellArg(file)} ${shellArg(username)} ${shellArg(password)}`;
1008
+ await sshDeploy.runCommand(bc.ssh, cmd);
1009
+ } else {
1010
+ await execFileAsync('mosquitto_passwd', ['-b', file, username, password]);
1011
+ }
1012
+ res.json({ ok: true });
1013
+ } catch (err) {
1014
+ handleError(res, err);
1015
+ }
1016
+ });
1017
+
1018
+ /**
1019
+ * DELETE /she/broker/passwd/:user
1020
+ * Delete a user from a mosquitto password file.
1021
+ * Body: { file }
1022
+ */
1023
+ router.delete('/passwd/:user', async (req, res) => {
1024
+ try {
1025
+ const bc = getBrokerConfig(req);
1026
+ const { file } = req.body;
1027
+ const username = req.params.user;
1028
+ if (!file || typeof file !== 'string' || !file.startsWith('/')) return res.status(400).json({ error: 'absolute file path required' });
1029
+
1030
+ if (bc.ssh && bc.ssh.host) {
1031
+ const cmd = `sudo mosquitto_passwd -D ${shellArg(file)} ${shellArg(username)}`;
1032
+ await sshDeploy.runCommand(bc.ssh, cmd);
1033
+ } else {
1034
+ await execFileAsync('mosquitto_passwd', ['-D', file, username]);
1035
+ }
1036
+ res.json({ ok: true });
1037
+ } catch (err) {
1038
+ handleError(res, err);
1039
+ }
1040
+ });
1041
+
1042
+ // ── ACL file management ───────────────────────────────────────────────────────
1043
+
1044
+ /**
1045
+ * GET /she/broker/acl?file=<path>
1046
+ * Read a mosquitto ACL file (static file, not dynsec).
1047
+ */
1048
+ router.get('/acl', async (req, res) => {
1049
+ try {
1050
+ const bc = getBrokerConfig(req);
1051
+ const file = String(req.query.file || '').trim();
1052
+ if (!file || !file.startsWith('/')) return res.status(400).json({ error: 'absolute file path required' });
1053
+
1054
+ let content = '';
1055
+ if (bc.ssh && bc.ssh.host) {
1056
+ try {
1057
+ content = await sshDeploy.readRemoteFile(bc.ssh, file);
1058
+ } catch (e) {
1059
+ if (!String(e.message).toLowerCase().includes('no such file')) throw e;
1060
+ }
1061
+ } else {
1062
+ try {
1063
+ content = fs.readFileSync(file, 'utf8');
1064
+ } catch (e) {
1065
+ if (e.code !== 'ENOENT') throw e;
1066
+ }
1067
+ }
1068
+ res.json({ content });
1069
+ } catch (err) {
1070
+ handleError(res, err);
1071
+ }
1072
+ });
1073
+
1074
+ /**
1075
+ * PUT /she/broker/acl
1076
+ * Write a mosquitto ACL file.
1077
+ * Body: { file, content }
1078
+ */
1079
+ router.put('/acl', async (req, res) => {
1080
+ try {
1081
+ const bc = getBrokerConfig(req);
1082
+ const { file, content } = req.body;
1083
+ if (!file || typeof file !== 'string' || !file.startsWith('/')) return res.status(400).json({ error: 'absolute file path required' });
1084
+ if (typeof content !== 'string') return res.status(400).json({ error: 'content must be a string' });
1085
+
1086
+ if (bc.ssh && bc.ssh.host) {
1087
+ await sshDeploy.uploadContent(bc.ssh, content, file);
1088
+ } else {
1089
+ fs.writeFileSync(file, content, 'utf8');
1090
+ }
1091
+ res.json({ ok: true });
1092
+ } catch (err) {
1093
+ handleError(res, err);
1094
+ }
1095
+ });
1096
+
953
1097
  module.exports = { router, setLogger, setStore };
954
1098
 
955
1099
  // ── dynsec: ACL topic inspection ──────────────────────────────────────────────