smart-home-engine 1.4.0 → 1.5.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.
- package/README.md +1 -1
- package/dist/web/assets/{index-AxQ546-B.js → index-COVhu4fS.js} +69 -69
- package/dist/web/assets/{index-Cp5N3a9H.css → index-CiYcuYTG.css} +1 -1
- package/dist/web/assets/{tsMode-DEX2KSLC.js → tsMode-CVHEzKL2.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/lib/ca.js +97 -1
- package/src/lib/dynsec.js +4 -1
- package/src/web/broker-api.js +146 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{m as O}from"./monaco-langs-BW2J83t5.js";import{t as I}from"./index-
|
|
1
|
+
import{m as O}from"./monaco-langs-BW2J83t5.js";import{t as I}from"./index-COVhu4fS.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-COVhu4fS.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-
|
|
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
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
|
-
|
|
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) =>
|
|
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) {
|
package/src/web/broker-api.js
CHANGED
|
@@ -577,6 +577,51 @@ router.post('/ca/import', async (req, res) => {
|
|
|
577
577
|
}
|
|
578
578
|
});
|
|
579
579
|
|
|
580
|
+
/**
|
|
581
|
+
* GET /she/broker/fs/complete?path=<prefix>
|
|
582
|
+
* Returns filesystem path completions for a partial absolute path.
|
|
583
|
+
* Works locally or via SSH when broker.ssh is configured.
|
|
584
|
+
*/
|
|
585
|
+
router.get('/fs/complete', async (req, res) => {
|
|
586
|
+
try {
|
|
587
|
+
const bc = getBrokerConfig(req);
|
|
588
|
+
const inputPath = String(req.query.path || '').trim();
|
|
589
|
+
if (!inputPath.startsWith('/')) return res.json({ suggestions: [] });
|
|
590
|
+
|
|
591
|
+
const endsWithSlash = inputPath.endsWith('/');
|
|
592
|
+
const posix = require('path').posix;
|
|
593
|
+
const dir = endsWithSlash ? inputPath : posix.dirname(inputPath);
|
|
594
|
+
const prefix = endsWithSlash ? '' : posix.basename(inputPath);
|
|
595
|
+
|
|
596
|
+
let entries = [];
|
|
597
|
+
if (bc.ssh && bc.ssh.host) {
|
|
598
|
+
try {
|
|
599
|
+
const { stdout } = await sshDeploy.runCommand(bc.ssh, `ls -1p -- "${dir}" 2>/dev/null`);
|
|
600
|
+
entries = stdout.split('\n').filter(Boolean);
|
|
601
|
+
} catch {
|
|
602
|
+
entries = [];
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
try {
|
|
606
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
607
|
+
entries = items.map((d) => d.name + (d.isDirectory() ? '/' : ''));
|
|
608
|
+
} catch {
|
|
609
|
+
entries = [];
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const base = dir.endsWith('/') ? dir : dir + '/';
|
|
614
|
+
const suggestions = entries
|
|
615
|
+
.filter((e) => e.startsWith(prefix))
|
|
616
|
+
.map((e) => base + e)
|
|
617
|
+
.slice(0, 25);
|
|
618
|
+
|
|
619
|
+
res.json({ suggestions });
|
|
620
|
+
} catch {
|
|
621
|
+
res.json({ suggestions: [] });
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
580
625
|
/** GET /she/broker/ca/server — Server cert info */
|
|
581
626
|
router.get('/ca/server', async (req, res) => {
|
|
582
627
|
try {
|
|
@@ -593,7 +638,7 @@ router.get('/ca/server', async (req, res) => {
|
|
|
593
638
|
}
|
|
594
639
|
});
|
|
595
640
|
|
|
596
|
-
/** POST /she/broker/ca/server/generate — Generate server cert */
|
|
641
|
+
/** POST /she/broker/ca/server/generate — Generate self-signed server cert (auto-creates internal CA) */
|
|
597
642
|
router.post('/ca/server/generate', async (req, res) => {
|
|
598
643
|
try {
|
|
599
644
|
const bc = getBrokerConfig(req);
|
|
@@ -605,6 +650,79 @@ router.post('/ca/server/generate', async (req, res) => {
|
|
|
605
650
|
}
|
|
606
651
|
});
|
|
607
652
|
|
|
653
|
+
/**
|
|
654
|
+
* POST /she/broker/ca/server/csr
|
|
655
|
+
* Generate a private key + CSR for the server cert.
|
|
656
|
+
* The CSR PEM is returned for external signing; the key is kept on disk.
|
|
657
|
+
* Body: { cn?, san?: string[], days?: number }
|
|
658
|
+
*/
|
|
659
|
+
router.post('/ca/server/csr', async (req, res) => {
|
|
660
|
+
try {
|
|
661
|
+
const bc = getBrokerConfig(req);
|
|
662
|
+
const { cn, san, days } = req.body;
|
|
663
|
+
const result = await ca.generateServerCSR(bc, { cn, san, days });
|
|
664
|
+
res.json({ ok: true, csrPem: result.csrPem });
|
|
665
|
+
} catch (err) {
|
|
666
|
+
handleError(res, err);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* POST /she/broker/ca/server/import
|
|
672
|
+
* Install an externally-signed server certificate (PEM or PKCS#12).
|
|
673
|
+
* Body (PEM): { cert: string, key?: string }
|
|
674
|
+
* key is optional when a key already exists on disk (e.g. from /csr).
|
|
675
|
+
* Body (P12): { p12base64: string, passphrase?: string }
|
|
676
|
+
*/
|
|
677
|
+
router.post('/ca/server/import', async (req, res) => {
|
|
678
|
+
try {
|
|
679
|
+
const bc = getBrokerConfig(req);
|
|
680
|
+
const { cert, key, p12base64, passphrase } = req.body;
|
|
681
|
+
let certPem, keyPem;
|
|
682
|
+
if (p12base64 !== undefined) {
|
|
683
|
+
const p12Buffer = Buffer.from(p12base64, 'base64');
|
|
684
|
+
({ certPem, keyPem } = await ca.extractFromP12(p12Buffer, passphrase ?? ''));
|
|
685
|
+
} else {
|
|
686
|
+
if (!cert) return res.status(400).json({ error: 'cert is required' });
|
|
687
|
+
certPem = cert;
|
|
688
|
+
keyPem = key || null;
|
|
689
|
+
}
|
|
690
|
+
const result = await ca.importServerCert(bc, { certPem, keyPem });
|
|
691
|
+
res.json({ ok: true, server: result });
|
|
692
|
+
} catch (err) {
|
|
693
|
+
handleError(res, err);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* POST /she/broker/ca/server/pathlink
|
|
699
|
+
* Install a server cert+key from existing file paths on the broker host.
|
|
700
|
+
* Reads via SSH if broker.ssh is configured, otherwise reads local files.
|
|
701
|
+
* Body: { certPath: string, keyPath: string }
|
|
702
|
+
*/
|
|
703
|
+
router.post('/ca/server/pathlink', async (req, res) => {
|
|
704
|
+
try {
|
|
705
|
+
const bc = getBrokerConfig(req);
|
|
706
|
+
const { certPath, keyPath } = req.body;
|
|
707
|
+
if (!certPath || !keyPath) return res.status(400).json({ error: 'certPath and keyPath are required' });
|
|
708
|
+
if (!certPath.startsWith('/') || !keyPath.startsWith('/')) return res.status(400).json({ error: 'Absolute paths required' });
|
|
709
|
+
|
|
710
|
+
let certPem, keyPem;
|
|
711
|
+
if (bc.ssh && bc.ssh.host) {
|
|
712
|
+
certPem = await sshDeploy.readRemoteFile(bc.ssh, certPath);
|
|
713
|
+
keyPem = await sshDeploy.readRemoteFile(bc.ssh, keyPath);
|
|
714
|
+
} else {
|
|
715
|
+
certPem = fs.readFileSync(certPath, 'utf8');
|
|
716
|
+
keyPem = fs.readFileSync(keyPath, 'utf8');
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const result = await ca.importServerCert(bc, { certPem, keyPem });
|
|
720
|
+
res.json({ ok: true, server: result });
|
|
721
|
+
} catch (err) {
|
|
722
|
+
handleError(res, err);
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
|
|
608
726
|
/** GET /she/broker/ca/certs — List issued client certs (from sheDB) */
|
|
609
727
|
router.get('/ca/certs', async (req, res) => {
|
|
610
728
|
try {
|
|
@@ -782,6 +900,33 @@ router.delete('/ca/trusted/:fingerprint', async (req, res) => {
|
|
|
782
900
|
}
|
|
783
901
|
});
|
|
784
902
|
|
|
903
|
+
/**
|
|
904
|
+
* POST /she/broker/ca/trusted/addpath
|
|
905
|
+
* Add a trusted CA cert from an existing file path on the broker host.
|
|
906
|
+
* Reads via SSH if broker.ssh is configured, otherwise reads local file.
|
|
907
|
+
* Body: { path: string }
|
|
908
|
+
*/
|
|
909
|
+
router.post('/ca/trusted/addpath', async (req, res) => {
|
|
910
|
+
try {
|
|
911
|
+
const bc = getBrokerConfig(req);
|
|
912
|
+
const { path: filePath } = req.body;
|
|
913
|
+
if (!filePath || typeof filePath !== 'string') return res.status(400).json({ error: 'path required' });
|
|
914
|
+
if (!filePath.startsWith('/')) return res.status(400).json({ error: 'Absolute path required' });
|
|
915
|
+
|
|
916
|
+
let pemContent;
|
|
917
|
+
if (bc.ssh && bc.ssh.host) {
|
|
918
|
+
pemContent = await sshDeploy.readRemoteFile(bc.ssh, filePath);
|
|
919
|
+
} else {
|
|
920
|
+
pemContent = fs.readFileSync(filePath, 'utf8');
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const result = await ca.addTrustedCert(bc, pemContent);
|
|
924
|
+
res.json({ ok: true, ...result });
|
|
925
|
+
} catch (err) {
|
|
926
|
+
handleError(res, err);
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
|
|
785
930
|
module.exports = { router, setLogger, setStore };
|
|
786
931
|
|
|
787
932
|
// ── SSH routes ─────────────────────────────────────────────────────────────────
|