smart-home-engine 0.19.8 → 0.20.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-BW2J83t5.js";import{t as I}from"./index-ves1PGl3.js";/*!-----------------------------------------------------------------------------
1
+ import{m as O}from"./monaco-langs-BW2J83t5.js";import{t as I}from"./index-CZEb06G1.js";/*!-----------------------------------------------------------------------------
2
2
  * Copyright (c) Microsoft Corporation. All rights reserved.
3
3
  * Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
4
4
  * Released under the MIT license
@@ -155,10 +155,10 @@
155
155
  }
156
156
  })();
157
157
  </script>
158
- <script type="module" crossorigin src="/assets/index-ves1PGl3.js"></script>
158
+ <script type="module" crossorigin src="/assets/index-CZEb06G1.js"></script>
159
159
  <link rel="modulepreload" crossorigin href="/assets/monaco-langs-BW2J83t5.js">
160
160
  <link rel="stylesheet" crossorigin href="/assets/monaco-langs-DyX1CsEw.css">
161
- <link rel="stylesheet" crossorigin href="/assets/index-CtuJWQQl.css">
161
+ <link rel="stylesheet" crossorigin href="/assets/index-bwhJdsUd.css">
162
162
  </head>
163
163
  <body>
164
164
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-home-engine",
3
- "version": "0.19.8",
3
+ "version": "0.20.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": {
@@ -28,10 +28,10 @@
28
28
  "author": "Sebastian 'hobbyquaker' Raff <hobbyquaker@gmail.com>",
29
29
  "license": "MIT",
30
30
  "dependencies": {
31
- "bcryptjs": "^2.4.3",
32
31
  "@elastic/elasticsearch": "^9.4.2",
33
32
  "@influxdata/influxdb-client": "^1.35.0",
34
33
  "@matter/main": "^0.17.0",
34
+ "bcryptjs": "^2.4.3",
35
35
  "chokidar": "^4.0.0",
36
36
  "express": "^5.2.1",
37
37
  "ioredis": "^5.11.0",
@@ -39,6 +39,7 @@
39
39
  "node-schedule": "^2.0.0",
40
40
  "pino": "^9.0.0",
41
41
  "pino-pretty": "^13.0.0",
42
+ "semantic-compare": "^1.0.2",
42
43
  "suncalc": "^1.9.0",
43
44
  "ws": "^8.21.0",
44
45
  "yargs": "^17.0.0"
@@ -12,7 +12,7 @@ SHE_USER=she
12
12
  SERVICE_SRC="$(npm root -g)/smart-home-engine/service/smart-home-engine.service"
13
13
  SERVICE_DST=/etc/systemd/system/smart-home-engine.service
14
14
 
15
- # --- ensure sudo is installed (required for web UI restart button) --------
15
+ # --- ensure sudo is installed (required for web UI restart/update buttons) -
16
16
  if ! command -v sudo &>/dev/null; then
17
17
  echo "sudo not found, installing..."
18
18
  apt-get install -y sudo
@@ -35,9 +35,13 @@ fi
35
35
  # --- state directory (required by ReadWritePaths before first start) ------
36
36
  install -d -o "$SHE_USER" -g "$SHE_USER" -m 700 /home/she/.she
37
37
 
38
- # --- sudoers rule (allows web UI restart button to work) -----------------
38
+ # --- sudoers rules -------------------------------------------------------
39
+ NPM_BIN="$(command -v npm)"
39
40
  SUDOERS_FILE=/etc/sudoers.d/she
40
- echo "she ALL=(root) NOPASSWD: /usr/bin/systemctl restart smart-home-engine" > "$SUDOERS_FILE"
41
+ cat > "$SUDOERS_FILE" <<EOF
42
+ she ALL=(root) NOPASSWD: /usr/bin/systemctl restart smart-home-engine
43
+ she ALL=(root) NOPASSWD: $NPM_BIN install -g smart-home-engine
44
+ EOF
41
45
  chmod 440 "$SUDOERS_FILE"
42
46
  echo "created $SUDOERS_FILE"
43
47
 
package/src/index.js CHANGED
@@ -79,6 +79,7 @@ if (typeof config.port !== 'undefined') {
79
79
  auth: config.auth,
80
80
  password: config.password || null,
81
81
  proxyHeader: config.proxyHeader,
82
+ proxyLogoutUrl: config.proxyLogoutUrl || null,
82
83
  bindAddress: config.bindAddress,
83
84
  configPath: config.config,
84
85
  scriptDir: config.dir || null,
@@ -283,7 +284,13 @@ if (!config.url) {
283
284
  }
284
285
 
285
286
  if (config.url) {
286
- mqtt = modules.mqtt.connect(config.url, { will: { topic: config.name + '/connected', payload: '0', retain: true } });
287
+ const _mqttOpts = { will: { topic: config.name + '/connected', payload: '0', retain: true } };
288
+ if (config.mqttUsername) _mqttOpts.username = config.mqttUsername;
289
+ if (config.mqttPassword) _mqttOpts.password = config.mqttPassword;
290
+ if (config.mqttCa) _mqttOpts.ca = config.mqttCa;
291
+ if (config.mqttCert) _mqttOpts.cert = config.mqttCert;
292
+ if (config.mqttKey) _mqttOpts.key = config.mqttKey;
293
+ mqtt = modules.mqtt.connect(config.url, _mqttOpts);
287
294
  mqtt.publish(config.name + '/connected', '2', { retain: true });
288
295
 
289
296
  mqtt.on('connect', () => {
package/src/web/auth.js CHANGED
@@ -32,15 +32,17 @@ const _sessions = new Map(); // token (hex64) → { createdAt: number }
32
32
  let _mode = 'none';
33
33
  let _passwordHash = null; // bcrypt hash, only used in 'password' mode
34
34
  let _proxyHeader = 'x-remote-user'; // lowercase for req.headers lookup
35
+ let _proxyLogoutUrl = null; // URL to redirect to on logout in proxy mode
35
36
  let _configPath = null;
36
37
 
37
38
  /**
38
39
  * Initialise auth state. Called once from startServer().
39
40
  */
40
- function init({ auth = 'none', password = null, proxyHeader = 'X-Remote-User', configPath = null } = {}) {
41
+ function init({ auth = 'none', password = null, proxyHeader = 'X-Remote-User', proxyLogoutUrl = null, configPath = null } = {}) {
41
42
  _mode = auth;
42
43
  _passwordHash = password || null;
43
44
  _proxyHeader = proxyHeader.toLowerCase();
45
+ _proxyLogoutUrl = proxyLogoutUrl || null;
44
46
  _configPath = configPath;
45
47
  }
46
48
 
@@ -95,7 +97,9 @@ const router = express.Router();
95
97
 
96
98
  /** GET /she/auth/mode — always public */
97
99
  router.get('/mode', (req, res) => {
98
- res.json({ mode: _mode });
100
+ const r = { mode: _mode };
101
+ if (_mode === 'proxy' && _proxyLogoutUrl) r.proxyLogoutUrl = _proxyLogoutUrl;
102
+ res.json(r);
99
103
  });
100
104
 
101
105
  /** POST /she/auth/login — always public; only meaningful in password mode */
@@ -135,7 +139,7 @@ router.post('/setup', async (req, res) => {
135
139
  return res.status(401).json({ error: 'Unauthorized' });
136
140
  }
137
141
 
138
- const { mode, password, proxyHeader } = req.body || {};
142
+ const { mode, password, proxyHeader, proxyLogoutUrl } = req.body || {};
139
143
 
140
144
  if (!['none', 'password', 'proxy'].includes(mode)) {
141
145
  return res.status(400).json({ error: 'Invalid auth mode. Must be none, password, or proxy.' });
@@ -157,12 +161,14 @@ router.post('/setup', async (req, res) => {
157
161
  cfg.auth = mode;
158
162
  delete cfg.password;
159
163
  delete cfg.proxyHeader;
164
+ delete cfg.proxyLogoutUrl;
160
165
 
161
166
  if (mode === 'password') {
162
167
  cfg.password = await bcrypt.hash(password, BCRYPT_ROUNDS);
163
168
  }
164
169
  if (mode === 'proxy') {
165
170
  cfg.proxyHeader = proxyHeader || 'X-Remote-User';
171
+ if (proxyLogoutUrl) cfg.proxyLogoutUrl = proxyLogoutUrl;
166
172
  }
167
173
 
168
174
  // Write back
@@ -173,6 +179,7 @@ router.post('/setup', async (req, res) => {
173
179
  _mode = mode;
174
180
  _passwordHash = cfg.password || null;
175
181
  _proxyHeader = (cfg.proxyHeader || 'X-Remote-User').toLowerCase();
182
+ _proxyLogoutUrl = cfg.proxyLogoutUrl || null;
176
183
 
177
184
  // Invalidate all existing sessions when switching away from password mode
178
185
  if (mode !== 'password') _sessions.clear();
package/src/web/server.js CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  const express = require('express');
4
4
  const path = require('path');
5
+ const { spawnSync, spawn } = require('child_process');
6
+ const semverCompare = require('semantic-compare');
7
+ const pkg = require('../../package.json');
5
8
  const { router: configRouter } = require('./config-api');
6
9
  const { router: scriptsRouter } = require('./scripts-api');
7
10
  const { router: shedbRouter } = require('./shedb-api');
@@ -56,19 +59,37 @@ app.use('/she/ai', aiRouter);
56
59
  // When running under systemd, delegate to `sudo systemctl restart` so the
57
60
  // service actually comes back up. Otherwise fall back to exit(0) and let
58
61
  // whatever process manager is in use handle it.
62
+ function _systemdRestart() {
63
+ if (process.env.INVOCATION_ID) {
64
+ spawn('sudo', ['systemctl', 'restart', 'smart-home-engine'], { detached: true, stdio: 'ignore' }).unref();
65
+ } else {
66
+ process.exit(0);
67
+ }
68
+ }
69
+
59
70
  app.post('/she/restart', (req, res) => {
71
+ res.json({ ok: true });
72
+ setTimeout(_systemdRestart, 200);
73
+ });
74
+
75
+ // npm version check — poll once on startup and every hour
76
+ let _latestNpmVersion = null;
77
+ async function _checkNpmVersion() {
78
+ try {
79
+ const res = await fetch('https://registry.npmjs.org/smart-home-engine/latest');
80
+ const data = await res.json();
81
+ _latestNpmVersion = (data.version && semverCompare(pkg.version, data.version) < 0) ? data.version : null;
82
+ } catch { /* best-effort */ }
83
+ }
84
+ _checkNpmVersion();
85
+ setInterval(_checkNpmVersion, 60 * 60 * 1000);
86
+
87
+ // Update — install latest npm package, then restart
88
+ app.post('/she/update', (req, res) => {
60
89
  res.json({ ok: true });
61
90
  setTimeout(() => {
62
- if (process.env.INVOCATION_ID) {
63
- // Running under systemd
64
- require('child_process').spawn(
65
- 'sudo',
66
- ['systemctl', 'restart', 'smart-home-engine'],
67
- { detached: true, stdio: 'ignore' },
68
- ).unref();
69
- } else {
70
- process.exit(0);
71
- }
91
+ spawnSync('sudo', ['npm', 'install', '-g', 'smart-home-engine'], { stdio: 'inherit' });
92
+ _systemdRestart();
72
93
  }, 200);
73
94
  });
74
95
 
@@ -78,7 +99,9 @@ function setStatsProvider(fn) {
78
99
  _getStats = fn;
79
100
  }
80
101
  app.get('/she/status', (req, res) => {
81
- res.json(_getStats ? _getStats() : { scripts: 0, topics: 0 });
102
+ const s = _getStats ? _getStats() : { scripts: 0, topics: 0 };
103
+ if (_latestNpmVersion) s.latestVersion = _latestNpmVersion;
104
+ res.json(s);
82
105
  });
83
106
 
84
107
  // Serve the built Svelte SPA from dist/web/
@@ -167,7 +190,7 @@ let httpServer = null;
167
190
  /**
168
191
  * Start listening. Resolves with the actual port (useful when port 0 is given).
169
192
  * @param {number} port
170
- * @param {{ auth?: string, password?: string, proxyHeader?: string, bindAddress?: string, configPath?: string, scriptDir?: string }} [options]
193
+ * @param {{ auth?: string, password?: string, proxyHeader?: string, proxyLogoutUrl?: string, bindAddress?: string, configPath?: string, scriptDir?: string }} [options]
171
194
  * @returns {Promise<number>}
172
195
  */
173
196
  function startServer(port, options = {}) {
@@ -175,6 +198,7 @@ function startServer(port, options = {}) {
175
198
  auth: options.auth || 'none',
176
199
  password: options.password || null,
177
200
  proxyHeader: options.proxyHeader || 'X-Remote-User',
201
+ proxyLogoutUrl: options.proxyLogoutUrl || null,
178
202
  configPath: options.configPath || null,
179
203
  });
180
204
  if (options.configPath) {