smart-home-engine 1.2.0 → 1.4.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-AxQ546-B.js +230 -0
- package/dist/web/assets/{index-CiR8cZ_Y.css → index-Cp5N3a9H.css} +1 -1
- package/dist/web/assets/{tsMode-Dj2ZPBcc.js → tsMode-DEX2KSLC.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/lib/ca.js +112 -3
- package/src/lib/dynsec.js +13 -0
- package/src/web/broker-api.js +62 -0
- package/dist/web/assets/index-CY1IzFyN.js +0 -230
|
@@ -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-AxQ546-B.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-AxQ546-B.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-Cp5N3a9H.css">
|
|
179
179
|
</head>
|
|
180
180
|
<body>
|
|
181
181
|
<div id="app"></div>
|
package/package.json
CHANGED
package/src/lib/ca.js
CHANGED
|
@@ -104,18 +104,116 @@ async function generateCA(config, { cn = 'she-broker-ca', days = 365 } = {}) {
|
|
|
104
104
|
async function getCA(config) {
|
|
105
105
|
const dir = caDir(config);
|
|
106
106
|
const crtPath = path.join(dir, 'ca.crt');
|
|
107
|
+
const chainPath = path.join(dir, 'ca-chain.crt');
|
|
107
108
|
if (!fs.existsSync(crtPath)) return null;
|
|
108
109
|
try {
|
|
109
110
|
const fingerprint = await certFingerprint(crtPath);
|
|
110
111
|
const expires = await certExpiry(crtPath);
|
|
111
112
|
const cn = await certCN(crtPath);
|
|
112
113
|
const crt = fs.readFileSync(crtPath, 'utf8');
|
|
113
|
-
|
|
114
|
+
const hasChain = fs.existsSync(chainPath);
|
|
115
|
+
const chainCn = hasChain ? await certCN(chainPath).catch(() => null) : null;
|
|
116
|
+
return { crt, fingerprint, expires, cn, hasChain, chainCn };
|
|
114
117
|
} catch {
|
|
115
118
|
return null;
|
|
116
119
|
}
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Import an existing CA keypair (PEM format) into the CA directory.
|
|
124
|
+
* Overwrites any existing ca.crt / ca.key.
|
|
125
|
+
* Optionally accepts a PEM chain (intermediate/root certs above this CA) stored
|
|
126
|
+
* as ca-chain.crt; used to build complete certificate chains in issued P12 bundles.
|
|
127
|
+
*
|
|
128
|
+
* @param {object} config
|
|
129
|
+
* @param {{ certPem: string, keyPem: string, chainPem?: string|null }} options
|
|
130
|
+
*/
|
|
131
|
+
async function importCA(config, { certPem, keyPem, chainPem = null }) {
|
|
132
|
+
const dir = caDir(config);
|
|
133
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
134
|
+
|
|
135
|
+
const tmpCert = path.join(dir, '_import_cert.tmp');
|
|
136
|
+
const tmpKey = path.join(dir, '_import_key.tmp');
|
|
137
|
+
fs.writeFileSync(tmpCert, certPem.trim() + '\n', 'utf8');
|
|
138
|
+
fs.writeFileSync(tmpKey, keyPem.trim() + '\n', 'utf8');
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await openssl(['x509', '-in', tmpCert, '-noout']);
|
|
142
|
+
await openssl(['pkey', '-in', tmpKey, '-noout']);
|
|
143
|
+
} catch (e) {
|
|
144
|
+
try { fs.unlinkSync(tmpCert); } catch { /* ignore */ }
|
|
145
|
+
try { fs.unlinkSync(tmpKey); } catch { /* ignore */ }
|
|
146
|
+
throw new Error(`Invalid certificate or key: ${e.message}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const keyPath = path.join(dir, 'ca.key');
|
|
150
|
+
const crtPath = path.join(dir, 'ca.crt');
|
|
151
|
+
const srlPath = path.join(dir, 'ca.srl');
|
|
152
|
+
const chainPath = path.join(dir, 'ca-chain.crt');
|
|
153
|
+
|
|
154
|
+
fs.renameSync(tmpCert, crtPath);
|
|
155
|
+
fs.renameSync(tmpKey, keyPath);
|
|
156
|
+
try { fs.chmodSync(keyPath, 0o600); } catch { /* ignore */ }
|
|
157
|
+
|
|
158
|
+
if (!fs.existsSync(srlPath)) {
|
|
159
|
+
fs.writeFileSync(srlPath, '01\n', 'utf8');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (chainPem && chainPem.trim()) {
|
|
163
|
+
const tmpChain = path.join(dir, '_import_chain.tmp');
|
|
164
|
+
fs.writeFileSync(tmpChain, chainPem.trim() + '\n', 'utf8');
|
|
165
|
+
try {
|
|
166
|
+
await openssl(['x509', '-in', tmpChain, '-noout']);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
try { fs.unlinkSync(tmpChain); } catch { /* ignore */ }
|
|
169
|
+
throw new Error(`Invalid chain certificate: ${e.message}`);
|
|
170
|
+
}
|
|
171
|
+
fs.renameSync(tmpChain, chainPath);
|
|
172
|
+
} else if (fs.existsSync(chainPath)) {
|
|
173
|
+
fs.unlinkSync(chainPath);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const fingerprint = await certFingerprint(crtPath);
|
|
177
|
+
const expires = await certExpiry(crtPath);
|
|
178
|
+
const cn = await certCN(crtPath);
|
|
179
|
+
const crt = fs.readFileSync(crtPath, 'utf8');
|
|
180
|
+
return { crt, fingerprint, expires, cn };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Extract certificate and private key PEM strings from a PKCS#12 file.
|
|
185
|
+
* Tries OpenSSL 3 defaults first, falls back to -legacy for older P12 files.
|
|
186
|
+
*
|
|
187
|
+
* @param {Buffer} p12Buffer raw .p12/.pfx file bytes
|
|
188
|
+
* @param {string} passphrase decryption passphrase (may be empty string)
|
|
189
|
+
* @returns {Promise<{ certPem: string, keyPem: string }>}
|
|
190
|
+
*/
|
|
191
|
+
async function extractFromP12(p12Buffer, passphrase) {
|
|
192
|
+
const tmpP12 = path.join(os.tmpdir(), `she_p12_${Date.now()}.tmp`);
|
|
193
|
+
fs.writeFileSync(tmpP12, p12Buffer);
|
|
194
|
+
try {
|
|
195
|
+
const passArg = `pass:${passphrase}`;
|
|
196
|
+
const tryExtract = async (extraFlags = []) => {
|
|
197
|
+
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]);
|
|
199
|
+
return { certOut, keyOut };
|
|
200
|
+
};
|
|
201
|
+
let certOut, keyOut;
|
|
202
|
+
try {
|
|
203
|
+
({ certOut, keyOut } = await tryExtract());
|
|
204
|
+
} catch {
|
|
205
|
+
({ certOut, keyOut } = await tryExtract(['-legacy']));
|
|
206
|
+
}
|
|
207
|
+
const certMatch = certOut.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/);
|
|
208
|
+
const keyMatch = keyOut.match(/-----BEGIN [\w ]+ KEY-----[\s\S]+?-----END [\w ]+ KEY-----/);
|
|
209
|
+
if (!certMatch) throw new Error('No certificate found in P12 file');
|
|
210
|
+
if (!keyMatch) throw new Error('No private key found in P12 file');
|
|
211
|
+
return { certPem: certMatch[0] + '\n', keyPem: keyMatch[0] + '\n' };
|
|
212
|
+
} finally {
|
|
213
|
+
try { fs.unlinkSync(tmpP12); } catch { /* ignore */ }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
119
217
|
// ── Server certificate ─────────────────────────────────────────────────────────
|
|
120
218
|
|
|
121
219
|
/**
|
|
@@ -235,9 +333,18 @@ async function issueClientCert(config, { cn, days = 365 } = {}) {
|
|
|
235
333
|
// Read serial from the signed cert
|
|
236
334
|
const serial = await certSerial(crtPath);
|
|
237
335
|
|
|
238
|
-
// Bundle to .p12
|
|
336
|
+
// Bundle to .p12 — include signing chain if ca-chain.crt exists
|
|
239
337
|
const passphrase = crypto.randomBytes(10).toString('hex');
|
|
240
|
-
|
|
338
|
+
const chainPath = path.join(dir, 'ca-chain.crt');
|
|
339
|
+
let p12CertfileArg = caCrtPath;
|
|
340
|
+
let combinedChainTmp = null;
|
|
341
|
+
if (fs.existsSync(chainPath)) {
|
|
342
|
+
combinedChainTmp = path.join(dir, '.combined-chain-tmp.crt');
|
|
343
|
+
fs.writeFileSync(combinedChainTmp, fs.readFileSync(caCrtPath, 'utf8') + fs.readFileSync(chainPath, 'utf8'));
|
|
344
|
+
p12CertfileArg = combinedChainTmp;
|
|
345
|
+
}
|
|
346
|
+
await openssl(['pkcs12', '-export', '-in', crtPath, '-inkey', keyPath, '-certfile', p12CertfileArg, '-out', p12Path, '-passout', `pass:${passphrase}`, '-legacy']);
|
|
347
|
+
if (combinedChainTmp) try { fs.unlinkSync(combinedChainTmp); } catch { /* ignore */ }
|
|
241
348
|
|
|
242
349
|
const fingerprint = await certFingerprint(crtPath);
|
|
243
350
|
const expires = await certExpiry(crtPath);
|
|
@@ -460,6 +567,8 @@ module.exports = {
|
|
|
460
567
|
caCertsDir,
|
|
461
568
|
generateCA,
|
|
462
569
|
getCA,
|
|
570
|
+
importCA,
|
|
571
|
+
extractFromP12,
|
|
463
572
|
generateServerCert,
|
|
464
573
|
issueClientCert,
|
|
465
574
|
generateCRL,
|
package/src/lib/dynsec.js
CHANGED
|
@@ -343,6 +343,16 @@ function setDefaultACLAccess(acls) {
|
|
|
343
343
|
return _request('setDefaultACLAccess', { acls });
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
+
// ── Anonymous group ────────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
function getAnonymousGroup() {
|
|
349
|
+
return _request('getAnonymousGroup').then((r) => r.data?.group ?? r.group ?? null);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function setAnonymousGroup(groupname) {
|
|
353
|
+
return _request('setAnonymousGroup', { groupname });
|
|
354
|
+
}
|
|
355
|
+
|
|
346
356
|
module.exports = {
|
|
347
357
|
init,
|
|
348
358
|
getStatus,
|
|
@@ -376,4 +386,7 @@ module.exports = {
|
|
|
376
386
|
// Default ACLs
|
|
377
387
|
getDefaultACLAccess,
|
|
378
388
|
setDefaultACLAccess,
|
|
389
|
+
// Anonymous group
|
|
390
|
+
getAnonymousGroup,
|
|
391
|
+
setAnonymousGroup,
|
|
379
392
|
};
|
package/src/web/broker-api.js
CHANGED
|
@@ -490,6 +490,42 @@ router.get('/acl-defaults', async (req, res) => {
|
|
|
490
490
|
}
|
|
491
491
|
});
|
|
492
492
|
|
|
493
|
+
/** PUT /she/broker/acl-defaults */
|
|
494
|
+
router.put('/acl-defaults', async (req, res) => {
|
|
495
|
+
try {
|
|
496
|
+
const { acls } = req.body;
|
|
497
|
+
if (!Array.isArray(acls)) return res.status(400).json({ error: 'acls array required' });
|
|
498
|
+
await dynsec.setDefaultACLAccess(acls);
|
|
499
|
+
res.json({ ok: true });
|
|
500
|
+
} catch (err) {
|
|
501
|
+
handleError(res, err);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// ── dynsec: Anonymous group ───────────────────────────────────────────────────
|
|
506
|
+
|
|
507
|
+
/** GET /she/broker/anonymous-group */
|
|
508
|
+
router.get('/anonymous-group', async (req, res) => {
|
|
509
|
+
try {
|
|
510
|
+
const group = await dynsec.getAnonymousGroup();
|
|
511
|
+
res.json({ group });
|
|
512
|
+
} catch (err) {
|
|
513
|
+
handleError(res, err);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
/** PUT /she/broker/anonymous-group */
|
|
518
|
+
router.put('/anonymous-group', async (req, res) => {
|
|
519
|
+
try {
|
|
520
|
+
const { groupname } = req.body;
|
|
521
|
+
if (typeof groupname !== 'string' && groupname !== null) return res.status(400).json({ error: 'groupname string or null required' });
|
|
522
|
+
await dynsec.setAnonymousGroup(groupname);
|
|
523
|
+
res.json({ ok: true });
|
|
524
|
+
} catch (err) {
|
|
525
|
+
handleError(res, err);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
493
529
|
// ── CA routes ─────────────────────────────────────────────────────────────────
|
|
494
530
|
|
|
495
531
|
/** GET /she/broker/ca — CA cert info */
|
|
@@ -515,6 +551,32 @@ router.post('/ca/generate', async (req, res) => {
|
|
|
515
551
|
}
|
|
516
552
|
});
|
|
517
553
|
|
|
554
|
+
/**
|
|
555
|
+
* POST /she/broker/ca/import
|
|
556
|
+
* Import an existing CA from PEM text or a PKCS#12 file.
|
|
557
|
+
* Body (PEM mode): { cert: string, key: string, chain?: string }
|
|
558
|
+
* Body (P12 mode): { p12base64: string, passphrase: string, chain?: string }
|
|
559
|
+
*/
|
|
560
|
+
router.post('/ca/import', async (req, res) => {
|
|
561
|
+
try {
|
|
562
|
+
const bc = getBrokerConfig(req);
|
|
563
|
+
const { cert, key, chain, p12base64, passphrase } = req.body;
|
|
564
|
+
let certPem, keyPem;
|
|
565
|
+
if (p12base64 !== undefined) {
|
|
566
|
+
const p12Buffer = Buffer.from(p12base64, 'base64');
|
|
567
|
+
({ certPem, keyPem } = await ca.extractFromP12(p12Buffer, passphrase ?? ''));
|
|
568
|
+
} else {
|
|
569
|
+
if (!cert || !key) return res.status(400).json({ error: 'cert and key are required' });
|
|
570
|
+
certPem = cert;
|
|
571
|
+
keyPem = key;
|
|
572
|
+
}
|
|
573
|
+
const result = await ca.importCA(bc, { certPem, keyPem, chainPem: chain || null });
|
|
574
|
+
res.json({ ok: true, ca: result });
|
|
575
|
+
} catch (err) {
|
|
576
|
+
handleError(res, err);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
|
|
518
580
|
/** GET /she/broker/ca/server — Server cert info */
|
|
519
581
|
router.get('/ca/server', async (req, res) => {
|
|
520
582
|
try {
|