smart-home-engine 1.4.0 → 1.5.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-BW2J83t5.js";import{t as I}from"./index-AxQ546-B.js";/*!-----------------------------------------------------------------------------
1
+ import{m as O}from"./monaco-langs-BW2J83t5.js";import{t as I}from"./index-76-mgKLK.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-AxQ546-B.js"></script>
175
+ <script type="module" crossorigin src="/assets/index-76-mgKLK.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
- <link rel="stylesheet" crossorigin href="/assets/index-Cp5N3a9H.css">
178
+ <link rel="stylesheet" crossorigin href="/assets/index-CiYcuYTG.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.4.0",
3
+ "version": "1.5.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": {
package/src/lib/ca.js CHANGED
@@ -228,7 +228,10 @@ async function generateServerCert(config, { cn, san = [], days = 365 } = {}) {
228
228
  const caCrtPath = path.join(dir, 'ca.crt');
229
229
  const srlPath = path.join(dir, 'ca.srl');
230
230
 
231
- if (!fs.existsSync(caCrtPath)) throw new Error('No CA found generate CA first');
231
+ // Auto-generate an internal CA if none exists (used for self-signed server certs)
232
+ if (!fs.existsSync(caCrtPath)) {
233
+ await generateCA(config, { cn: 'she-internal-ca', days: 3650 });
234
+ }
232
235
 
233
236
  const serverDir = path.join(dir, 'server');
234
237
  fs.mkdirSync(serverDir, { recursive: true });
@@ -288,6 +291,97 @@ async function generateServerCert(config, { cn, san = [], days = 365 } = {}) {
288
291
 
289
292
  // ── Client certificate issuance ────────────────────────────────────────────────
290
293
 
294
+ /**
295
+ * Generate a server private key and CSR without signing it.
296
+ * The key is stored on disk as server/server.key; the CSR is returned as PEM.
297
+ * The user takes the CSR to an external CA to get it signed, then calls
298
+ * importServerCert() to install the resulting certificate.
299
+ *
300
+ * @param {object} config
301
+ * @param {{ cn?: string, san?: string[], days?: number }} options (days ignored — for future use)
302
+ * @returns {Promise<{ csrPem: string }>}
303
+ */
304
+ async function generateServerCSR(config, { cn, san = [] } = {}) {
305
+ const dir = caDir(config);
306
+ const serverDir = path.join(dir, 'server');
307
+ fs.mkdirSync(serverDir, { recursive: true });
308
+
309
+ const keyPath = path.join(serverDir, 'server.key');
310
+ const csrPath = path.join(serverDir, 'server.csr');
311
+
312
+ // Generate key
313
+ await openssl(['genpkey', '-algorithm', 'ed25519', '-out', keyPath]);
314
+ try { fs.chmodSync(keyPath, 0o600); } catch { /* ignore */ }
315
+
316
+ // Build subject and optional SAN extension for the CSR
317
+ const sanEntries = [cn, ...san].filter(Boolean).map((s, i) => {
318
+ return /^\d+\.\d+\.\d+\.\d+$/.test(s) ? `IP.${i + 1}:${s}` : `DNS.${i + 1}:${s}`;
319
+ });
320
+ const subjArgs = ['-subj', `/CN=${cn || 'mosquitto'}`];
321
+ let reqArgs = ['req', '-new', '-key', keyPath, '-out', csrPath, ...subjArgs];
322
+ if (sanEntries.length) {
323
+ const extPath = path.join(serverDir, 'server-csr.conf');
324
+ fs.writeFileSync(extPath, `[req]\ndistinguished_name=dn\nreq_extensions=SAN\n[dn]\n[SAN]\nsubjectAltName=${sanEntries.join(',')}\n`, 'utf8');
325
+ reqArgs = ['req', '-new', '-key', keyPath, '-out', csrPath, ...subjArgs, '-config', extPath];
326
+ }
327
+ await openssl(reqArgs);
328
+
329
+ const csrPem = fs.readFileSync(csrPath, 'utf8');
330
+ return { csrPem };
331
+ }
332
+
333
+ /**
334
+ * Install an externally-signed certificate as the server certificate.
335
+ * If keyPem is provided it is written as server.key; otherwise the existing
336
+ * server.key on disk (e.g. from a prior generateServerCSR call) is used.
337
+ *
338
+ * @param {object} config
339
+ * @param {{ certPem: string, keyPem?: string|null }} options
340
+ * @returns {Promise<{ fingerprint: string, expires: string, cn: string }>}
341
+ */
342
+ async function importServerCert(config, { certPem, keyPem = null } = {}) {
343
+ const dir = caDir(config);
344
+ const serverDir = path.join(dir, 'server');
345
+ fs.mkdirSync(serverDir, { recursive: true });
346
+
347
+ const crtPath = path.join(serverDir, 'server.crt');
348
+ const keyPath = path.join(serverDir, 'server.key');
349
+
350
+ // Validate cert
351
+ const tmpCrt = crtPath + '.tmp';
352
+ fs.writeFileSync(tmpCrt, certPem.trim() + '\n', 'utf8');
353
+ try {
354
+ await openssl(['x509', '-in', tmpCrt, '-noout']);
355
+ } catch (e) {
356
+ try { fs.unlinkSync(tmpCrt); } catch { /* ignore */ }
357
+ throw new Error(`Invalid certificate: ${e.message}`);
358
+ }
359
+
360
+ if (keyPem) {
361
+ const tmpKey = keyPath + '.tmp';
362
+ fs.writeFileSync(tmpKey, keyPem.trim() + '\n', 'utf8');
363
+ try {
364
+ await openssl(['pkey', '-in', tmpKey, '-noout']);
365
+ } catch (e) {
366
+ try { fs.unlinkSync(tmpCrt); } catch { /* ignore */ }
367
+ try { fs.unlinkSync(tmpKey); } catch { /* ignore */ }
368
+ throw new Error(`Invalid private key: ${e.message}`);
369
+ }
370
+ fs.renameSync(tmpKey, keyPath);
371
+ try { fs.chmodSync(keyPath, 0o600); } catch { /* ignore */ }
372
+ } else if (!fs.existsSync(keyPath)) {
373
+ try { fs.unlinkSync(tmpCrt); } catch { /* ignore */ }
374
+ throw new Error('No private key provided and no existing server.key on disk');
375
+ }
376
+
377
+ fs.renameSync(tmpCrt, crtPath);
378
+
379
+ const fingerprint = await certFingerprint(crtPath);
380
+ const expires = await certExpiry(crtPath);
381
+ const cn = await certCN(crtPath);
382
+ return { fingerprint, expires, cn };
383
+ }
384
+
291
385
  /**
292
386
  * Issue a client certificate signed by the local CA.
293
387
  * Returns paths to .p12, .crt, .key and the p12 passphrase.
@@ -570,6 +664,8 @@ module.exports = {
570
664
  importCA,
571
665
  extractFromP12,
572
666
  generateServerCert,
667
+ generateServerCSR,
668
+ importServerCert,
573
669
  issueClientCert,
574
670
  generateCRL,
575
671
  listTrustedCerts,
package/src/lib/dynsec.js CHANGED
@@ -346,7 +346,10 @@ function setDefaultACLAccess(acls) {
346
346
  // ── Anonymous group ────────────────────────────────────────────────────────────
347
347
 
348
348
  function getAnonymousGroup() {
349
- return _request('getAnonymousGroup').then((r) => r.data?.group ?? r.group ?? null);
349
+ return _request('getAnonymousGroup').then((r) => {
350
+ const g = r.data?.group ?? r.group ?? null;
351
+ return g?.groupname ?? g ?? null;
352
+ });
350
353
  }
351
354
 
352
355
  function setAnonymousGroup(groupname) {
@@ -593,7 +593,7 @@ router.get('/ca/server', async (req, res) => {
593
593
  }
594
594
  });
595
595
 
596
- /** POST /she/broker/ca/server/generate — Generate server cert */
596
+ /** POST /she/broker/ca/server/generate — Generate self-signed server cert (auto-creates internal CA) */
597
597
  router.post('/ca/server/generate', async (req, res) => {
598
598
  try {
599
599
  const bc = getBrokerConfig(req);
@@ -605,6 +605,50 @@ router.post('/ca/server/generate', async (req, res) => {
605
605
  }
606
606
  });
607
607
 
608
+ /**
609
+ * POST /she/broker/ca/server/csr
610
+ * Generate a private key + CSR for the server cert.
611
+ * The CSR PEM is returned for external signing; the key is kept on disk.
612
+ * Body: { cn?, san?: string[], days?: number }
613
+ */
614
+ router.post('/ca/server/csr', async (req, res) => {
615
+ try {
616
+ const bc = getBrokerConfig(req);
617
+ const { cn, san, days } = req.body;
618
+ const result = await ca.generateServerCSR(bc, { cn, san, days });
619
+ res.json({ ok: true, csrPem: result.csrPem });
620
+ } catch (err) {
621
+ handleError(res, err);
622
+ }
623
+ });
624
+
625
+ /**
626
+ * POST /she/broker/ca/server/import
627
+ * Install an externally-signed server certificate (PEM or PKCS#12).
628
+ * Body (PEM): { cert: string, key?: string }
629
+ * key is optional when a key already exists on disk (e.g. from /csr).
630
+ * Body (P12): { p12base64: string, passphrase?: string }
631
+ */
632
+ router.post('/ca/server/import', async (req, res) => {
633
+ try {
634
+ const bc = getBrokerConfig(req);
635
+ const { cert, key, p12base64, passphrase } = req.body;
636
+ let certPem, keyPem;
637
+ if (p12base64 !== undefined) {
638
+ const p12Buffer = Buffer.from(p12base64, 'base64');
639
+ ({ certPem, keyPem } = await ca.extractFromP12(p12Buffer, passphrase ?? ''));
640
+ } else {
641
+ if (!cert) return res.status(400).json({ error: 'cert is required' });
642
+ certPem = cert;
643
+ keyPem = key || null;
644
+ }
645
+ const result = await ca.importServerCert(bc, { certPem, keyPem });
646
+ res.json({ ok: true, server: result });
647
+ } catch (err) {
648
+ handleError(res, err);
649
+ }
650
+ });
651
+
608
652
  /** GET /she/broker/ca/certs — List issued client certs (from sheDB) */
609
653
  router.get('/ca/certs', async (req, res) => {
610
654
  try {