smart-home-engine 1.1.6 → 1.1.9

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-DRKYxwDj.js";/*!-----------------------------------------------------------------------------
1
+ import{m as O}from"./monaco-langs-BW2J83t5.js";import{t as I}from"./index-CZNc_m6c.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,7 +172,7 @@
172
172
  }
173
173
  })();
174
174
  </script>
175
- <script type="module" crossorigin src="/assets/index-DRKYxwDj.js"></script>
175
+ <script type="module" crossorigin src="/assets/index-CZNc_m6c.js"></script>
176
176
  <link rel="modulepreload" crossorigin href="/assets/monaco-langs-BW2J83t5.js">
177
177
  <link rel="stylesheet" crossorigin href="/assets/monaco-langs-DyX1CsEw.css">
178
178
  <link rel="stylesheet" crossorigin href="/assets/index-iTrR9H4f.css">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-home-engine",
3
- "version": "1.1.6",
3
+ "version": "1.1.9",
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": {
package/src/index.js CHANGED
@@ -146,6 +146,7 @@ const scheduler = modules['node-schedule'];
146
146
  const StateStore = require('./lib/state-store');
147
147
  const sandboxModules = [];
148
148
  const store = new StateStore();
149
+ if (typeof config.port !== 'undefined') require('./web/broker-api').setStore(store);
149
150
  const scripts = {};
150
151
  const scriptOrigins = new Map(); // file → 'builtin' | 'user'
151
152
  const subscriptions = [];
@@ -361,6 +362,7 @@ if (config.url) {
361
362
  log.info('mqtt connected ' + config.url);
362
363
  log.debug('mqtt subscribe #');
363
364
  mqtt.subscribe('#');
365
+ mqtt.subscribe('$SYS/#');
364
366
  mqttEventCallbacks.filter((c) => c.event === 'connect').forEach((c) => c.callback());
365
367
 
366
368
  if (!_started) {
package/src/lib/dynsec.js CHANGED
@@ -46,9 +46,13 @@ function _drain() {
46
46
  const timer = setTimeout(() => {
47
47
  _inflight = false;
48
48
  _inflightResolve = null;
49
- if (_log) _log.debug(`dynsec: timeout waiting for response to "${command}" (${_timeout}ms)`);
49
+ if (_log) _log.warn(`dynsec: timeout waiting for response to "${command}" (${_timeout}ms) — is the dynsec plugin loaded and the admin user configured?`);
50
50
  reject(new Error(`dynsec timeout waiting for response to "${command}"`));
51
- _drain();
51
+ // Fail-fast: reject all remaining queued commands since the broker is not responding
52
+ while (_queue.length > 0) {
53
+ const queued = _queue.shift();
54
+ queued.reject(new Error(`dynsec: aborting "${queued.command}" — previous command timed out`));
55
+ }
52
56
  }, _timeout);
53
57
 
54
58
  _inflightResolve = (responses) => {
@@ -122,11 +126,15 @@ function init(config, log) {
122
126
  _client = mqtt.connect(config.url, opts);
123
127
 
124
128
  _client.on('connect', () => {
125
- _connected = true;
126
- _log.info('dynsec: connected as', dynsecCfg.adminUsername);
129
+ _log.info('dynsec: MQTT connect event, subscribing to response topic');
127
130
  _client.subscribe(RESPONSE_TOPIC, (err) => {
128
- if (err) _log.error('dynsec: failed to subscribe to response topic:', err.message);
129
- else _drain(); // flush any requests queued before connection
131
+ if (err) {
132
+ _log.error('dynsec: failed to subscribe to response topic:', err.message);
133
+ } else {
134
+ _connected = true;
135
+ _log.info('dynsec: ready — subscribed as', dynsecCfg.adminUsername);
136
+ _drain(); // flush any requests queued before connection
137
+ }
130
138
  });
131
139
  });
132
140
 
@@ -16,6 +16,7 @@
16
16
  const express = require('express');
17
17
  const path = require('path');
18
18
  const fs = require('fs');
19
+ const crypto = require('crypto');
19
20
  const dynsec = require('../lib/dynsec');
20
21
  const mosquittoConf = require('../lib/mosquitto-conf');
21
22
  const ca = require('../lib/ca');
@@ -28,12 +29,18 @@ const DEFAULT_SSH_KEY = path.join(sheConfig['data-dir'], 'ssh', 'broker_id_ed255
28
29
  const router = express.Router();
29
30
 
30
31
  let _log = null;
32
+ let _store = null;
31
33
 
32
34
  /** Must be called once from index.js so broker-api can emit debug-level log lines. */
33
35
  function setLogger(log) {
34
36
  _log = log;
35
37
  }
36
38
 
39
+ /** Must be called once from index.js to give broker-api access to the MQTT state store. */
40
+ function setStore(store) {
41
+ _store = store;
42
+ }
43
+
37
44
  // ── Helpers ────────────────────────────────────────────────────────────────────
38
45
 
39
46
  /** Get broker config from live config.json */
@@ -71,13 +78,14 @@ function handleError(res, err) {
71
78
  */
72
79
  router.get('/status', (req, res) => {
73
80
  const ds = dynsec.getStatus();
74
- const mqttState = req.app.locals.mqttState || {};
75
81
 
76
- const sysPrefixes = ['$SYS/broker/version', '$SYS/broker/clients/', '$SYS/broker/uptime'];
77
82
  const sys = {};
78
- for (const [topic, entry] of Object.entries(mqttState)) {
79
- if (sysPrefixes.some((p) => topic.startsWith(p))) {
80
- sys[topic] = entry;
83
+ if (_store) {
84
+ const sysPrefixes = ['$SYS/broker/version', '$SYS/broker/uptime', '$SYS/broker/clients/', '$SYS/broker/messages/'];
85
+ for (const [topic, entry] of _store.mqttEntries()) {
86
+ if (sysPrefixes.some((p) => topic.startsWith(p))) {
87
+ sys[topic] = entry;
88
+ }
81
89
  }
82
90
  }
83
91
 
@@ -89,14 +97,22 @@ router.get('/status', (req, res) => {
89
97
  /**
90
98
  * GET /she/broker/config
91
99
  * Returns parsed config structure + raw text + checksum.
100
+ * In remote mode, reads mosquitto.conf from the broker host via SSH.
92
101
  */
93
- router.get('/config', (req, res) => {
102
+ router.get('/config', async (req, res) => {
94
103
  try {
95
104
  const bc = getBrokerConfig(req);
96
105
  const fp = confPath(bc);
106
+ const backups = mosquittoConf.listBackups(fp).map((b) => path.basename(b));
107
+ if (bc.ssh && bc.ssh.host) {
108
+ _log?.debug(`broker: reading remote config from ${bc.ssh.host}:${fp}`);
109
+ const raw = await sshDeploy.readRemoteFile(bc.ssh, fp);
110
+ const parsed = mosquittoConf.parseText(raw);
111
+ const cs = crypto.createHash('sha256').update(raw).digest('hex');
112
+ return res.json({ ...parsed, checksum: cs, backups });
113
+ }
97
114
  const parsed = mosquittoConf.parse(fp);
98
115
  const cs = mosquittoConf.checksum(fp);
99
- const backups = mosquittoConf.listBackups(fp).map((b) => path.basename(b));
100
116
  res.json({ ...parsed, checksum: cs, backups });
101
117
  } catch (err) {
102
118
  handleError(res, err);
@@ -107,17 +123,25 @@ router.get('/config', (req, res) => {
107
123
  * PUT /she/broker/config
108
124
  * Write structured config (body: { listeners, managed, passthrough, checksum? }).
109
125
  * body.checksum is the client's last-known checksum for external-modify detection.
126
+ * In remote mode, the file is also deployed to the broker host via SCP.
110
127
  */
111
- router.put('/config', (req, res) => {
128
+ router.put('/config', async (req, res) => {
112
129
  try {
113
130
  const bc = getBrokerConfig(req);
114
131
  const fp = confPath(bc);
115
132
  const { listeners, managed, passthrough, checksum: clientChecksum } = req.body;
116
133
  const content = mosquittoConf.serialise({ listeners, managed, passthrough });
117
- const result = mosquittoConf.write(fp, content, clientChecksum ?? null);
134
+ const remote = bc.ssh && bc.ssh.host;
135
+ _log?.info(`broker: writing config to ${remote ? `remote ${bc.ssh.host}:${fp}` : `local ${fp}`}`);
136
+ // In remote mode skip local conflict check — remote is the source of truth
137
+ const result = mosquittoConf.write(fp, content, remote ? null : (clientChecksum ?? null));
118
138
  if (!result.ok) {
119
139
  return res.status(409).json({ error: 'external_modify', message: 'mosquitto.conf was modified externally since last read' });
120
140
  }
141
+ if (remote) {
142
+ _log?.debug(`broker: uploading config to ${bc.ssh.host}:${fp}`);
143
+ await sshDeploy.uploadContent(bc.ssh, content, fp);
144
+ }
121
145
  _lastWriteChecksum.set(fp, mosquittoConf.checksum(fp));
122
146
  res.json({ ok: true, backupPath: result.backupPath ? path.basename(result.backupPath) : null });
123
147
  } catch (err) {
@@ -128,8 +152,9 @@ router.put('/config', (req, res) => {
128
152
  /**
129
153
  * PUT /she/broker/config/raw
130
154
  * Write raw mosquitto.conf text directly (used by the Advanced editor).
155
+ * In remote mode, the file is also deployed to the broker host via SCP.
131
156
  */
132
- router.put('/config/raw', (req, res) => {
157
+ router.put('/config/raw', async (req, res) => {
133
158
  try {
134
159
  const bc = getBrokerConfig(req);
135
160
  const fp = confPath(bc);
@@ -137,10 +162,17 @@ router.put('/config/raw', (req, res) => {
137
162
  if (typeof content !== 'string') {
138
163
  return res.status(400).json({ error: 'content must be a string' });
139
164
  }
140
- const result = mosquittoConf.write(fp, content, clientChecksum ?? null);
165
+ const remote = bc.ssh && bc.ssh.host;
166
+ _log?.info(`broker: writing raw config to ${remote ? `remote ${bc.ssh.host}:${fp}` : `local ${fp}`}`);
167
+ // In remote mode skip local conflict check — remote is the source of truth
168
+ const result = mosquittoConf.write(fp, content, remote ? null : (clientChecksum ?? null));
141
169
  if (!result.ok) {
142
170
  return res.status(409).json({ error: 'external_modify', message: 'mosquitto.conf was modified externally since last read' });
143
171
  }
172
+ if (remote) {
173
+ _log?.debug(`broker: uploading raw config to ${bc.ssh.host}:${fp}`);
174
+ await sshDeploy.uploadContent(bc.ssh, content, fp);
175
+ }
144
176
  _lastWriteChecksum.set(fp, mosquittoConf.checksum(fp));
145
177
  res.json({ ok: true, backupPath: result.backupPath ? path.basename(result.backupPath) : null });
146
178
  } catch (err) {
@@ -673,7 +705,7 @@ router.delete('/ca/trusted/:fingerprint', async (req, res) => {
673
705
  }
674
706
  });
675
707
 
676
- module.exports = { router, setLogger };
708
+ module.exports = { router, setLogger, setStore };
677
709
 
678
710
  // ── SSH routes ─────────────────────────────────────────────────────────────────
679
711
  // Note: these routes are mounted on the same router but defined after module.exports