smart-home-engine 1.10.2 → 1.12.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.
@@ -1,4 +1,4 @@
1
- import{m as O}from"./monaco-langs-Dt8a9eeo.js";import{t as I}from"./index-BpeQz77V.js";/*!-----------------------------------------------------------------------------
1
+ import{m as O}from"./monaco-langs-Dt8a9eeo.js";import{t as I}from"./index-CxzLp43k.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-BpeQz77V.js"></script>
175
+ <script type="module" crossorigin src="/assets/index-CxzLp43k.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-B7BKqQNL.css">
178
+ <link rel="stylesheet" crossorigin href="/assets/index-CLFCyis7.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.10.2",
3
+ "version": "1.12.0",
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": {
@@ -11,9 +11,11 @@
11
11
  "test:verbose": "cross-env NODE_OPTIONS=\"--no-warnings\" jest --verbose --forceExit",
12
12
  "lint": "eslint --cache --cache-location .cache/.eslintcache .",
13
13
  "lint:fix": "eslint --cache --cache-location .cache/.eslintcache --fix .",
14
+ "lint:ci": "eslint --no-cache .",
14
15
  "format": "prettier --cache --cache-location .cache/.prettiercache --write .",
15
16
  "format:check": "prettier --cache --cache-location .cache/.prettiercache --check .",
16
17
  "build:web": "cd web && npm install && npm run build",
18
+ "prepare": "git config core.hooksPath .githooks || true",
17
19
  "postinstall": "node -e \"console.log('\\n smart-home-engine installed.\\n To run as a systemd service: sudo she --install\\n')\""
18
20
  },
19
21
  "bin": {
package/src/lib/ca.js CHANGED
@@ -141,8 +141,16 @@ async function importCA(config, { certPem, keyPem, chainPem = null }) {
141
141
  await openssl(['x509', '-in', tmpCert, '-noout']);
142
142
  await openssl(['pkey', '-in', tmpKey, '-noout']);
143
143
  } catch (e) {
144
- try { fs.unlinkSync(tmpCert); } catch { /* ignore */ }
145
- try { fs.unlinkSync(tmpKey); } catch { /* ignore */ }
144
+ try {
145
+ fs.unlinkSync(tmpCert);
146
+ } catch {
147
+ /* ignore */
148
+ }
149
+ try {
150
+ fs.unlinkSync(tmpKey);
151
+ } catch {
152
+ /* ignore */
153
+ }
146
154
  throw new Error(`Invalid certificate or key: ${e.message}`);
147
155
  }
148
156
 
@@ -153,7 +161,11 @@ async function importCA(config, { certPem, keyPem, chainPem = null }) {
153
161
 
154
162
  fs.renameSync(tmpCert, crtPath);
155
163
  fs.renameSync(tmpKey, keyPath);
156
- try { fs.chmodSync(keyPath, 0o600); } catch { /* ignore */ }
164
+ try {
165
+ fs.chmodSync(keyPath, 0o600);
166
+ } catch {
167
+ /* ignore */
168
+ }
157
169
 
158
170
  if (!fs.existsSync(srlPath)) {
159
171
  fs.writeFileSync(srlPath, '01\n', 'utf8');
@@ -165,7 +177,11 @@ async function importCA(config, { certPem, keyPem, chainPem = null }) {
165
177
  try {
166
178
  await openssl(['x509', '-in', tmpChain, '-noout']);
167
179
  } catch (e) {
168
- try { fs.unlinkSync(tmpChain); } catch { /* ignore */ }
180
+ try {
181
+ fs.unlinkSync(tmpChain);
182
+ } catch {
183
+ /* ignore */
184
+ }
169
185
  throw new Error(`Invalid chain certificate: ${e.message}`);
170
186
  }
171
187
  fs.renameSync(tmpChain, chainPath);
@@ -195,7 +211,7 @@ async function extractFromP12(p12Buffer, passphrase) {
195
211
  const passArg = `pass:${passphrase}`;
196
212
  const tryExtract = async (extraFlags = []) => {
197
213
  const { stdout: certOut } = await openssl(['pkcs12', '-in', tmpP12, '-clcerts', '-nokeys', '-passin', passArg, '-nodes', ...extraFlags]);
198
- const { stdout: keyOut } = await openssl(['pkcs12', '-in', tmpP12, '-nocerts', '-nodes', '-passin', passArg, ...extraFlags]);
214
+ const { stdout: keyOut } = await openssl(['pkcs12', '-in', tmpP12, '-nocerts', '-nodes', '-passin', passArg, ...extraFlags]);
199
215
  return { certOut, keyOut };
200
216
  };
201
217
  let certOut, keyOut;
@@ -205,12 +221,16 @@ async function extractFromP12(p12Buffer, passphrase) {
205
221
  ({ certOut, keyOut } = await tryExtract(['-legacy']));
206
222
  }
207
223
  const certMatch = certOut.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/);
208
- const keyMatch = keyOut.match(/-----BEGIN [\w ]+ KEY-----[\s\S]+?-----END [\w ]+ KEY-----/);
224
+ const keyMatch = keyOut.match(/-----BEGIN [\w ]+ KEY-----[\s\S]+?-----END [\w ]+ KEY-----/);
209
225
  if (!certMatch) throw new Error('No certificate found in P12 file');
210
- if (!keyMatch) throw new Error('No private key found in P12 file');
226
+ if (!keyMatch) throw new Error('No private key found in P12 file');
211
227
  return { certPem: certMatch[0] + '\n', keyPem: keyMatch[0] + '\n' };
212
228
  } finally {
213
- try { fs.unlinkSync(tmpP12); } catch { /* ignore */ }
229
+ try {
230
+ fs.unlinkSync(tmpP12);
231
+ } catch {
232
+ /* ignore */
233
+ }
214
234
  }
215
235
  }
216
236
 
@@ -311,7 +331,11 @@ async function generateServerCSR(config, { cn, san = [] } = {}) {
311
331
 
312
332
  // Generate key
313
333
  await openssl(['genpkey', '-algorithm', 'ed25519', '-out', keyPath]);
314
- try { fs.chmodSync(keyPath, 0o600); } catch { /* ignore */ }
334
+ try {
335
+ fs.chmodSync(keyPath, 0o600);
336
+ } catch {
337
+ /* ignore */
338
+ }
315
339
 
316
340
  // Build subject and optional SAN extension for the CSR
317
341
  const sanEntries = [cn, ...san].filter(Boolean).map((s, i) => {
@@ -353,7 +377,11 @@ async function importServerCert(config, { certPem, keyPem = null } = {}) {
353
377
  try {
354
378
  await openssl(['x509', '-in', tmpCrt, '-noout']);
355
379
  } catch (e) {
356
- try { fs.unlinkSync(tmpCrt); } catch { /* ignore */ }
380
+ try {
381
+ fs.unlinkSync(tmpCrt);
382
+ } catch {
383
+ /* ignore */
384
+ }
357
385
  throw new Error(`Invalid certificate: ${e.message}`);
358
386
  }
359
387
 
@@ -363,14 +391,30 @@ async function importServerCert(config, { certPem, keyPem = null } = {}) {
363
391
  try {
364
392
  await openssl(['pkey', '-in', tmpKey, '-noout']);
365
393
  } catch (e) {
366
- try { fs.unlinkSync(tmpCrt); } catch { /* ignore */ }
367
- try { fs.unlinkSync(tmpKey); } catch { /* ignore */ }
394
+ try {
395
+ fs.unlinkSync(tmpCrt);
396
+ } catch {
397
+ /* ignore */
398
+ }
399
+ try {
400
+ fs.unlinkSync(tmpKey);
401
+ } catch {
402
+ /* ignore */
403
+ }
368
404
  throw new Error(`Invalid private key: ${e.message}`);
369
405
  }
370
406
  fs.renameSync(tmpKey, keyPath);
371
- try { fs.chmodSync(keyPath, 0o600); } catch { /* ignore */ }
407
+ try {
408
+ fs.chmodSync(keyPath, 0o600);
409
+ } catch {
410
+ /* ignore */
411
+ }
372
412
  } else if (!fs.existsSync(keyPath)) {
373
- try { fs.unlinkSync(tmpCrt); } catch { /* ignore */ }
413
+ try {
414
+ fs.unlinkSync(tmpCrt);
415
+ } catch {
416
+ /* ignore */
417
+ }
374
418
  throw new Error('No private key provided and no existing server.key on disk');
375
419
  }
376
420
 
@@ -438,7 +482,12 @@ async function issueClientCert(config, { cn, days = 365 } = {}) {
438
482
  p12CertfileArg = combinedChainTmp;
439
483
  }
440
484
  await openssl(['pkcs12', '-export', '-in', crtPath, '-inkey', keyPath, '-certfile', p12CertfileArg, '-out', p12Path, '-passout', `pass:${passphrase}`, '-legacy']);
441
- if (combinedChainTmp) try { fs.unlinkSync(combinedChainTmp); } catch { /* ignore */ }
485
+ if (combinedChainTmp)
486
+ try {
487
+ fs.unlinkSync(combinedChainTmp);
488
+ } catch {
489
+ /* ignore */
490
+ }
442
491
 
443
492
  const fingerprint = await certFingerprint(crtPath);
444
493
  const expires = await certExpiry(crtPath);
package/src/lib/dynsec.js CHANGED
@@ -145,13 +145,15 @@ function init(config, log) {
145
145
  _log.info('dynsec: ready — subscribed as', dynsecCfg.adminUsername);
146
146
  _drain(); // flush any requests queued before connection
147
147
  // Probe whether the plugin is actually loaded and responding
148
- _request('getDefaultACLAccess').then(() => {
149
- _dynsecReady = true;
150
- _log.info('dynsec: plugin confirmed active');
151
- }).catch((err) => {
152
- _dynsecReady = false;
153
- _log.warn('dynsec: plugin probe failed — is the dynamic-security plugin loaded in mosquitto.conf?', err.message);
154
- });
148
+ _request('getDefaultACLAccess')
149
+ .then(() => {
150
+ _dynsecReady = true;
151
+ _log.info('dynsec: plugin confirmed active');
152
+ })
153
+ .catch((err) => {
154
+ _dynsecReady = false;
155
+ _log.warn('dynsec: plugin probe failed — is the dynamic-security plugin loaded in mosquitto.conf?', err.message);
156
+ });
155
157
  }
156
158
  });
157
159
  // The admin role has subscribePattern + publishClientReceive for $SYS/#.
@@ -179,7 +181,7 @@ function init(config, log) {
179
181
  const val = payload.toString();
180
182
  const now = Date.now();
181
183
  const prev = _sysData[topic];
182
- _sysData[topic] = { val, ts: now, lc: prev && prev.val !== val ? now : (prev ? prev.lc : now) };
184
+ _sysData[topic] = { val, ts: now, lc: prev && prev.val !== val ? now : prev ? prev.lc : now };
183
185
  return;
184
186
  }
185
187
  if (topic !== RESPONSE_TOPIC) return;
@@ -30,17 +30,32 @@ const execFileAsync = promisify(execFile);
30
30
  // when read from an old conf it is normalised to 'plugin_opt_config_file' on
31
31
  // the same line that recognises it (see parseText below).
32
32
  const MANAGED_SINGLE_KEYS = new Set([
33
- 'per_listener_settings', 'allow_anonymous',
34
- 'persistence', 'persistence_location',
35
- 'log_dest', 'log_type',
36
- 'plugin', 'plugin_opt_config_file', 'plugin_opt_dynsec_config_file',
33
+ 'per_listener_settings',
34
+ 'allow_anonymous',
35
+ 'persistence',
36
+ 'persistence_location',
37
+ 'log_dest',
38
+ 'log_type',
39
+ 'plugin',
40
+ 'plugin_opt_config_file',
41
+ 'plugin_opt_dynsec_config_file',
37
42
  // Connection limits
38
- 'max_connections', 'max_inflight_messages', 'max_queued_messages',
39
- 'max_packet_size', 'message_size_limit', 'max_keepalive',
43
+ 'max_connections',
44
+ 'max_inflight_messages',
45
+ 'max_queued_messages',
46
+ 'max_packet_size',
47
+ 'message_size_limit',
48
+ 'max_keepalive',
40
49
  // Sessions
41
- 'persistent_client_expiration', 'retain_available',
50
+ 'persistent_client_expiration',
51
+ 'retain_available',
42
52
  // Performance / misc
43
- 'set_tcp_nodelay', 'connection_messages',
53
+ 'set_tcp_nodelay',
54
+ 'connection_messages',
55
+ // $SYS reporting
56
+ 'sys_interval',
57
+ // Include dirs
58
+ 'include_dir',
44
59
  ]);
45
60
 
46
61
  /**
@@ -205,14 +220,26 @@ function serialise(conf) {
205
220
  const { managed = {}, listeners = [], passthrough = [] } = conf;
206
221
 
207
222
  const keyOrder = [
208
- 'per_listener_settings', 'allow_anonymous',
209
- 'persistence', 'persistence_location',
210
- 'max_connections', 'max_inflight_messages', 'max_queued_messages',
211
- 'max_packet_size', 'message_size_limit', 'max_keepalive',
212
- 'persistent_client_expiration', 'retain_available',
213
- 'set_tcp_nodelay', 'connection_messages',
214
- 'log_dest', 'log_type',
215
- 'plugin', 'plugin_opt_config_file',
223
+ 'per_listener_settings',
224
+ 'allow_anonymous',
225
+ 'persistence',
226
+ 'persistence_location',
227
+ 'max_connections',
228
+ 'max_inflight_messages',
229
+ 'max_queued_messages',
230
+ 'max_packet_size',
231
+ 'message_size_limit',
232
+ 'max_keepalive',
233
+ 'persistent_client_expiration',
234
+ 'retain_available',
235
+ 'set_tcp_nodelay',
236
+ 'connection_messages',
237
+ 'sys_interval',
238
+ 'log_dest',
239
+ 'log_type',
240
+ 'plugin',
241
+ 'plugin_opt_config_file',
242
+ 'include_dir',
216
243
  ];
217
244
  for (const key of keyOrder) {
218
245
  if (managed[key] === undefined) continue;
@@ -220,7 +247,10 @@ function serialise(conf) {
220
247
  if (Array.isArray(val)) {
221
248
  const written = new Set();
222
249
  for (const v of val) {
223
- if (!written.has(v)) { written.add(v); lines.push(`${key} ${v}`); }
250
+ if (!written.has(v)) {
251
+ written.add(v);
252
+ lines.push(`${key} ${v}`);
253
+ }
224
254
  }
225
255
  } else {
226
256
  lines.push(`${key} ${val}`);
@@ -21,7 +21,7 @@ function parsePayload(payload) {
21
21
  if (Array.isArray(parsed)) return { val: parsed };
22
22
  if (!parsed || typeof parsed.val === 'undefined') return { val: parsed };
23
23
  return parsed;
24
- } catch (_) {
24
+ } catch {
25
25
  return { val: str };
26
26
  }
27
27
  }
@@ -94,10 +94,14 @@ router.get('/status', (req, res) => {
94
94
  // received by the dynsec client (e.g. when dynsec is not configured).
95
95
  if (_store) {
96
96
  const sysPrefixes = [
97
- '$SYS/broker/version', '$SYS/broker/uptime',
98
- '$SYS/broker/clients/', '$SYS/broker/messages/',
99
- '$SYS/broker/subscriptions/', '$SYS/broker/retained messages/',
100
- '$SYS/broker/bytes/', '$SYS/broker/heap/',
97
+ '$SYS/broker/version',
98
+ '$SYS/broker/uptime',
99
+ '$SYS/broker/clients/',
100
+ '$SYS/broker/messages/',
101
+ '$SYS/broker/subscriptions/',
102
+ '$SYS/broker/retained messages/',
103
+ '$SYS/broker/bytes/',
104
+ '$SYS/broker/heap/',
101
105
  ];
102
106
  for (const [topic, entry] of _store.mqttEntries()) {
103
107
  if (!sys[topic] && sysPrefixes.some((p) => topic.startsWith(p))) {
@@ -963,16 +967,12 @@ router.get('/acl-check', async (req, res) => {
963
967
 
964
968
  const mqttWildcard = require('../lib/mqtt-wildcards');
965
969
 
966
- const [roles, groups, defaultAcls] = await Promise.all([
967
- dynsec.listRoles(true),
968
- dynsec.listGroups(true),
969
- dynsec.getDefaultACLAccess(),
970
- ]);
970
+ const [roles, groups, defaultAcls] = await Promise.all([dynsec.listRoles(true), dynsec.listGroups(true), dynsec.getDefaultACLAccess()]);
971
971
 
972
972
  // Build index: rolename → groups that carry it (with their member lists)
973
973
  const groupsByRole = new Map();
974
974
  for (const g of groups) {
975
- for (const r of (g.roles ?? [])) {
975
+ for (const r of g.roles ?? []) {
976
976
  if (!groupsByRole.has(r.rolename)) groupsByRole.set(r.rolename, []);
977
977
  groupsByRole.get(r.rolename).push({
978
978
  groupname: g.groupname,
@@ -981,9 +981,9 @@ router.get('/acl-check', async (req, res) => {
981
981
  }
982
982
  }
983
983
 
984
- const SEND_TYPES = new Set(['publishClientSend']);
984
+ const SEND_TYPES = new Set(['publishClientSend']);
985
985
  const SUBSCRIBE_TYPES = new Set(['subscribePattern', 'subscribeLiteral']);
986
- const RECEIVE_TYPES = new Set(['publishClientReceive']);
986
+ const RECEIVE_TYPES = new Set(['publishClientReceive']);
987
987
 
988
988
  function aclMatches(acltype, aclTopic) {
989
989
  if (aclTopic.includes('%u') || aclTopic.includes('%c')) return 'dynamic';
@@ -996,7 +996,7 @@ router.get('/acl-check', async (req, res) => {
996
996
  function matchRoles(typeSet) {
997
997
  const result = [];
998
998
  for (const role of roles) {
999
- for (const acl of (role.acls ?? [])) {
999
+ for (const acl of role.acls ?? []) {
1000
1000
  if (!typeSet.has(acl.acltype)) continue;
1001
1001
  const m = aclMatches(acl.acltype, acl.topic);
1002
1002
  if (m !== 'no-match') {
@@ -1021,9 +1021,9 @@ router.get('/acl-check', async (req, res) => {
1021
1021
 
1022
1022
  res.json({
1023
1023
  topic,
1024
- send: { roles: matchRoles(SEND_TYPES), default: defaultAllow('publishClientSend') },
1024
+ send: { roles: matchRoles(SEND_TYPES), default: defaultAllow('publishClientSend') },
1025
1025
  subscribe: { roles: matchRoles(SUBSCRIBE_TYPES), default: defaultAllow('subscribePattern') },
1026
- receive: { roles: matchRoles(RECEIVE_TYPES), default: defaultAllow('publishClientReceive') },
1026
+ receive: { roles: matchRoles(RECEIVE_TYPES), default: defaultAllow('publishClientReceive') },
1027
1027
  });
1028
1028
  } catch (err) {
1029
1029
  handleError(res, err);
@@ -1068,9 +1068,7 @@ router.get('/ip-addresses', async (req, res) => {
1068
1068
  const result = await sshDeploy.runCommand(bc.ssh, 'ip a 2>/dev/null || ip addr 2>/dev/null');
1069
1069
  stdout = result.stdout;
1070
1070
  } else {
1071
- const result = await execFileAsync('ip', ['a'], { timeout: 5000 }).catch(() =>
1072
- execFileAsync('ip', ['addr'], { timeout: 5000 }),
1073
- );
1071
+ const result = await execFileAsync('ip', ['a'], { timeout: 5000 }).catch(() => execFileAsync('ip', ['addr'], { timeout: 5000 }));
1074
1072
  stdout = result.stdout;
1075
1073
  }
1076
1074
  // Extract IPv4 and IPv6 addresses; skip loopback
@@ -1121,8 +1119,8 @@ router.post('/ssh/keygen', async (req, res) => {
1121
1119
 
1122
1120
  /** POST /she/broker/ssh/test — Test SSH connection */
1123
1121
  router.post('/ssh/test', async (req, res) => {
1122
+ const bc = getBrokerConfig(req);
1124
1123
  try {
1125
- const bc = getBrokerConfig(req);
1126
1124
  if (!bc.ssh || !bc.ssh.host) return res.status(400).json({ error: 'broker.ssh.host not configured' });
1127
1125
  const user = (bc.ssh && bc.ssh.user) || require('os').userInfo().username;
1128
1126
  const key = sshDeploy.expandHome((bc.ssh && bc.ssh.identityFile) || DEFAULT_SSH_KEY);
@@ -1449,7 +1447,6 @@ router.post('/wizard/reinit', (req, res) => {
1449
1447
  router.get('/wizard/diagnose', async (req, res) => {
1450
1448
  try {
1451
1449
  const bc = getBrokerConfig(req);
1452
- const configPath = req.app.locals.configPath;
1453
1450
  const adminUsername = bc.dynsec && bc.dynsec.adminUsername;
1454
1451
  const configDir = bc.configDir || '/etc/mosquitto';
1455
1452
  const dynSecPath = `${configDir}/dynamic-security.json`;
@@ -1491,16 +1488,21 @@ router.get('/wizard/diagnose', async (req, res) => {
1491
1488
  const hasAdminRole = adminRoles.includes('admin');
1492
1489
 
1493
1490
  const adminRoleDef = roles.find((r) => r.rolename === 'admin');
1494
- const adminRoleAcls = adminRoleDef ? (adminRoleDef.acls || []) : [];
1495
- const hasControlSendAcl = adminRoleAcls.some(
1496
- (a) => a.acltype === 'publishClientSend' && (a.topic || '').includes('$CONTROL/dynamic-security')
1497
- );
1491
+ const adminRoleAcls = adminRoleDef ? adminRoleDef.acls || [] : [];
1492
+ const hasControlSendAcl = adminRoleAcls.some((a) => a.acltype === 'publishClientSend' && (a.topic || '').includes('$CONTROL/dynamic-security'));
1498
1493
 
1499
1494
  const issues = [];
1500
1495
  if (!adminUsername) issues.push('broker.dynsec.adminUsername not set in config.json');
1501
- if (adminUsername && !adminClientExists) issues.push(`User "${adminUsername}" not found in dynamic-security.json — mosquitto_ctrl init may have silently failed (file exists and is not writable by the SSH user). Delete ${dynSecPath} on the broker and re-run the wizard.`);
1502
- if (adminClientExists && !hasAdminRole) issues.push(`User "${adminUsername}" exists but does not have the "admin" role — ACL for $CONTROL/dynamic-security/# may be missing.`);
1503
- if (hasAdminRole && !hasControlSendAcl) issues.push(`The "admin" role exists but is missing a publishClientSend ACL for $CONTROL/dynamic-security/#. Re-run mosquitto_ctrl dynsec init to regenerate the file.`);
1496
+ if (adminUsername && !adminClientExists)
1497
+ issues.push(
1498
+ `User "${adminUsername}" not found in dynamic-security.json mosquitto_ctrl init may have silently failed (file exists and is not writable by the SSH user). Delete ${dynSecPath} on the broker and re-run the wizard.`,
1499
+ );
1500
+ if (adminClientExists && !hasAdminRole)
1501
+ issues.push(`User "${adminUsername}" exists but does not have the "admin" role — ACL for $CONTROL/dynamic-security/# may be missing.`);
1502
+ if (hasAdminRole && !hasControlSendAcl)
1503
+ issues.push(
1504
+ `The "admin" role exists but is missing a publishClientSend ACL for $CONTROL/dynamic-security/#. Re-run mosquitto_ctrl dynsec init to regenerate the file.`,
1505
+ );
1504
1506
 
1505
1507
  res.json({
1506
1508
  ok: issues.length === 0,
@@ -18,7 +18,7 @@
18
18
  const express = require('express');
19
19
  const { broadcast, broadcastBrokerLog } = require('./log-ws');
20
20
 
21
- const BROKER_LOG_TOPICS = new Set(['D', 'I', 'N', 'W', 'E'].map(l => `$SYS/broker/log/${l}`));
21
+ const BROKER_LOG_TOPICS = new Set(['D', 'I', 'N', 'W', 'E'].map((l) => `$SYS/broker/log/${l}`));
22
22
 
23
23
  const router = express.Router();
24
24
 
@@ -173,7 +173,7 @@ router.use((req, res) => {
173
173
 
174
174
  if (mode === 'files') {
175
175
  const lq = q.toLowerCase();
176
- const all = allFiles.map(f => f.path).filter(p => p.toLowerCase().includes(lq));
176
+ const all = allFiles.map((f) => f.path).filter((p) => p.toLowerCase().includes(lq));
177
177
  return res.json({ results: all.slice(0, 200), truncated: all.length > 200 });
178
178
  }
179
179
 
@@ -181,8 +181,11 @@ router.use((req, res) => {
181
181
  const MAX = 500;
182
182
  let pattern = null;
183
183
  if (isRegex) {
184
- try { pattern = new RegExp(q, caseSensitive ? '' : 'i'); }
185
- catch (e) { return res.status(400).json({ error: 'Invalid regex: ' + e.message }); }
184
+ try {
185
+ pattern = new RegExp(q, caseSensitive ? '' : 'i');
186
+ } catch (e) {
187
+ return res.status(400).json({ error: 'Invalid regex: ' + e.message });
188
+ }
186
189
  }
187
190
  const results = [];
188
191
  let total = 0;
@@ -192,12 +195,19 @@ router.use((req, res) => {
192
195
  const abs = safePath(root, file.path);
193
196
  if (!abs) continue;
194
197
  let buf;
195
- try { buf = fs.readFileSync(abs); } catch { continue; }
198
+ try {
199
+ buf = fs.readFileSync(abs);
200
+ } catch {
201
+ continue;
202
+ }
196
203
  if (buf.slice(0, 8192).indexOf(0) !== -1) continue; // binary
197
204
  const lines = buf.toString('utf8').split('\n');
198
205
  const fileMatches = [];
199
206
  for (let i = 0; i < lines.length; i++) {
200
- if (total >= MAX) { truncated = true; break outer; }
207
+ if (total >= MAX) {
208
+ truncated = true;
209
+ break outer;
210
+ }
201
211
  const line = lines[i];
202
212
  let col = -1;
203
213
  if (pattern) {