@camstack/system 1.2.438 → 1.2.440

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.
Files changed (120) hide show
  1. package/dist/addon-runner.js +743 -79
  2. package/dist/addon-runner.mjs +743 -79
  3. package/dist/addon-utils.js +1 -1
  4. package/dist/addon-utils.mjs +1 -1
  5. package/dist/{agent-https-BZIwS4xF.js → agent-https-DlpTMDqA.js} +252 -22
  6. package/dist/{agent-https-BPfHrI4c.mjs → agent-https-U0d4uvn2.mjs} +211 -23
  7. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  8. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  9. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  10. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  11. package/dist/builtins/alerts/alerts.addon.js +1 -1
  12. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  13. package/dist/builtins/autotrack/index.js +1 -1
  14. package/dist/builtins/autotrack/index.mjs +1 -1
  15. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  16. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  17. package/dist/builtins/calendars/calendars.addon.js +1 -1
  18. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  19. package/dist/builtins/camera-grid/index.js +1 -1
  20. package/dist/builtins/camera-grid/index.mjs +1 -1
  21. package/dist/builtins/composer/composer.addon.js +2 -2
  22. package/dist/builtins/composer/composer.addon.mjs +2 -2
  23. package/dist/builtins/console-logging/index.js +1 -1
  24. package/dist/builtins/console-logging/index.mjs +1 -1
  25. package/dist/builtins/core-blocks/block-settings-store.d.ts +1 -1
  26. package/dist/builtins/core-blocks/core-blocks.addon.js +77 -11
  27. package/dist/builtins/core-blocks/core-blocks.addon.mjs +77 -12
  28. package/dist/builtins/core-blocks/index.mjs +1 -1
  29. package/dist/builtins/device-manager/device-manager.addon.js +197 -60
  30. package/dist/builtins/device-manager/device-manager.addon.mjs +196 -61
  31. package/dist/builtins/device-manager/device-removal-hooks.d.ts +0 -11
  32. package/dist/builtins/device-manager/device-tombstone-store.d.ts +12 -2
  33. package/dist/builtins/hub-forwarder/index.js +1 -1
  34. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  35. package/dist/builtins/keychain/index.js +29 -11
  36. package/dist/builtins/keychain/index.mjs +29 -11
  37. package/dist/builtins/keychain/keychain-service.d.ts +8 -0
  38. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  39. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  40. package/dist/builtins/local-auth/local-auth.addon.js +2 -2
  41. package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
  42. package/dist/builtins/local-network/local-network.addon.js +7 -2
  43. package/dist/builtins/local-network/local-network.addon.mjs +7 -2
  44. package/dist/builtins/loki-logging/index.js +1 -1
  45. package/dist/builtins/loki-logging/index.mjs +1 -1
  46. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  47. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  48. package/dist/builtins/platform-probe/index.js +1 -1
  49. package/dist/builtins/platform-probe/index.mjs +1 -1
  50. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  51. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  52. package/dist/builtins/snapshot/index.js +1 -1
  53. package/dist/builtins/snapshot/index.mjs +1 -1
  54. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  55. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  56. package/dist/builtins/sqlite-storage/index.js +1 -1
  57. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  58. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +38 -7
  59. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +69 -3
  60. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +69 -3
  61. package/dist/builtins/sqlite-storage/storage-admin-provider.d.ts +31 -0
  62. package/dist/builtins/storage-orchestrator/hub-settings-forward.d.ts +3 -3
  63. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +4 -4
  64. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +4 -4
  65. package/dist/builtins/system-config/system-config.addon.js +4 -4
  66. package/dist/builtins/system-config/system-config.addon.mjs +285 -285
  67. package/dist/builtins/winston-logging/index.js +1 -1
  68. package/dist/builtins/winston-logging/index.mjs +1 -1
  69. package/dist/{composition-sources-DpHGCjdL.mjs → composition-sources-CS0D7imL.mjs} +1 -1
  70. package/dist/{composition-sources-DI3Z83vG.js → composition-sources-cBG8fBTu.js} +1 -1
  71. package/dist/{constants-DQK705Sb.mjs → constants-CIzPjw7L.mjs} +1 -1
  72. package/dist/{constants-wkcn5Ij3.js → constants-CRI3QzT4.js} +1 -1
  73. package/dist/{device-ca-DvIZjXIl.mjs → device-ca-DLKXYseS.mjs} +2 -2
  74. package/dist/{device-ca-BSvkXqyI.js → device-ca-DNHjts4_.js} +2 -2
  75. package/dist/{dist-Dc8GKC5H.mjs → dist-9fcsrFU3.mjs} +271 -32
  76. package/dist/{dist-GgG-7amw.js → dist-BKhlcYz6.js} +288 -31
  77. package/dist/file-host-worker.js +1 -1
  78. package/dist/file-host-worker.mjs +1 -1
  79. package/dist/{hub-mdns-ciao-CBLvq2oj.js → hub-mdns-ciao-3Zc9G9MS.js} +377 -69
  80. package/dist/{hub-mdns-ciao-DMlQTdPU.mjs → hub-mdns-ciao-Dy2YjRqC.mjs} +344 -72
  81. package/dist/index.js +372 -339
  82. package/dist/index.mjs +361 -341
  83. package/dist/kernel/addon-settings-store.d.ts +43 -0
  84. package/dist/kernel/config-manager.d.ts +0 -19
  85. package/dist/kernel/device-settings-store.d.ts +74 -0
  86. package/dist/kernel/legacy-blob-guard.d.ts +58 -0
  87. package/dist/kernel/moleculer/runner-addon-settings.d.ts +34 -0
  88. package/dist/kernel/moleculer/runner-device-settings.d.ts +39 -0
  89. package/dist/kernel/prefix-walk.d.ts +39 -0
  90. package/dist/kernel/sections-store.d.ts +20 -0
  91. package/dist/kernel/settings-door-view.d.ts +2 -2
  92. package/dist/kernel/settings-view-types.d.ts +5 -30
  93. package/dist/kernel/store-settings-view.d.ts +0 -3
  94. package/dist/{lib-NNxHSOIE.js → lib-8oZrELLm.js} +1 -1
  95. package/dist/{lib-BeGzVQcd.mjs → lib-DK38ompL.mjs} +1 -1
  96. package/dist/prefix-range-D_KueXbQ.js +27 -0
  97. package/dist/prefix-range-Dl0znowN.mjs +22 -0
  98. package/dist/prefix-walk-DAOBSyH1.mjs +88 -0
  99. package/dist/prefix-walk-Dv6MLZlM.js +99 -0
  100. package/dist/{restore-archive-CTRz8v9o.js → restore-archive-49aZwkp8.js} +1 -1
  101. package/dist/{restore-archive-DPg9duVY.mjs → restore-archive-DgvDecXM.mjs} +82 -82
  102. package/dist/{retired-settings-keys-Is0zO8gX.mjs → retired-settings-keys-Bhwe4ymb.mjs} +2 -2
  103. package/dist/{retired-settings-keys-Dr_4lfKu.js → retired-settings-keys-D_VwZiDt.js} +10 -4
  104. package/dist/tls/agent-https.d.ts +2 -2
  105. package/dist/tls/boot-tls.d.ts +6 -0
  106. package/dist/tls/cert-evaluation.d.ts +1 -1
  107. package/dist/tls/cert-issue.d.ts +5 -1
  108. package/dist/tls/cert-manager.d.ts +18 -0
  109. package/dist/tls/cert-name-constraints.d.ts +53 -0
  110. package/dist/tls/index.d.ts +5 -4
  111. package/dist/tls/tls-actions.d.ts +27 -7
  112. package/dist/tls/tls-status.d.ts +22 -0
  113. package/dist/tls/uploaded-cert.d.ts +36 -4
  114. package/dist/{wal-maintenance-Bx3lcVA7.mjs → wal-maintenance-BZGmF-3U.mjs} +0 -0
  115. package/dist/{wal-maintenance-mjB2i-VR.js → wal-maintenance-C041zn6V.js} +0 -0
  116. package/dist/{x509.es-r7Ht1thA.js → x509.es-C4cOo48M.js} +36 -0
  117. package/dist/{x509.es-CDvj67p1.mjs → x509.es-DK__ETIe.mjs} +1 -1
  118. package/package.json +7 -2
  119. package/dist/{custom-action-registry-Bf_YnagS.js → custom-action-registry-BKmoaqG0.js} +1 -1
  120. package/dist/{custom-action-registry-BQnbN0DP.mjs → custom-action-registry-CyUuhATc.mjs} +1 -1
@@ -1,10 +1,13 @@
1
1
  import { o as __toESM } from "./chunk-CNf5ZN-e.mjs";
2
- import { D as reasonRequiresNewCa, E as evaluateExistingCert, _ as generateCa, b as collectCertIdentity, v as issueLeaf, y as loadCa } from "./agent-https-BPfHrI4c.mjs";
2
+ import { xi as userError } from "./dist-9fcsrFU3.mjs";
3
+ import { f as X509Certificate$1 } from "./x509.es-DK__ETIe.mjs";
4
+ import { A as isConstrainedCa, E as reasonRequiresNewCa, M as permitsIdentity, P as readNameConstraints, T as evaluateExistingCert, _ as issueLeaf, g as generateCa, v as loadCa, y as collectCertIdentity } from "./agent-https-U0d4uvn2.mjs";
3
5
  import { X509Certificate, createPrivateKey } from "node:crypto";
4
6
  import { writeOwnerOnlyFileSync } from "@camstack/types/node";
5
7
  import { mkdir, writeFile } from "node:fs/promises";
6
- import { join } from "node:path";
7
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
8
+ import { dirname, join } from "node:path";
9
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
10
+ import { rootCertificates } from "node:tls";
8
11
  import { createServer } from "node:http";
9
12
  //#region src/http/lan-http-bind.ts
10
13
  /**
@@ -188,24 +191,26 @@ async function ensureTlsCert(dataDir, options) {
188
191
  };
189
192
  const commonName = options?.commonName ?? DEFAULT_COMMON_NAME;
190
193
  const validDays = Math.min(options?.validDays ?? 397, 397);
191
- const identity = collectCertIdentity({
194
+ const rawIdentity = collectCertIdentity({
192
195
  commonName,
193
196
  extraSans: options?.extraSans ?? [],
194
197
  ...options?.hostFacts === void 0 ? {} : { hostFacts: options.hostFacts }
195
198
  });
196
- const reason = decideRegeneration(paths, identity);
197
- if (reason === null) return describe(paths, false, null, false, null, null);
199
+ const onDisk = constrainToDiskCa(paths, rawIdentity);
200
+ const reason = decideRegeneration(paths, onDisk.identity);
201
+ if (reason === null) return describe(paths, false, null, false, null, null, onDisk.dropped);
198
202
  const previousFingerprint = readFingerprint(paths.certPath);
199
203
  const caRotated = reasonRequiresNewCa(reason);
200
204
  mkdirSync(tlsDir, { recursive: true });
205
+ let dropped = [];
201
206
  try {
202
- await regenerate(paths, identity, commonName, validDays, caRotated);
207
+ dropped = await regenerate(paths, rawIdentity, commonName, validDays, caRotated, reason);
203
208
  } catch (err) {
204
209
  const message = err instanceof Error ? err.message : String(err);
205
210
  if (!canKeepServing(paths)) throw err;
206
- return describe(paths, false, null, false, null, message);
211
+ return describe(paths, false, null, false, null, message, onDisk.dropped);
207
212
  }
208
- return describe(paths, true, reason, caRotated, previousFingerprint, null);
213
+ return describe(paths, true, reason, caRotated, previousFingerprint, null, dropped);
209
214
  }
210
215
  /**
211
216
  * Operator "Regenerate": issue a new leaf under the existing local CA.
@@ -229,8 +234,39 @@ async function reissueTlsLeaf(dataDir, options) {
229
234
  });
230
235
  const previousFingerprint = readFingerprint(paths.certPath);
231
236
  mkdirSync(tlsDir, { recursive: true });
232
- await regenerate(paths, identity, commonName, validDays, false);
233
- return describe(paths, true, "san-coverage-gap", false, previousFingerprint, null);
237
+ return describe(paths, true, "san-coverage-gap", false, previousFingerprint, null, await regenerate(paths, identity, commonName, validDays, false, null));
238
+ }
239
+ /**
240
+ * Uploaded mode keeps a generated CA beside the upload (agents' leaves are still signed
241
+ * by it). When that CA is unconstrained, replace it alone (D873): the served pair is the
242
+ * operator's and is never touched. Returns whether the CA was replaced.
243
+ */
244
+ async function replaceUnconstrainedLocalCa(dataDir, options) {
245
+ const tlsDir = join(dataDir, "tls");
246
+ const paths = {
247
+ certPath: join(tlsDir, "camstack.crt"),
248
+ keyPath: join(tlsDir, "camstack.key"),
249
+ caCertPath: join(tlsDir, "camstack-ca.crt"),
250
+ caKeyPath: join(tlsDir, "camstack-ca.key")
251
+ };
252
+ if (!existsSync(paths.caCertPath) || !existsSync(paths.caKeyPath)) return false;
253
+ try {
254
+ if (isConstrainedCa(new X509Certificate$1(readFileSync(paths.caCertPath, "utf-8")))) return false;
255
+ } catch {
256
+ return false;
257
+ }
258
+ const identity = collectCertIdentity({
259
+ commonName: options?.commonName ?? DEFAULT_COMMON_NAME,
260
+ extraSans: options?.extraSans ?? [],
261
+ ...options?.hostFacts === void 0 ? {} : { hostFacts: options.hostFacts }
262
+ });
263
+ const ca = await generateCa(/* @__PURE__ */ new Date(), identity.dnsNames);
264
+ archivePreviousCa(paths, ca);
265
+ await writeFile(`${paths.caCertPath}.new`, ca.certPem, "utf-8");
266
+ writeOwnerOnlyFileSync(`${paths.caKeyPath}.new`, ca.keyPem);
267
+ renameSync(`${paths.caCertPath}.new`, paths.caCertPath);
268
+ renameSync(`${paths.caKeyPath}.new`, paths.caKeyPath);
269
+ return true;
234
270
  }
235
271
  /** True when the material already on disk can still terminate TLS. */
236
272
  function canKeepServing(paths) {
@@ -267,12 +303,18 @@ function decideRegeneration(paths, identity) {
267
303
  * moves leaves a key/certificate mismatch, which the evaluator reports as
268
304
  * `key-mismatch` and heals on the next boot.
269
305
  */
270
- async function regenerate(paths, identity, commonName, validDays, newCa) {
306
+ async function regenerate(paths, rawIdentity, commonName, validDays, newCa, reason) {
271
307
  const now = /* @__PURE__ */ new Date();
272
- const ca = newCa ? await generateCa(now) : await loadCa(readFileSync(paths.caCertPath, "utf-8"), readFileSync(paths.caKeyPath, "utf-8"));
308
+ const ca = newCa ? await generateCa(now, rawIdentity.dnsNames) : await loadCa(readFileSync(paths.caCertPath, "utf-8"), readFileSync(paths.caKeyPath, "utf-8"));
309
+ const constraints = readNameConstraints(ca.cert);
310
+ const { identity, dropped } = constraints === null ? {
311
+ identity: rawIdentity,
312
+ dropped: []
313
+ } : permitsIdentity(constraints, rawIdentity);
273
314
  const leaf = await issueLeaf(ca, identity, commonName, validDays, now);
274
315
  const chainPem = `${leaf.certPem.trimEnd()}\n${ca.certPem.trimEnd()}\n`;
275
316
  if (newCa) {
317
+ if (reason === "ca-unconstrained") archivePreviousCa(paths, ca);
276
318
  await writeFile(`${paths.caCertPath}.new`, ca.certPem, "utf-8");
277
319
  writeOwnerOnlyFileSync(`${paths.caKeyPath}.new`, ca.keyPem);
278
320
  renameSync(`${paths.caCertPath}.new`, paths.caCertPath);
@@ -282,8 +324,56 @@ async function regenerate(paths, identity, commonName, validDays, newCa) {
282
324
  await writeFile(`${paths.certPath}.new`, chainPem, "utf-8");
283
325
  renameSync(`${paths.keyPath}.new`, paths.keyPath);
284
326
  renameSync(`${paths.certPath}.new`, paths.certPath);
327
+ return dropped;
285
328
  }
286
- function describe(paths, generated, reason, caRotated, previousFingerprintSha256, reissueError) {
329
+ /** The identity as far as the CA on disk may certify it; unchanged while that CA is absent or unconstrained. */
330
+ function constrainToDiskCa(paths, identity) {
331
+ try {
332
+ const constraints = readNameConstraints(new X509Certificate$1(readFileSync(paths.caCertPath, "utf-8")));
333
+ return constraints === null ? {
334
+ identity,
335
+ dropped: []
336
+ } : permitsIdentity(constraints, identity);
337
+ } catch {
338
+ return {
339
+ identity,
340
+ dropped: []
341
+ };
342
+ }
343
+ }
344
+ /**
345
+ * One-time replacement of an UNCONSTRAINED generated CA (D873): keep the old material
346
+ * beside the new one until the new leaf is serving (`retirePreviousLocalCa`), and write the
347
+ * marker the admin UI reads to tell the operator that clients must re-trust the CA.
348
+ */
349
+ function archivePreviousCa(paths, replacement) {
350
+ if (!existsSync(paths.caCertPath)) return;
351
+ const previousCert = `${paths.caCertPath}.previous`;
352
+ const previousKey = `${paths.caKeyPath}.previous`;
353
+ copyFileSync(paths.caCertPath, previousCert);
354
+ if (existsSync(paths.caKeyPath)) copyFileSync(paths.caKeyPath, previousKey);
355
+ const previousFingerprint = readFingerprint(paths.caCertPath);
356
+ writeOwnerOnlyFileSync(join(dirname(paths.caCertPath), "ca-replaced.json"), `${JSON.stringify({
357
+ replacedAt: (/* @__PURE__ */ new Date()).toISOString(),
358
+ previousFingerprintSha256: previousFingerprint,
359
+ fingerprintSha256: new X509Certificate(replacement.certPem).fingerprint256
360
+ })}\n`);
361
+ }
362
+ /** Marker path for the status reader and the retire step. */
363
+ function caReplacementMarkerFile(dataDir) {
364
+ return join(dataDir, "tls", "ca-replaced.json");
365
+ }
366
+ /**
367
+ * Called once the HTTPS listener serves the leaf of the replacement CA: drop the retired
368
+ * unconstrained CA, key first. Idempotent; returns whether anything was removed.
369
+ */
370
+ function retirePreviousLocalCa(dataDir) {
371
+ const base = join(dataDir, "tls", "camstack-ca");
372
+ const present = [`${base}.key.previous`, `${base}.crt.previous`].filter((file) => existsSync(file));
373
+ for (const file of present) rmSync(file, { force: true });
374
+ return present.length > 0;
375
+ }
376
+ function describe(paths, generated, reason, caRotated, previousFingerprintSha256, reissueError, droppedNames) {
287
377
  const leaf = new X509Certificate(readFileSync(paths.certPath));
288
378
  const ca = new X509Certificate(readFileSync(paths.caCertPath));
289
379
  const san = leaf.subjectAltName ?? "";
@@ -297,6 +387,7 @@ function describe(paths, generated, reason, caRotated, previousFingerprintSha256
297
387
  caFingerprintSha256: ca.fingerprint256,
298
388
  validTo: new Date(leaf.validTo).toISOString(),
299
389
  sans: san === "" ? [] : san.split(", "),
390
+ droppedNames,
300
391
  reissueError
301
392
  };
302
393
  }
@@ -312,6 +403,140 @@ function loadTlsCert(certPath, keyPath) {
312
403
  };
313
404
  }
314
405
  //#endregion
406
+ //#region src/tls/uploaded-cert.ts
407
+ var MESSAGES = {
408
+ "tls.cert-unparsable": "Certificate PEM could not be parsed.",
409
+ "tls.key-unparsable": "Private key PEM could not be parsed.",
410
+ "tls.key-mismatch": "Private key does not match the certificate.",
411
+ "tls.missing-server-auth": "Certificate is missing extendedKeyUsage = serverAuth.",
412
+ "tls.ca-required": "This certificate was not issued by a public certificate authority, so its CA certificate is required: devices need it to trust the hub. Paste the CA certificate (for mkcert, the rootCA.pem in the folder `mkcert -CAROOT` prints).",
413
+ "tls.ca-unparsable": "The CA certificate could not be parsed.",
414
+ "tls.ca-not-issuer": "The certificate was not issued by the CA certificate you supplied.",
415
+ "tls.ca-not-a-ca": "The supplied CA certificate is not a certificate authority (CA:TRUE is missing).",
416
+ "tls.ca-expired": "The supplied CA certificate has expired or is not valid yet."
417
+ };
418
+ function failure(code) {
419
+ return {
420
+ ok: false,
421
+ code,
422
+ error: MESSAGES[code]
423
+ };
424
+ }
425
+ /**
426
+ * Operator-supplied material must parse, match its key, and carry
427
+ * `extendedKeyUsage = serverAuth` — the same leaf defects D227 refused
428
+ * to keep generating.
429
+ */
430
+ function validateUploadedTls(certPem, keyPem) {
431
+ let leaf;
432
+ try {
433
+ leaf = new X509Certificate(certPem);
434
+ } catch {
435
+ return failure("tls.cert-unparsable");
436
+ }
437
+ try {
438
+ if (!leaf.checkPrivateKey(createPrivateKey(keyPem))) return failure("tls.key-mismatch");
439
+ } catch {
440
+ return failure("tls.key-unparsable");
441
+ }
442
+ const eku = leaf.keyUsage;
443
+ if (eku === void 0 || !eku.includes("1.3.6.1.5.5.7.3.1")) return failure("tls.missing-server-auth");
444
+ return {
445
+ ok: true,
446
+ fingerprintSha256: leaf.fingerprint256,
447
+ validTo: new Date(leaf.validTo).toISOString()
448
+ };
449
+ }
450
+ /** Every certificate in a PEM bundle, in order. */
451
+ function pemCertificates(pem) {
452
+ const blocks = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g);
453
+ return blocks === null ? [] : blocks.map((block) => `${block}\n`);
454
+ }
455
+ function parseAll(pems) {
456
+ const parsed = [];
457
+ for (const pem of pems) try {
458
+ parsed.push(new X509Certificate(pem));
459
+ } catch {}
460
+ return parsed;
461
+ }
462
+ function issuedBy(child, issuer) {
463
+ try {
464
+ return child.checkIssued(issuer) && child.verify(issuer.publicKey);
465
+ } catch {
466
+ return false;
467
+ }
468
+ }
469
+ var defaultPublicRoots;
470
+ /** The trust anchors of public CAs that ship with Node. */
471
+ function publicRootCertificates() {
472
+ defaultPublicRoots ??= parseAll(rootCertificates);
473
+ return defaultPublicRoots;
474
+ }
475
+ /**
476
+ * True when the served chain leads to a public CA that clients already trust:
477
+ * the leaf, or one of the chain's own certificates, is signed by a public root.
478
+ * Such a certificate needs nothing installed on a device, so no CA is required.
479
+ */
480
+ function chainsToPublicRoot(chainPem, roots = publicRootCertificates()) {
481
+ const [leaf, ...rest] = parseAll(pemCertificates(chainPem));
482
+ if (leaf === void 0) return false;
483
+ let current = leaf;
484
+ const pool = [...rest];
485
+ for (let depth = 0; depth < 8; depth += 1) {
486
+ if (roots.some((root) => issuedBy(current, root))) return true;
487
+ const next = pool.find((candidate) => candidate !== current && issuedBy(current, candidate));
488
+ if (next === void 0) return false;
489
+ pool.splice(pool.indexOf(next), 1);
490
+ current = next;
491
+ }
492
+ return false;
493
+ }
494
+ /** Is `cert` a usable CA at `now`? */
495
+ function caProblem(cert, now) {
496
+ if (!cert.ca) return "tls.ca-not-a-ca";
497
+ if (now < new Date(cert.validFrom) || now > new Date(cert.validTo)) return "tls.ca-expired";
498
+ return null;
499
+ }
500
+ /**
501
+ * The CA rule for an upload (D873). A CA the operator supplies must parse, be a CA, be in
502
+ * date and actually have signed the leaf. Without one, the upload is accepted only when
503
+ * its own chain carries the issuer or leads to a public root; anything else is refused,
504
+ * because a device cannot be told to trust a certificate nobody can hand it the CA of.
505
+ */
506
+ function checkUploadedCa(chainPem, caPem, options) {
507
+ const now = options?.now ?? /* @__PURE__ */ new Date();
508
+ const [leaf, ...chainRest] = parseAll(pemCertificates(chainPem));
509
+ if (leaf === void 0) return failure("tls.cert-unparsable");
510
+ if (caPem !== void 0 && caPem.trim() !== "") {
511
+ const candidates = parseAll(pemCertificates(caPem));
512
+ if (candidates.length === 0) return failure("tls.ca-unparsable");
513
+ return pickIssuer(leaf, candidates, now, chainPem, options?.publicRoots);
514
+ }
515
+ const fromChain = chainRest.filter((cert) => issuedBy(leaf, cert));
516
+ if (fromChain.length > 0) return pickIssuer(leaf, fromChain, now, chainPem, options?.publicRoots);
517
+ if (chainsToPublicRoot(chainPem, options?.publicRoots)) return {
518
+ ok: true,
519
+ issuerPem: null,
520
+ publicChain: true
521
+ };
522
+ return failure("tls.ca-required");
523
+ }
524
+ function pickIssuer(leaf, candidates, now, chainPem, publicRoots) {
525
+ const signers = candidates.filter((candidate) => issuedBy(leaf, candidate));
526
+ if (signers.length === 0) return failure("tls.ca-not-issuer");
527
+ let firstProblem = null;
528
+ for (const signer of signers) {
529
+ const problem = caProblem(signer, now);
530
+ if (problem === null) return {
531
+ ok: true,
532
+ issuerPem: `${signer.toString().trimEnd()}\n`,
533
+ publicChain: chainsToPublicRoot(chainPem, publicRoots)
534
+ };
535
+ firstProblem ??= problem;
536
+ }
537
+ return failure(firstProblem ?? "tls.ca-not-issuer");
538
+ }
539
+ //#endregion
315
540
  //#region src/tls/tls-status.ts
316
541
  function tlsDir(dataDir) {
317
542
  return join(dataDir, "tls");
@@ -344,6 +569,44 @@ function writeExtraSans(dataDir, sans) {
344
569
  mkdirSync(tlsDir(dataDir), { recursive: true });
345
570
  writeFileSync(join(tlsDir(dataDir), "extra-sans.json"), `${JSON.stringify(sans)}\n`, "utf-8");
346
571
  }
572
+ /** The kept copy of the last upload: the chain and key as the operator supplied them. */
573
+ var STORED_UPLOAD_CERT_FILE = "uploaded.crt";
574
+ var STORED_UPLOAD_KEY_FILE = "uploaded.key";
575
+ var CA_REPLACED_MARKER_FILE = "ca-replaced.json";
576
+ function readStoredUpload(dir) {
577
+ const certPath = join(dir, STORED_UPLOAD_CERT_FILE);
578
+ if (!existsSync(certPath) || !existsSync(join(dir, "uploaded.key"))) return null;
579
+ try {
580
+ const leaf = new X509Certificate(readFileSync(certPath, "utf-8"));
581
+ return {
582
+ leafFingerprintSha256: leaf.fingerprint256,
583
+ validTo: new Date(leaf.validTo).toISOString(),
584
+ hasCa: existsSync(join(dir, UPLOADED_CA_FILE))
585
+ };
586
+ } catch {
587
+ return null;
588
+ }
589
+ }
590
+ function readLocalCaConstrained(dir) {
591
+ const caPath = join(dir, "camstack-ca.crt");
592
+ if (!existsSync(caPath)) return null;
593
+ try {
594
+ return isConstrainedCa(new X509Certificate$1(readFileSync(caPath, "utf-8")));
595
+ } catch {
596
+ return null;
597
+ }
598
+ }
599
+ function readCaReplacedAt(dir) {
600
+ const path = join(dir, CA_REPLACED_MARKER_FILE);
601
+ if (!existsSync(path)) return null;
602
+ try {
603
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
604
+ const at = parsed !== null && typeof parsed === "object" ? Reflect.get(parsed, "replacedAt") : null;
605
+ return typeof at === "string" ? at : null;
606
+ } catch {
607
+ return null;
608
+ }
609
+ }
347
610
  /** Where an upload's own CA is kept — never over the local CA's `camstack-ca.crt`. */
348
611
  var UPLOADED_CA_FILE = "uploaded-ca.crt";
349
612
  /** Every certificate in a PEM bundle, in order. */
@@ -398,7 +661,11 @@ function readTlsAccessStatus(dataDir, servedLeafFingerprint) {
398
661
  sans: [],
399
662
  caCertPem: (mode === "uploaded" ? null : generatedIssuer(dir))?.pem ?? null,
400
663
  reissueError: null,
401
- restartRequired: false
664
+ restartRequired: false,
665
+ caMissing: false,
666
+ localCaConstrained: readLocalCaConstrained(dir),
667
+ caReplacedAt: readCaReplacedAt(dir),
668
+ storedUpload: readStoredUpload(dir)
402
669
  };
403
670
  try {
404
671
  const chainPem = readFileSync(certPath, "utf-8");
@@ -413,7 +680,11 @@ function readTlsAccessStatus(dataDir, servedLeafFingerprint) {
413
680
  sans: san === "" ? [] : san.split(", "),
414
681
  caCertPem: ca?.pem ?? null,
415
682
  reissueError: null,
416
- restartRequired: servedLeafFingerprint !== null && servedLeafFingerprint !== leaf.fingerprint256
683
+ restartRequired: servedLeafFingerprint !== null && servedLeafFingerprint !== leaf.fingerprint256,
684
+ caMissing: mode === "uploaded" && ca === null && !chainsToPublicRoot(chainPem),
685
+ localCaConstrained: readLocalCaConstrained(dir),
686
+ caReplacedAt: readCaReplacedAt(dir),
687
+ storedUpload: readStoredUpload(dir)
417
688
  };
418
689
  } catch {
419
690
  return {
@@ -424,7 +695,11 @@ function readTlsAccessStatus(dataDir, servedLeafFingerprint) {
424
695
  sans: [],
425
696
  caCertPem: null,
426
697
  reissueError: "Certificate on disk could not be parsed.",
427
- restartRequired: false
698
+ restartRequired: false,
699
+ caMissing: false,
700
+ localCaConstrained: readLocalCaConstrained(dir),
701
+ caReplacedAt: readCaReplacedAt(dir),
702
+ storedUpload: readStoredUpload(dir)
428
703
  };
429
704
  }
430
705
  }
@@ -446,7 +721,8 @@ async function prepareBootTls(dataDir, options) {
446
721
  mode: "uploaded",
447
722
  pair: loadTlsCert(certPath, keyPath),
448
723
  ensured: void 0,
449
- uploadedMissing: false
724
+ uploadedMissing: false,
725
+ dormantCaReplaced: await replaceUnconstrainedLocalCa(dataDir, options)
450
726
  };
451
727
  const uploadedMissing = marked === "uploaded";
452
728
  if (uploadedMissing) writeTlsMode(dataDir, "generated");
@@ -455,7 +731,8 @@ async function prepareBootTls(dataDir, options) {
455
731
  mode: "generated",
456
732
  pair: loadTlsCert(ensured.certPath, ensured.keyPath),
457
733
  ensured,
458
- uploadedMissing
734
+ uploadedMissing,
735
+ dormantCaReplaced: false
459
736
  };
460
737
  }
461
738
  //#endregion
@@ -533,66 +810,51 @@ function reloadHttpsListener(pair) {
533
810
  }
534
811
  }
535
812
  //#endregion
536
- //#region src/tls/uploaded-cert.ts
537
- /**
538
- * Operator-supplied material must parse, match its key, and carry
539
- * `extendedKeyUsage = serverAuth` — the same leaf defects D227 refused
540
- * to keep generating.
541
- */
542
- function validateUploadedTls(certPem, keyPem) {
543
- let leaf;
544
- try {
545
- leaf = new X509Certificate(certPem);
546
- } catch {
547
- return {
548
- ok: false,
549
- error: "Certificate PEM could not be parsed."
550
- };
551
- }
552
- try {
553
- if (!leaf.checkPrivateKey(createPrivateKey(keyPem))) return {
554
- ok: false,
555
- error: "Private key does not match the certificate."
556
- };
557
- } catch {
558
- return {
559
- ok: false,
560
- error: "Private key PEM could not be parsed."
561
- };
562
- }
563
- const eku = leaf.keyUsage;
564
- if (eku === void 0 || !eku.includes("1.3.6.1.5.5.7.3.1")) return {
565
- ok: false,
566
- error: "Certificate is missing extendedKeyUsage = serverAuth."
567
- };
568
- return {
569
- ok: true,
570
- fingerprintSha256: leaf.fingerprint256,
571
- validTo: new Date(leaf.validTo).toISOString()
572
- };
573
- }
574
- //#endregion
575
813
  //#region src/tls/tls-actions.ts
576
814
  function withNewline(pem) {
577
815
  return pem.endsWith("\n") ? pem : `${pem}\n`;
578
816
  }
579
817
  /**
580
- * Store an operator's certificate. The leaf (or chain) and key replace the
581
- * served pair; a CA, when given, goes to its own file. The local CA
582
- * (`camstack-ca.crt` + key) is never touched — it did not sign the upload,
583
- * and it is what "Revert to generated" and every device that trusts it
584
- * still need.
818
+ * Store an operator's certificate (D873). The material is validated first, and a
819
+ * certificate that does not lead to a public CA must come with the CA that signed it
820
+ * (validated: it is a CA, in date, and issued the leaf) - otherwise "Download CA" would
821
+ * have nothing to hand out. Nothing is written when anything is refused.
822
+ *
823
+ * The leaf (or chain) and key are written twice: as the served pair, and as a kept copy
824
+ * (`uploaded.crt` / `uploaded.key`, with the issuing CA in `uploaded-ca.crt`) that
825
+ * "Revert to generated" leaves in place so the upload can be selected again. The local
826
+ * CA (`camstack-ca.crt` + key) is never touched - it did not sign the upload, and it is
827
+ * what every device that trusts it still needs.
585
828
  */
586
- async function storeUploadedCertificate(dataDir, input) {
829
+ async function storeUploadedCertificate(dataDir, input, options) {
587
830
  const valid = validateUploadedTls(input.certPem, input.keyPem);
588
- if (!valid.ok) throw new Error(valid.error);
831
+ if (!valid.ok) throw userError(valid.code, {}, valid.error);
832
+ const ca = checkUploadedCa(input.certPem, input.caPem, options);
833
+ if (!ca.ok) throw userError(ca.code, {}, ca.error);
589
834
  const dir = join(dataDir, "tls");
590
835
  await mkdir(dir, { recursive: true });
591
- await writeFile(join(dir, "camstack.crt"), withNewline(input.certPem));
592
- writeOwnerOnlyFileSync(join(dir, "camstack.key"), withNewline(input.keyPem));
836
+ const certText = withNewline(input.certPem);
837
+ const keyText = withNewline(input.keyPem);
838
+ writeOwnerOnlyFileSync(join(dir, STORED_UPLOAD_KEY_FILE), keyText);
839
+ await writeFile(join(dir, STORED_UPLOAD_CERT_FILE), certText);
593
840
  const caPath = join(dir, UPLOADED_CA_FILE);
594
- if (input.caPem !== void 0 && input.caPem.trim() !== "") await writeFile(caPath, withNewline(input.caPem));
841
+ if (ca.issuerPem !== null) await writeFile(caPath, ca.issuerPem);
595
842
  else rmSync(caPath, { force: true });
843
+ await writeFile(join(dir, "camstack.crt"), certText);
844
+ writeOwnerOnlyFileSync(join(dir, "camstack.key"), keyText);
845
+ writeTlsMode(dataDir, "uploaded");
846
+ }
847
+ /**
848
+ * Serve the kept upload again after a revert (D873). Refused with a clear error when none
849
+ * is kept; the generated material stays on disk and is untouched.
850
+ */
851
+ async function useStoredUploadedCertificate(dataDir) {
852
+ const dir = join(dataDir, "tls");
853
+ const certPath = join(dir, STORED_UPLOAD_CERT_FILE);
854
+ const keyPath = join(dir, STORED_UPLOAD_KEY_FILE);
855
+ if (!existsSync(certPath) || !existsSync(keyPath)) throw userError("tls.no-stored-upload", {}, "No uploaded certificate is kept on this node.");
856
+ copyFileSync(certPath, join(dir, "camstack.crt"));
857
+ copyFileSync(keyPath, join(dir, "camstack.key"));
596
858
  writeTlsMode(dataDir, "uploaded");
597
859
  }
598
860
  /** Issue a new generated leaf under the local CA. Refused while an upload is served. */
@@ -600,10 +862,20 @@ async function regenerateCertificate(dataDir, options) {
600
862
  if (readTlsMode(dataDir) === "uploaded") throw new Error("Cannot regenerate an uploaded certificate — revert to generated first.");
601
863
  await reissueTlsLeaf(dataDir, options);
602
864
  }
603
- /** Drop the upload and serve the generated local-CA leaf again (D227). */
865
+ /**
866
+ * Serve the generated local-CA leaf again (D227). The uploaded certificate and its CA stay
867
+ * on disk (D873) so it can be selected again; an upload made before they were kept is
868
+ * copied aside first, from the pair being served.
869
+ */
604
870
  async function revertToGeneratedCertificate(dataDir, options) {
871
+ const dir = join(dataDir, "tls");
872
+ const keptCert = join(dir, STORED_UPLOAD_CERT_FILE);
873
+ const keptKey = join(dir, STORED_UPLOAD_KEY_FILE);
874
+ if (readTlsMode(dataDir) === "uploaded" && !existsSync(keptCert) && existsSync(join(dir, "camstack.crt")) && existsSync(join(dir, "camstack.key"))) {
875
+ copyFileSync(join(dir, "camstack.crt"), keptCert);
876
+ copyFileSync(join(dir, "camstack.key"), keptKey);
877
+ }
605
878
  writeTlsMode(dataDir, "generated");
606
- rmSync(join(dataDir, "tls", UPLOADED_CA_FILE), { force: true });
607
879
  await ensureTlsCert(dataDir, options);
608
880
  }
609
881
  /**
@@ -623,7 +895,7 @@ function applyCertificateToListener(dataDir) {
623
895
  //#region src/builtins/local-network/hub-mdns-ciao.ts
624
896
  function createCiaoPublisher() {
625
897
  return { async publish(announcement) {
626
- const { getResponder } = await import("./lib-BeGzVQcd.mjs").then((m) => /* @__PURE__ */ __toESM(m.default));
898
+ const { getResponder } = await import("./lib-DK38ompL.mjs").then((m) => /* @__PURE__ */ __toESM(m.default));
627
899
  const responder = getResponder();
628
900
  const service = responder.createService({
629
901
  name: announcement.name,
@@ -645,4 +917,4 @@ function createCiaoPublisher() {
645
917
  } };
646
918
  }
647
919
  //#endregion
648
- export { DEFAULT_LAN_HTTP_PORT as C, closeLanHttp as D, bindPendingLanHttp as E, readLanHttpState as O, reissueTlsLeaf as S, applyLanHttp as T, splitPemCertificates as _, storeUploadedCertificate as a, ensureTlsCert as b, leafFingerprintOf as c, servedHttpsLeafFingerprint as d, prepareBootTls as f, readTlsMode as g, readTlsAccessStatus as h, revertToGeneratedCertificate as i, registerLanHttpHandler as k, registerHttpsListener as l, readExtraSans as m, applyCertificateToListener as n, validateUploadedTls as o, UPLOADED_CA_FILE as p, regenerateCertificate as r, clearHttpsListener as s, createCiaoPublisher as t, reloadHttpsListener as u, writeExtraSans as v, allFamiliesListenHost as w, loadTlsCert as x, writeTlsMode as y };
920
+ export { allFamiliesListenHost as A, caReplacementMarkerFile as C, replaceUnconstrainedLocalCa as D, reissueTlsLeaf as E, registerLanHttpHandler as F, bindPendingLanHttp as M, closeLanHttp as N, retirePreviousLocalCa as O, readLanHttpState as P, validateUploadedTls as S, loadTlsCert as T, splitPemCertificates as _, storeUploadedCertificate as a, chainsToPublicRoot as b, leafFingerprintOf as c, servedHttpsLeafFingerprint as d, prepareBootTls as f, readTlsMode as g, readTlsAccessStatus as h, revertToGeneratedCertificate as i, applyLanHttp as j, DEFAULT_LAN_HTTP_PORT as k, registerHttpsListener as l, readExtraSans as m, applyCertificateToListener as n, useStoredUploadedCertificate as o, UPLOADED_CA_FILE as p, regenerateCertificate as r, clearHttpsListener as s, createCiaoPublisher as t, reloadHttpsListener as u, writeExtraSans as v, ensureTlsCert as w, checkUploadedCa as x, writeTlsMode as y };