smart-home-engine 1.15.1 → 1.17.0
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.
- package/dist/web/assets/index-BWK6XOv0.js +234 -0
- package/dist/web/assets/{index-LTpStzqg.css → index-C-4azUYf.css} +1 -1
- package/dist/web/assets/{tsMode-acXHtETC.js → tsMode-WXSFC0ZU.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/sandbox/shedb-sandbox.js +56 -1
- package/src/web/broker-api.js +144 -0
- package/src/web/shedb.js +29 -2
- package/dist/web/assets/index-Vw4M_nL7.js +0 -232
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{m as O}from"./monaco-langs-Dt8a9eeo.js";import{t as I}from"./index-
|
|
1
|
+
import{m as O}from"./monaco-langs-Dt8a9eeo.js";import{t as I}from"./index-BWK6XOv0.js";/*!-----------------------------------------------------------------------------
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
* Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
|
|
4
4
|
* Released under the MIT license
|
package/dist/web/index.html
CHANGED
|
@@ -172,10 +172,10 @@
|
|
|
172
172
|
}
|
|
173
173
|
})();
|
|
174
174
|
</script>
|
|
175
|
-
<script type="module" crossorigin src="/assets/index-
|
|
175
|
+
<script type="module" crossorigin src="/assets/index-BWK6XOv0.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-
|
|
178
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C-4azUYf.css">
|
|
179
179
|
</head>
|
|
180
180
|
<body>
|
|
181
181
|
<div id="app"></div>
|
package/package.json
CHANGED
|
@@ -13,9 +13,12 @@
|
|
|
13
13
|
* she.db.prop(id, method, prop, val)→ void (method: 'set'|'create'|'del')
|
|
14
14
|
* she.db.sub(pattern, callback) → void (callback(id, doc))
|
|
15
15
|
* she.db.query(filter, mapFn, reduceFn) → Array (ad-hoc synchronous query)
|
|
16
|
+
* she.db.getView(id) → Array|undefined (current view result)
|
|
17
|
+
* she.db.subView(pattern, callback) → void (callback(id, result))
|
|
18
|
+
* she.db.setView(id, definition) → void (create/update named view)
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
|
-
const { getCore, addListener, removeListenersByScript } = require('../web/shedb');
|
|
21
|
+
const { getCore, addListener, removeListenersByScript, addViewListener, removeViewListenersByScript } = require('../web/shedb');
|
|
19
22
|
|
|
20
23
|
module.exports = function (she, { scriptDomain, scriptName, scriptFile }) {
|
|
21
24
|
const core = getCore();
|
|
@@ -80,6 +83,57 @@ module.exports = function (she, { scriptDomain, scriptName, scriptFile }) {
|
|
|
80
83
|
if (!core) return [];
|
|
81
84
|
return core.adhocQuery(filter, mapFn, reduceFn);
|
|
82
85
|
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Return the current computed result array for a named view, or undefined if
|
|
89
|
+
* the view does not exist, has not yet completed, or produced an error.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} id - view name
|
|
92
|
+
* @returns {Array|undefined}
|
|
93
|
+
*/
|
|
94
|
+
getView(id) {
|
|
95
|
+
if (!core) return undefined;
|
|
96
|
+
const view = core.views[id];
|
|
97
|
+
if (!view || view.error) return undefined;
|
|
98
|
+
return view.result;
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Subscribe to view result changes matching an MQTT wildcard pattern.
|
|
103
|
+
* callback(id, result) fires whenever a matching view's result changes;
|
|
104
|
+
* result is undefined if the view errored.
|
|
105
|
+
* Subscriptions are automatically removed when the script is hot-reloaded.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} pattern - MQTT wildcard, e.g. 'stats/#'
|
|
108
|
+
* @param {Function} callback - called as callback(id, result)
|
|
109
|
+
*/
|
|
110
|
+
subView(pattern, callback) {
|
|
111
|
+
if (!core) return;
|
|
112
|
+
const wrapped = scriptDomain.bind(callback);
|
|
113
|
+
addViewListener(pattern, wrapped, trackingKey);
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create or update a named persistent view.
|
|
118
|
+
* Definition: { map, filter?, reduce?, publish?, retain?, description? }
|
|
119
|
+
* `publish` maps to `mqttpub` internally.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} id
|
|
122
|
+
* @param {{ map: string, filter?: string, reduce?: string, publish?: boolean, retain?: boolean, description?: string }} definition
|
|
123
|
+
*/
|
|
124
|
+
setView(id, definition) {
|
|
125
|
+
if (!core) return;
|
|
126
|
+
if (!definition || typeof definition !== 'object') return;
|
|
127
|
+
const { map, filter, reduce, publish, retain, description } = definition;
|
|
128
|
+
if (typeof map !== 'string' || !map.trim()) return;
|
|
129
|
+
const payload = { map };
|
|
130
|
+
if (filter) payload.filter = filter;
|
|
131
|
+
if (reduce) payload.reduce = reduce;
|
|
132
|
+
if (publish) payload.mqttpub = true;
|
|
133
|
+
if (retain) payload.retain = true;
|
|
134
|
+
if (description) payload.description = String(description).slice(0, 500);
|
|
135
|
+
core.query(id, payload);
|
|
136
|
+
},
|
|
83
137
|
};
|
|
84
138
|
};
|
|
85
139
|
|
|
@@ -89,4 +143,5 @@ module.exports = function (she, { scriptDomain, scriptName, scriptFile }) {
|
|
|
89
143
|
*/
|
|
90
144
|
module.exports.cleanup = function (scriptFile) {
|
|
91
145
|
removeListenersByScript(scriptFile);
|
|
146
|
+
removeViewListenersByScript(scriptFile);
|
|
92
147
|
};
|
package/src/web/broker-api.js
CHANGED
|
@@ -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 ──────────────────────────────────────────────
|
package/src/web/shedb.js
CHANGED
|
@@ -23,6 +23,9 @@ let _broadcast = () => {};
|
|
|
23
23
|
/** Registry for she.db.sub() sandbox subscriptions */
|
|
24
24
|
const _listeners = []; // { pattern: string, callback: Function, _script: string }
|
|
25
25
|
|
|
26
|
+
/** Registry for she.db.subView() sandbox subscriptions */
|
|
27
|
+
const _viewListeners = []; // { pattern: string, callback: Function, _script: string }
|
|
28
|
+
|
|
26
29
|
/**
|
|
27
30
|
* Initialise sheDB.
|
|
28
31
|
*
|
|
@@ -80,6 +83,18 @@ function init({ dbPath, dbPublish, dbRetain, dbPrefix, mqttName, mqtt, log, broa
|
|
|
80
83
|
if (query && query.mqttpub && _mqtt && view && !view.error) {
|
|
81
84
|
_mqtt.publish(_dbPrefix + 'view/' + id, JSON.stringify(view.result ?? []), { retain: query.retain === true });
|
|
82
85
|
}
|
|
86
|
+
|
|
87
|
+
// Fire sandbox she.db.subView() listeners
|
|
88
|
+
const result = view && !view.error ? view.result : undefined;
|
|
89
|
+
for (const sub of _viewListeners) {
|
|
90
|
+
if (mqttWildcard(id, sub.pattern) !== null) {
|
|
91
|
+
try {
|
|
92
|
+
sub.callback(id, result);
|
|
93
|
+
} catch {
|
|
94
|
+
/* errors are caught by the script domain wrapper */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
83
98
|
});
|
|
84
99
|
|
|
85
100
|
// When a view definition is saved, immediately publish the existing cached
|
|
@@ -165,16 +180,28 @@ function addListener(pattern, callback, scriptName) {
|
|
|
165
180
|
_listeners.push({ pattern, callback, _script: scriptName });
|
|
166
181
|
}
|
|
167
182
|
|
|
168
|
-
/** Remove all listeners registered by a script (called on hot-reload / unload). */
|
|
183
|
+
/** Remove all she.db.sub() listeners registered by a script (called on hot-reload / unload). */
|
|
169
184
|
function removeListenersByScript(scriptName) {
|
|
170
185
|
for (let i = _listeners.length - 1; i >= 0; i--) {
|
|
171
186
|
if (_listeners[i]._script === scriptName) _listeners.splice(i, 1);
|
|
172
187
|
}
|
|
173
188
|
}
|
|
174
189
|
|
|
190
|
+
/** Register a she.db.subView() listener. */
|
|
191
|
+
function addViewListener(pattern, callback, scriptName) {
|
|
192
|
+
_viewListeners.push({ pattern, callback, _script: scriptName });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Remove all she.db.subView() listeners registered by a script (called on hot-reload / unload). */
|
|
196
|
+
function removeViewListenersByScript(scriptName) {
|
|
197
|
+
for (let i = _viewListeners.length - 1; i >= 0; i--) {
|
|
198
|
+
if (_viewListeners[i]._script === scriptName) _viewListeners.splice(i, 1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
175
202
|
/** Return the live core instance (used by shedb-api.js). */
|
|
176
203
|
function getCore() {
|
|
177
204
|
return _core;
|
|
178
205
|
}
|
|
179
206
|
|
|
180
|
-
module.exports = { init, handleMqttMessage, addListener, removeListenersByScript, getCore };
|
|
207
|
+
module.exports = { init, handleMqttMessage, addListener, removeListenersByScript, addViewListener, removeViewListenersByScript, getCore };
|