@camstack/system 1.2.428 → 1.2.430
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/agent-https-names-CpUSuP4w.js +120 -0
- package/dist/agent-https-names-uetr4OQl.mjs +91 -0
- package/dist/auth/auth-audit-log.d.ts +1 -1
- package/dist/auth/device-ca-store.d.ts +57 -0
- package/dist/auth/device-ca.d.ts +75 -0
- package/dist/auth/device-credential-manager.d.ts +154 -0
- package/dist/auth/login-session-manager.d.ts +14 -2
- package/dist/auth/user-management-internal.d.ts +10 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +10 -6
- package/dist/builtins/keychain/index.mjs +8 -4
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.d.ts +6 -0
- package/dist/builtins/local-auth/local-auth.addon.js +955 -47
- package/dist/builtins/local-auth/local-auth.addon.mjs +946 -41
- package/dist/builtins/local-network/local-network.addon.js +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/file-host-client.d.ts +11 -0
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +34 -4
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +34 -4
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.d.ts +18 -0
- package/dist/builtins/system-config/system-config.addon.js +834 -18
- package/dist/builtins/system-config/system-config.addon.mjs +819 -19
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{composition-sources-BQNxXQZG.mjs → composition-sources-Cw_TXEq9.mjs} +1 -1
- package/dist/{composition-sources-DvCGZEUI.js → composition-sources-o5MPSlk6.js} +1 -1
- package/dist/{constants-CCSJGnjs.js → constants-BcNaVkvI.js} +1 -1
- package/dist/{constants-CR4If_lP.mjs → constants-CttE77VF.mjs} +1 -1
- package/dist/device-ca-B-BTxeCj.mjs +355 -0
- package/dist/device-ca-CKiqh2Iu.js +462 -0
- package/dist/{dist-DHSJV4Qc.mjs → dist-CXG6muCR.mjs} +1052 -35
- package/dist/{dist-BQG3QFg7.js → dist-DxJdci75.js} +1099 -34
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +59 -12
- package/dist/index.mjs +10 -9
- package/dist/kernel/index.d.ts +2 -2
- package/dist/kernel/moleculer/mesh-mtls/index.d.ts +4 -1
- package/dist/kernel/moleculer/mesh-mtls/mesh-certificate-registry.d.ts +91 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-enroll.d.ts +4 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-peer-transport.d.ts +23 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-renewal.d.ts +126 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-tls-gate.d.ts +40 -0
- package/dist/kernel/moleculer/mesh-mtls/tls-tcp-transporter.d.ts +15 -0
- package/dist/{restore-archive-Dccm8lkA.js → restore-archive-CQJNoqRK.js} +1 -1
- package/dist/{restore-archive-DH1Pxq_1.mjs → restore-archive-D_wBL1OO.mjs} +1 -1
- package/dist/{retired-settings-keys-CPz7zpza.mjs → retired-settings-keys-EmDe11cP.mjs} +1 -1
- package/dist/{retired-settings-keys-BfGaSeQI.js → retired-settings-keys-g1oifXxR.js} +1 -1
- package/dist/tls/agent-https-names.d.ts +27 -0
- package/dist/tls/agent-https.d.ts +111 -0
- package/dist/tls/cert-issue.d.ts +7 -0
- package/dist/tls/index.d.ts +3 -0
- package/dist/{tls-BgImdxCO.js → tls-C5IYg3V5.js} +406 -0
- package/dist/{tls-CaOLZ09W.mjs → tls-CpcGkeJV.mjs} +314 -4
- package/dist/{wal-maintenance-DItkuOvu.js → wal-maintenance-BW1LIHi0.js} +0 -0
- package/dist/{wal-maintenance-Drqhcjit.mjs → wal-maintenance-vhPZLYuA.mjs} +0 -0
- package/package.json +4 -1
- package/dist/jwt-signing-key-BZm055IP.js +0 -174
- package/dist/jwt-signing-key-DL-qk1SM.mjs +0 -127
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM, r as __exportAll, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { Br as hydrateSchema, Fr as defineCustomActions, Gr as isSecretMarker, Pr as customAction, Wr as isSecretClear, _r as BaseAddon, ci as transformSecretWrite, gr as errMsg, ii as secretTreeFromSchema, qr as makeSecretMarker } from "../../dist-CXG6muCR.mjs";
|
|
3
3
|
import { t as require_safe_buffer } from "../../safe-buffer-B1OjL1I9.mjs";
|
|
4
|
-
import { a as MESH_CA_KEY_REF, b as setProcessClusterSecretGate, c as MESH_JOIN_TOKEN_MAX_TTL_SEC, d as CLUSTER_SECRET_REF, i as MESH_CA_FIELD, l as SERVER_AUTH_OID, n as DEFAULT_HUB_TLS_TCP_PORT, o as MESH_CA_SETTINGS_KEY, r as MESH_CA_COMMON_NAME, s as MESH_CA_VALIDITY_DAYS, t as CLIENT_AUTH_OID, v as hashClusterSecret } from "../../constants-CR4If_lP.mjs";
|
|
5
4
|
import { a as KeyUsagesExtension, c as Pkcs10CertificateRequestGenerator, d as X509Certificate$1, f as X509CertificateGenerator, i as KeyUsageFlags, n as BasicConstraintsExtension, o as PemConverter, p as cryptoProvider, r as ExtendedKeyUsageExtension, s as Pkcs10CertificateRequest, t as AuthorityKeyIdentifierExtension, u as SubjectKeyIdentifierExtension } from "../../x509.es-CAjSsrkk.mjs";
|
|
5
|
+
import { a as setProcessAgentHttpsNames, n as AGENT_HTTPS_NAMES_COLUMNS, r as AgentHttpsNames, t as AGENT_HTTPS_NAMES_COLLECTION } from "../../agent-https-names-uetr4OQl.mjs";
|
|
6
|
+
import { a as MESH_CA_KEY_REF, b as setProcessClusterSecretGate, c as MESH_JOIN_TOKEN_MAX_TTL_SEC, d as CLUSTER_SECRET_REF, i as MESH_CA_FIELD, l as SERVER_AUTH_OID, n as DEFAULT_HUB_TLS_TCP_PORT, o as MESH_CA_SETTINGS_KEY, r as MESH_CA_COMMON_NAME, s as MESH_CA_VALIDITY_DAYS, t as CLIENT_AUTH_OID, v as hashClusterSecret } from "../../constants-CttE77VF.mjs";
|
|
6
7
|
import { t as require_src } from "../../src-hG4grMU4.mjs";
|
|
7
8
|
import { z } from "zod";
|
|
8
9
|
import { X509Certificate, createHash, createHmac, randomBytes, timingSafeEqual, webcrypto } from "node:crypto";
|
|
@@ -416,7 +417,7 @@ async function exportMeshPrivateKeyPem(key) {
|
|
|
416
417
|
}
|
|
417
418
|
//#endregion
|
|
418
419
|
//#region src/kernel/moleculer/mesh-mtls/mesh-ca-keeper.ts
|
|
419
|
-
var SLOT$
|
|
420
|
+
var SLOT$3 = Symbol.for("camstack.mesh.ca");
|
|
420
421
|
var NOT_LOADED = {
|
|
421
422
|
state: "unavailable",
|
|
422
423
|
reason: "not-loaded"
|
|
@@ -428,10 +429,10 @@ function isState(value) {
|
|
|
428
429
|
return state === "unavailable" && typeof Reflect.get(value, "reason") === "string";
|
|
429
430
|
}
|
|
430
431
|
function setProcessMeshCa(state) {
|
|
431
|
-
Reflect.set(globalThis, SLOT$
|
|
432
|
+
Reflect.set(globalThis, SLOT$3, state);
|
|
432
433
|
}
|
|
433
434
|
function getProcessMeshCaState() {
|
|
434
|
-
const value = Reflect.get(globalThis, SLOT$
|
|
435
|
+
const value = Reflect.get(globalThis, SLOT$3);
|
|
435
436
|
return isState(value) ? value : NOT_LOADED;
|
|
436
437
|
}
|
|
437
438
|
function requireProcessMeshCa() {
|
|
@@ -573,10 +574,10 @@ async function loadOrGenerateMeshCa(deps) {
|
|
|
573
574
|
var MeshCrl = class {
|
|
574
575
|
serials = /* @__PURE__ */ new Set();
|
|
575
576
|
revoke(serialNumber) {
|
|
576
|
-
this.serials.add(normalizeSerial(serialNumber));
|
|
577
|
+
this.serials.add(normalizeSerial$1(serialNumber));
|
|
577
578
|
}
|
|
578
579
|
isRevoked(serialNumber) {
|
|
579
|
-
return this.serials.has(normalizeSerial(serialNumber));
|
|
580
|
+
return this.serials.has(normalizeSerial$1(serialNumber));
|
|
580
581
|
}
|
|
581
582
|
list() {
|
|
582
583
|
return [...this.serials];
|
|
@@ -584,21 +585,650 @@ var MeshCrl = class {
|
|
|
584
585
|
/** Restore from persisted serials (system-settings). */
|
|
585
586
|
replaceAll(serials) {
|
|
586
587
|
this.serials.clear();
|
|
587
|
-
for (const s of serials) this.serials.add(normalizeSerial(s));
|
|
588
|
+
for (const s of serials) this.serials.add(normalizeSerial$1(s));
|
|
588
589
|
}
|
|
589
590
|
};
|
|
590
|
-
function normalizeSerial(serial) {
|
|
591
|
+
function normalizeSerial$1(serial) {
|
|
591
592
|
return serial.replace(/^0x/i, "").replace(/:/g, "").toLowerCase();
|
|
592
593
|
}
|
|
593
|
-
var SLOT$
|
|
594
|
+
var SLOT$2 = Symbol.for("camstack.mesh.crl");
|
|
594
595
|
function processMeshCrl() {
|
|
595
|
-
const existing = Reflect.get(globalThis, SLOT$
|
|
596
|
+
const existing = Reflect.get(globalThis, SLOT$2);
|
|
596
597
|
if (existing instanceof MeshCrl) return existing;
|
|
597
598
|
const crl = new MeshCrl();
|
|
598
|
-
Reflect.set(globalThis, SLOT$
|
|
599
|
+
Reflect.set(globalThis, SLOT$2, crl);
|
|
599
600
|
return crl;
|
|
600
601
|
}
|
|
601
602
|
//#endregion
|
|
603
|
+
//#region src/kernel/moleculer/mesh-mtls/mesh-certificate-registry.ts
|
|
604
|
+
/**
|
|
605
|
+
* The hub's record of every mesh leaf (D856): which certificate each node holds,
|
|
606
|
+
* when it expires, and which serials are revoked.
|
|
607
|
+
*
|
|
608
|
+
* Three ways a row appears:
|
|
609
|
+
* - `enroll` / `renewal` / `boot` - the hub issued it and wrote it down at the
|
|
610
|
+
* moment of issuing (the authoritative record);
|
|
611
|
+
* - `observed` - a node enrolled before this record existed (or by a path that
|
|
612
|
+
* did not write it) presented this leaf on its LIVE mutual-TLS connection.
|
|
613
|
+
*
|
|
614
|
+
* Revoked serials are persisted here and replayed into the in-memory CRL at boot,
|
|
615
|
+
* so a revocation survives a hub restart. Hub only: agents persist none of it.
|
|
616
|
+
*/
|
|
617
|
+
/**
|
|
618
|
+
* @durable class=config owner=system-config
|
|
619
|
+
* write="the hub records every mesh leaf it issues (enroll, renewal, boot) and every
|
|
620
|
+
* leaf a connected node is observed presenting; a revocation stamps revokedAt"
|
|
621
|
+
* retention="a revoked or expired row is kept 90 days past its end, then reaped; a
|
|
622
|
+
* live row is never deleted. Forgetting a node does not delete its rows."
|
|
623
|
+
*/
|
|
624
|
+
var MESH_NODE_CERTIFICATES_COLLECTION = "mesh_node_certificates";
|
|
625
|
+
var MESH_NODE_CERTIFICATES_COLUMNS = [
|
|
626
|
+
{
|
|
627
|
+
name: "id",
|
|
628
|
+
type: "TEXT",
|
|
629
|
+
primaryKey: true,
|
|
630
|
+
notNull: true
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
name: "nodeId",
|
|
634
|
+
type: "TEXT",
|
|
635
|
+
notNull: true
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
name: "serial",
|
|
639
|
+
type: "TEXT",
|
|
640
|
+
notNull: true
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
name: "fingerprint",
|
|
644
|
+
type: "TEXT",
|
|
645
|
+
notNull: true
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
name: "notBefore",
|
|
649
|
+
type: "INTEGER",
|
|
650
|
+
notNull: true
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
name: "notAfter",
|
|
654
|
+
type: "INTEGER",
|
|
655
|
+
notNull: true
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
name: "issuedAt",
|
|
659
|
+
type: "INTEGER",
|
|
660
|
+
notNull: true
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
name: "issuedVia",
|
|
664
|
+
type: "TEXT",
|
|
665
|
+
notNull: true
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
name: "revokedAt",
|
|
669
|
+
type: "INTEGER"
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
name: "revokedReason",
|
|
673
|
+
type: "TEXT"
|
|
674
|
+
}
|
|
675
|
+
];
|
|
676
|
+
var RETENTION_MS = 90 * 864e5;
|
|
677
|
+
/**
|
|
678
|
+
* A leaf the hub signed for a renewal that did not complete. It must not be the node's
|
|
679
|
+
* `current` leaf, but it is NOT on the CRL: the node may still hold it (the swap
|
|
680
|
+
* could have landed without the hub seeing it), and cutting it off would be the
|
|
681
|
+
* outage the hand-over exists to prevent. If the node is later seen presenting it, it
|
|
682
|
+
* is reinstated.
|
|
683
|
+
*/
|
|
684
|
+
var RENEWAL_ABANDONED = "renewal-abandoned";
|
|
685
|
+
var VIAS = [
|
|
686
|
+
"enroll",
|
|
687
|
+
"renewal",
|
|
688
|
+
"boot",
|
|
689
|
+
"observed"
|
|
690
|
+
];
|
|
691
|
+
/** CN, serial, fingerprint and validity of a leaf PEM (or DER). */
|
|
692
|
+
function describeMeshLeaf(cert) {
|
|
693
|
+
const x509 = new X509Certificate(cert);
|
|
694
|
+
return {
|
|
695
|
+
cn: subjectCn(x509.subject) ?? "",
|
|
696
|
+
serial: x509.serialNumber,
|
|
697
|
+
fingerprint: x509.fingerprint256,
|
|
698
|
+
notBefore: Date.parse(x509.validFrom),
|
|
699
|
+
notAfter: Date.parse(x509.validTo)
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
function normalizeSerial(serial) {
|
|
703
|
+
return serial.replace(/^0x/i, "").replace(/:/g, "").toLowerCase();
|
|
704
|
+
}
|
|
705
|
+
function parse$2(data) {
|
|
706
|
+
const str = (k) => {
|
|
707
|
+
const v = data[k];
|
|
708
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
709
|
+
};
|
|
710
|
+
const num = (k) => {
|
|
711
|
+
const v = data[k];
|
|
712
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
713
|
+
};
|
|
714
|
+
const nodeId = str("nodeId");
|
|
715
|
+
const serial = str("serial");
|
|
716
|
+
const fingerprint = str("fingerprint");
|
|
717
|
+
const notBefore = num("notBefore");
|
|
718
|
+
const notAfter = num("notAfter");
|
|
719
|
+
const issuedAt = num("issuedAt");
|
|
720
|
+
if (nodeId === null || serial === null || fingerprint === null || notBefore === null || notAfter === null || issuedAt === null) return null;
|
|
721
|
+
return {
|
|
722
|
+
nodeId,
|
|
723
|
+
serial,
|
|
724
|
+
fingerprint,
|
|
725
|
+
notBefore,
|
|
726
|
+
notAfter,
|
|
727
|
+
issuedAt,
|
|
728
|
+
issuedVia: VIAS.find((v) => v === data["issuedVia"]) ?? "observed",
|
|
729
|
+
revokedAt: num("revokedAt"),
|
|
730
|
+
revokedReason: str("revokedReason")
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
var MeshCertificateRegistry = class {
|
|
734
|
+
store;
|
|
735
|
+
deps;
|
|
736
|
+
rows = /* @__PURE__ */ new Map();
|
|
737
|
+
constructor(store, deps = {}) {
|
|
738
|
+
this.store = store;
|
|
739
|
+
this.deps = deps;
|
|
740
|
+
}
|
|
741
|
+
now() {
|
|
742
|
+
return (this.deps.now ?? Date.now)();
|
|
743
|
+
}
|
|
744
|
+
async load() {
|
|
745
|
+
const rows = await this.store.query.query({ collection: MESH_NODE_CERTIFICATES_COLLECTION });
|
|
746
|
+
for (const row of rows) {
|
|
747
|
+
const record = parse$2(row.data);
|
|
748
|
+
if (record !== null) this.rows.set(record.fingerprint, record);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/** Write down a leaf the hub just issued. Idempotent per fingerprint. */
|
|
752
|
+
async recordIssued(nodeId, certPem, via) {
|
|
753
|
+
const leaf = describeMeshLeaf(certPem);
|
|
754
|
+
const existing = this.rows.get(leaf.fingerprint);
|
|
755
|
+
if (existing !== void 0) return existing;
|
|
756
|
+
const record = {
|
|
757
|
+
nodeId,
|
|
758
|
+
serial: leaf.serial,
|
|
759
|
+
fingerprint: leaf.fingerprint,
|
|
760
|
+
notBefore: leaf.notBefore,
|
|
761
|
+
notAfter: leaf.notAfter,
|
|
762
|
+
issuedAt: this.now(),
|
|
763
|
+
issuedVia: via,
|
|
764
|
+
revokedAt: null,
|
|
765
|
+
revokedReason: null
|
|
766
|
+
};
|
|
767
|
+
await this.insert(record);
|
|
768
|
+
this.deps.logger?.info("Mesh certificate recorded", {
|
|
769
|
+
tags: { nodeId },
|
|
770
|
+
meta: {
|
|
771
|
+
via,
|
|
772
|
+
serial: leaf.serial,
|
|
773
|
+
notAfter: new Date(leaf.notAfter).toISOString()
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
return record;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* A node was seen presenting `leaf` on its live connection. A leaf the hub already
|
|
780
|
+
* knows is left alone; an unknown one is recorded `observed` (a node enrolled
|
|
781
|
+
* before this record existed).
|
|
782
|
+
*/
|
|
783
|
+
async recordObserved(nodeId, leaf) {
|
|
784
|
+
const known = this.rows.get(leaf.fingerprint);
|
|
785
|
+
if (known !== void 0) {
|
|
786
|
+
if (known.revokedReason !== "renewal-abandoned") return false;
|
|
787
|
+
const reinstated = {
|
|
788
|
+
...known,
|
|
789
|
+
revokedAt: null,
|
|
790
|
+
revokedReason: null
|
|
791
|
+
};
|
|
792
|
+
this.rows.set(known.fingerprint, reinstated);
|
|
793
|
+
await this.persist(reinstated);
|
|
794
|
+
this.deps.logger?.warn("An abandoned renewal leaf is in use - reinstated", {
|
|
795
|
+
tags: { nodeId },
|
|
796
|
+
meta: { serial: known.serial }
|
|
797
|
+
});
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
const record = {
|
|
801
|
+
nodeId,
|
|
802
|
+
serial: leaf.serial,
|
|
803
|
+
fingerprint: leaf.fingerprint,
|
|
804
|
+
notBefore: leaf.notBefore,
|
|
805
|
+
notAfter: leaf.notAfter,
|
|
806
|
+
issuedAt: this.now(),
|
|
807
|
+
issuedVia: "observed",
|
|
808
|
+
revokedAt: null,
|
|
809
|
+
revokedReason: null
|
|
810
|
+
};
|
|
811
|
+
await this.insert(record);
|
|
812
|
+
this.deps.logger?.info("Mesh certificate observed on a live connection and recorded", {
|
|
813
|
+
tags: { nodeId },
|
|
814
|
+
meta: {
|
|
815
|
+
serial: leaf.serial,
|
|
816
|
+
fingerprint: leaf.fingerprint
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
list(nodeId) {
|
|
822
|
+
return [...this.rows.values()].filter((r) => nodeId === void 0 || r.nodeId === nodeId).toSorted((a, b) => b.issuedAt - a.issuedAt);
|
|
823
|
+
}
|
|
824
|
+
/** The newest non-revoked leaf of a node, or `undefined`. */
|
|
825
|
+
currentFor(nodeId) {
|
|
826
|
+
return this.list(nodeId).find((r) => r.revokedAt === null);
|
|
827
|
+
}
|
|
828
|
+
/** Every non-revoked leaf of a node (a renewal in flight has two). */
|
|
829
|
+
liveFor(nodeId) {
|
|
830
|
+
return this.list(nodeId).filter((r) => r.revokedAt === null);
|
|
831
|
+
}
|
|
832
|
+
/** Stamp a leaf revoked (and mirror it into `crl`). `false` when unknown or already revoked. */
|
|
833
|
+
async revoke(fingerprint, reason, crl) {
|
|
834
|
+
const record = this.rows.get(fingerprint);
|
|
835
|
+
if (record === void 0 || record.revokedAt !== null) return false;
|
|
836
|
+
const revoked = {
|
|
837
|
+
...record,
|
|
838
|
+
revokedAt: this.now(),
|
|
839
|
+
revokedReason: reason
|
|
840
|
+
};
|
|
841
|
+
this.rows.set(fingerprint, revoked);
|
|
842
|
+
crl?.revoke(record.serial);
|
|
843
|
+
await this.persist(revoked);
|
|
844
|
+
this.deps.logger?.warn("Mesh certificate revoked", {
|
|
845
|
+
tags: { nodeId: record.nodeId },
|
|
846
|
+
meta: {
|
|
847
|
+
serial: record.serial,
|
|
848
|
+
reason
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Operator revocation of a whole node: EVERY leaf of the node that is not already on the
|
|
855
|
+
* CRL goes on it - the current one, an observed one, a renewal one, and a leaf that was set
|
|
856
|
+
* aside (`renewal-abandoned`: a copy of it may sit on a stolen box, and it must not stay
|
|
857
|
+
* valid just because the hub once set it aside). Returns the serials put on the CRL.
|
|
858
|
+
* A leaf revoked this way is never reinstated by `recordObserved`.
|
|
859
|
+
*/
|
|
860
|
+
async revokeAllForNode(nodeId, reason, crl) {
|
|
861
|
+
const serials = [];
|
|
862
|
+
for (const record of this.list(nodeId)) {
|
|
863
|
+
if (record.revokedAt !== null && record.revokedReason !== "renewal-abandoned") continue;
|
|
864
|
+
const revoked = {
|
|
865
|
+
...record,
|
|
866
|
+
revokedAt: record.revokedAt ?? this.now(),
|
|
867
|
+
revokedReason: reason
|
|
868
|
+
};
|
|
869
|
+
this.rows.set(record.fingerprint, revoked);
|
|
870
|
+
crl.revoke(record.serial);
|
|
871
|
+
await this.persist(revoked);
|
|
872
|
+
serials.push(record.serial);
|
|
873
|
+
}
|
|
874
|
+
if (serials.length > 0) this.deps.logger?.warn("Every mesh certificate of the node revoked", {
|
|
875
|
+
tags: { nodeId },
|
|
876
|
+
meta: {
|
|
877
|
+
serials,
|
|
878
|
+
reason
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
return serials;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* At boot: a node with TWO live leaves had a renewal in flight when the hub stopped. The
|
|
885
|
+
* newer (renewal) one is set aside - it is not the node's current leaf until the node is
|
|
886
|
+
* seen presenting it, which reinstates it. It is NOT put on the CRL.
|
|
887
|
+
*/
|
|
888
|
+
async settleInterruptedRenewals() {
|
|
889
|
+
let settled = 0;
|
|
890
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
891
|
+
for (const record of this.rows.values()) {
|
|
892
|
+
if (record.revokedAt !== null) continue;
|
|
893
|
+
byNode.set(record.nodeId, [...byNode.get(record.nodeId) ?? [], record]);
|
|
894
|
+
}
|
|
895
|
+
for (const records of byNode.values()) {
|
|
896
|
+
if (records.length < 2) continue;
|
|
897
|
+
for (const record of records) if (record.issuedVia === "renewal" && await this.abandon(record.fingerprint)) settled += 1;
|
|
898
|
+
}
|
|
899
|
+
return settled;
|
|
900
|
+
}
|
|
901
|
+
/** Set a never-completed renewal leaf aside (not current, NOT on the CRL). */
|
|
902
|
+
async abandon(fingerprint) {
|
|
903
|
+
const record = this.rows.get(fingerprint);
|
|
904
|
+
if (record === void 0 || record.revokedAt !== null) return false;
|
|
905
|
+
const abandoned = {
|
|
906
|
+
...record,
|
|
907
|
+
revokedAt: this.now(),
|
|
908
|
+
revokedReason: RENEWAL_ABANDONED
|
|
909
|
+
};
|
|
910
|
+
this.rows.set(fingerprint, abandoned);
|
|
911
|
+
await this.persist(abandoned);
|
|
912
|
+
this.deps.logger?.warn("Renewal leaf set aside (not current, not on the CRL)", {
|
|
913
|
+
tags: { nodeId: record.nodeId },
|
|
914
|
+
meta: { serial: record.serial }
|
|
915
|
+
});
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
/** Replay every persisted revocation into `crl` (hub boot). Set-aside leaves are not revocations. */
|
|
919
|
+
restoreCrl(crl) {
|
|
920
|
+
let restored = 0;
|
|
921
|
+
for (const record of this.rows.values()) {
|
|
922
|
+
if (record.revokedAt === null || record.revokedReason === "renewal-abandoned") continue;
|
|
923
|
+
crl.revoke(record.serial);
|
|
924
|
+
restored += 1;
|
|
925
|
+
}
|
|
926
|
+
return restored;
|
|
927
|
+
}
|
|
928
|
+
/** Is `serial` listed as revoked here? */
|
|
929
|
+
isRevoked(serial) {
|
|
930
|
+
const wanted = normalizeSerial(serial);
|
|
931
|
+
for (const record of this.rows.values()) if (record.revokedAt !== null && record.revokedReason !== "renewal-abandoned" && normalizeSerial(record.serial) === wanted) return true;
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
async reap() {
|
|
935
|
+
const cutoff = this.now() - RETENTION_MS;
|
|
936
|
+
const dead = [...this.rows.values()].filter((r) => (r.revokedAt ?? r.notAfter) < cutoff);
|
|
937
|
+
for (const r of dead) {
|
|
938
|
+
this.rows.delete(r.fingerprint);
|
|
939
|
+
await this.store.delete.mutate({
|
|
940
|
+
collection: MESH_NODE_CERTIFICATES_COLLECTION,
|
|
941
|
+
key: r.fingerprint
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
return dead.length;
|
|
945
|
+
}
|
|
946
|
+
async insert(record) {
|
|
947
|
+
this.rows.set(record.fingerprint, record);
|
|
948
|
+
await this.store.insert.mutate({
|
|
949
|
+
collection: MESH_NODE_CERTIFICATES_COLLECTION,
|
|
950
|
+
record: {
|
|
951
|
+
id: record.fingerprint,
|
|
952
|
+
data: { ...record }
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
async persist(record) {
|
|
957
|
+
try {
|
|
958
|
+
await this.store.update.mutate({
|
|
959
|
+
collection: MESH_NODE_CERTIFICATES_COLLECTION,
|
|
960
|
+
id: record.fingerprint,
|
|
961
|
+
data: { ...record }
|
|
962
|
+
});
|
|
963
|
+
} catch (error) {
|
|
964
|
+
this.deps.logger?.error("Mesh certificate write failed - the change holds until restart", {
|
|
965
|
+
tags: { nodeId: record.nodeId },
|
|
966
|
+
meta: { error: error instanceof Error ? error.message : String(error) }
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
var SLOT$1 = Symbol.for("camstack.mesh.certificate-registry");
|
|
972
|
+
function setProcessMeshCertificateRegistry(registry) {
|
|
973
|
+
if (registry === null) Reflect.deleteProperty(globalThis, SLOT$1);
|
|
974
|
+
else Reflect.set(globalThis, SLOT$1, registry);
|
|
975
|
+
}
|
|
976
|
+
function getProcessMeshCertificateRegistry() {
|
|
977
|
+
const value = Reflect.get(globalThis, SLOT$1);
|
|
978
|
+
return value !== null && typeof value === "object" && typeof Reflect.get(value, "recordIssued") === "function" ? value : null;
|
|
979
|
+
}
|
|
980
|
+
//#endregion
|
|
981
|
+
//#region src/kernel/moleculer/mesh-mtls/mesh-tls-gate.ts
|
|
982
|
+
var STATE_SLOT = Symbol.for("camstack.mesh.tls-listener-state");
|
|
983
|
+
function setProcessMeshTlsListenerState(state) {
|
|
984
|
+
if (state === null) Reflect.deleteProperty(globalThis, STATE_SLOT);
|
|
985
|
+
else Reflect.set(globalThis, STATE_SLOT, state);
|
|
986
|
+
}
|
|
987
|
+
/** `null` before the gate has run at all (no mesh CA yet). */
|
|
988
|
+
function getProcessMeshTlsListenerState() {
|
|
989
|
+
const value = Reflect.get(globalThis, STATE_SLOT);
|
|
990
|
+
if (value === null || typeof value !== "object") return null;
|
|
991
|
+
const state = Reflect.get(value, "state");
|
|
992
|
+
return state === "open" || state === "closed" ? value : null;
|
|
993
|
+
}
|
|
994
|
+
var DEFAULT_RETRY_MS = 15e3;
|
|
995
|
+
/**
|
|
996
|
+
* Nothing in `run` can reject: a load or an open that throws is reported and retried, so a
|
|
997
|
+
* failure can never become an unhandled rejection that leaves the listener closed in silence.
|
|
998
|
+
*/
|
|
999
|
+
function openMeshTlsWhenRevocationsLoaded(deps) {
|
|
1000
|
+
let attempt = 0;
|
|
1001
|
+
let opened = false;
|
|
1002
|
+
const now = deps.now ?? Date.now;
|
|
1003
|
+
let closedSince = null;
|
|
1004
|
+
const closed = (reason) => {
|
|
1005
|
+
closedSince ??= now();
|
|
1006
|
+
deps.onState?.({
|
|
1007
|
+
state: "closed",
|
|
1008
|
+
reason,
|
|
1009
|
+
since: closedSince
|
|
1010
|
+
});
|
|
1011
|
+
};
|
|
1012
|
+
const run = async () => {
|
|
1013
|
+
if (opened) return true;
|
|
1014
|
+
attempt += 1;
|
|
1015
|
+
let registry = null;
|
|
1016
|
+
try {
|
|
1017
|
+
registry = await deps.loadRegistry();
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
deps.onError?.(error);
|
|
1020
|
+
}
|
|
1021
|
+
if (registry === null) {
|
|
1022
|
+
closed("the revocation registry has not loaded - agents are refused");
|
|
1023
|
+
deps.onWaiting?.(attempt);
|
|
1024
|
+
deps.retry(() => void run(), deps.retryMs ?? DEFAULT_RETRY_MS);
|
|
1025
|
+
return false;
|
|
1026
|
+
}
|
|
1027
|
+
try {
|
|
1028
|
+
await deps.open(registry);
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
deps.onError?.(error);
|
|
1031
|
+
closed("the listener failed to open - retrying");
|
|
1032
|
+
deps.retry(() => void run(), deps.retryMs ?? DEFAULT_RETRY_MS);
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
1035
|
+
opened = true;
|
|
1036
|
+
deps.onState?.({ state: "open" });
|
|
1037
|
+
return true;
|
|
1038
|
+
};
|
|
1039
|
+
return run();
|
|
1040
|
+
}
|
|
1041
|
+
//#endregion
|
|
1042
|
+
//#region src/kernel/moleculer/mesh-mtls/mesh-renewal.ts
|
|
1043
|
+
var DEFAULT_RECONNECT_TIMEOUT_MS = 45e3;
|
|
1044
|
+
var DEFAULT_CONFIRM_RETRY_MS = 500;
|
|
1045
|
+
function defaultSetTimer(fn, ms) {
|
|
1046
|
+
const timer = setTimeout(fn, ms);
|
|
1047
|
+
timer.unref?.();
|
|
1048
|
+
return { cancel: () => clearTimeout(timer) };
|
|
1049
|
+
}
|
|
1050
|
+
function defaultSleep(ms) {
|
|
1051
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1052
|
+
}
|
|
1053
|
+
function message$1(error) {
|
|
1054
|
+
return error instanceof Error ? error.message : String(error);
|
|
1055
|
+
}
|
|
1056
|
+
async function renewMeshCertificate(deps) {
|
|
1057
|
+
const { nodeId, log } = deps;
|
|
1058
|
+
const tags = { nodeId };
|
|
1059
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
1060
|
+
const confirmRetryMs = deps.confirmRetryMs ?? DEFAULT_CONFIRM_RETRY_MS;
|
|
1061
|
+
const setTimer = deps.setTimer ?? defaultSetTimer;
|
|
1062
|
+
const timeoutMs = deps.reconnectTimeoutMs ?? DEFAULT_RECONNECT_TIMEOUT_MS;
|
|
1063
|
+
const now = deps.now ?? Date.now;
|
|
1064
|
+
const live = deps.peers.peerLeaf(nodeId);
|
|
1065
|
+
const previous = live ?? deps.registry.currentFor(nodeId);
|
|
1066
|
+
if (live !== void 0) await deps.registry.recordObserved(nodeId, {
|
|
1067
|
+
cn: nodeId,
|
|
1068
|
+
serial: live.serial,
|
|
1069
|
+
fingerprint: live.fingerprint,
|
|
1070
|
+
notBefore: live.notBefore,
|
|
1071
|
+
notAfter: live.notAfter
|
|
1072
|
+
});
|
|
1073
|
+
log.info("Mesh renewal: starting", {
|
|
1074
|
+
tags,
|
|
1075
|
+
meta: {
|
|
1076
|
+
currentSerial: previous?.serial ?? null,
|
|
1077
|
+
connected: live !== void 0
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
const fail = (stage, reason, keptPreviousLeaf) => {
|
|
1081
|
+
log.error("Mesh renewal: failed", {
|
|
1082
|
+
tags,
|
|
1083
|
+
meta: {
|
|
1084
|
+
stage,
|
|
1085
|
+
reason,
|
|
1086
|
+
keptPreviousLeaf
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
return {
|
|
1090
|
+
ok: false,
|
|
1091
|
+
stage,
|
|
1092
|
+
reason,
|
|
1093
|
+
keptPreviousLeaf
|
|
1094
|
+
};
|
|
1095
|
+
};
|
|
1096
|
+
let csrPem;
|
|
1097
|
+
try {
|
|
1098
|
+
csrPem = (await deps.client.prepareMeshRenewal()).csrPem;
|
|
1099
|
+
log.info("Mesh renewal: the node generated a new key and sent its CSR", { tags });
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
return fail("prepare", message$1(error), true);
|
|
1102
|
+
}
|
|
1103
|
+
let issued;
|
|
1104
|
+
try {
|
|
1105
|
+
issued = await issueMeshLeafFromCsr(deps.ca, csrPem, { expectedNodeId: nodeId });
|
|
1106
|
+
await deps.registry.recordIssued(nodeId, issued.certPem, "renewal");
|
|
1107
|
+
log.info("Mesh renewal: new leaf signed and recorded", {
|
|
1108
|
+
tags,
|
|
1109
|
+
meta: {
|
|
1110
|
+
serial: issued.serialNumber,
|
|
1111
|
+
notAfter: issued.notAfter.toISOString()
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
return fail("issue", message$1(error), true);
|
|
1116
|
+
}
|
|
1117
|
+
const newLeaf = describeMeshLeaf(issued.certPem);
|
|
1118
|
+
let resolveRegistered = () => void 0;
|
|
1119
|
+
const registeredPromise = new Promise((resolve) => {
|
|
1120
|
+
resolveRegistered = resolve;
|
|
1121
|
+
});
|
|
1122
|
+
const unsubscribe = deps.registrations.subscribe((id) => {
|
|
1123
|
+
if (id === nodeId && deps.peers.peerLeaf(nodeId)?.fingerprint === newLeaf.fingerprint) resolveRegistered(true);
|
|
1124
|
+
});
|
|
1125
|
+
const deadlineTimer = setTimer(() => resolveRegistered(false), timeoutMs);
|
|
1126
|
+
registeredPromise.then(() => deadlineTimer.cancel());
|
|
1127
|
+
try {
|
|
1128
|
+
await deps.client.installMeshLeaf({
|
|
1129
|
+
certPem: issued.certPem,
|
|
1130
|
+
caCertPem: deps.ca.certPem,
|
|
1131
|
+
confirmWithinMs: timeoutMs + 3e4
|
|
1132
|
+
});
|
|
1133
|
+
log.info("Mesh renewal: the node installed the new leaf and is reconnecting", { tags });
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
unsubscribe();
|
|
1136
|
+
deadlineTimer.cancel();
|
|
1137
|
+
await deps.client.rollbackMeshLeaf().catch(() => void 0);
|
|
1138
|
+
await deps.registry.abandon(newLeaf.fingerprint);
|
|
1139
|
+
return fail("install", message$1(error), true);
|
|
1140
|
+
}
|
|
1141
|
+
const deadline = now() + timeoutMs;
|
|
1142
|
+
const registered = await registeredPromise;
|
|
1143
|
+
unsubscribe();
|
|
1144
|
+
if (!registered) {
|
|
1145
|
+
const back = await deps.client.rollbackMeshLeaf().catch(() => ({ rolledBack: false }));
|
|
1146
|
+
log.warn("Mesh renewal: the node was not back and registered with the new leaf in time", {
|
|
1147
|
+
tags,
|
|
1148
|
+
meta: {
|
|
1149
|
+
timeoutMs,
|
|
1150
|
+
rolledBack: back.rolledBack
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
await deps.registry.abandon(newLeaf.fingerprint);
|
|
1154
|
+
return fail("reconnect", `the node was not back and registered with the new leaf within ${Math.round(timeoutMs / 1e3)} s; the previous leaf was kept and is still valid`, true);
|
|
1155
|
+
}
|
|
1156
|
+
log.info("Mesh renewal: the node registered presenting the new leaf", { tags });
|
|
1157
|
+
let confirmError = "the node did not confirm the new leaf";
|
|
1158
|
+
let confirmedByNode = false;
|
|
1159
|
+
for (;;) {
|
|
1160
|
+
try {
|
|
1161
|
+
if ((await deps.client.confirmMeshLeaf({ fingerprintSha256: newLeaf.fingerprint })).confirmed) {
|
|
1162
|
+
confirmedByNode = true;
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1165
|
+
confirmError = "the node did not confirm the new leaf";
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
confirmError = message$1(error);
|
|
1168
|
+
log.warn("Mesh renewal: confirm not accepted yet", {
|
|
1169
|
+
tags,
|
|
1170
|
+
meta: { error: confirmError }
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
if (now() >= deadline) break;
|
|
1174
|
+
await sleep(confirmRetryMs);
|
|
1175
|
+
}
|
|
1176
|
+
if (!confirmedByNode) return fail("confirm", confirmError, false);
|
|
1177
|
+
if (previous !== void 0 && previous.serial !== newLeaf.serial) {
|
|
1178
|
+
await deps.registry.revoke(previous.fingerprint, "replaced-by-renewal", deps.crl);
|
|
1179
|
+
deps.crl.revoke(previous.serial);
|
|
1180
|
+
log.info("Mesh renewal: the previous leaf is revoked", {
|
|
1181
|
+
tags,
|
|
1182
|
+
meta: { oldSerial: previous.serial }
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
log.info("Mesh renewal: done", {
|
|
1186
|
+
tags,
|
|
1187
|
+
meta: {
|
|
1188
|
+
newSerial: newLeaf.serial,
|
|
1189
|
+
notAfter: new Date(newLeaf.notAfter).toISOString()
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
return {
|
|
1193
|
+
ok: true,
|
|
1194
|
+
oldSerial: previous?.serial ?? null,
|
|
1195
|
+
newSerial: newLeaf.serial,
|
|
1196
|
+
newNotAfter: newLeaf.notAfter
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Revoke every live mesh leaf of `nodeId` and cut its connection. The node cannot
|
|
1201
|
+
* reconnect: it must re-join with a new join token. The node is NOT forgotten
|
|
1202
|
+
* (that stays `clusterNodes.forgetNode`).
|
|
1203
|
+
*/
|
|
1204
|
+
async function revokeMeshCertificate(deps) {
|
|
1205
|
+
const { nodeId, log } = deps;
|
|
1206
|
+
const tags = { nodeId };
|
|
1207
|
+
const live = deps.peers.peerLeaf(nodeId);
|
|
1208
|
+
if (live !== void 0) await deps.registry.recordObserved(nodeId, {
|
|
1209
|
+
cn: nodeId,
|
|
1210
|
+
serial: live.serial,
|
|
1211
|
+
fingerprint: live.fingerprint,
|
|
1212
|
+
notBefore: live.notBefore,
|
|
1213
|
+
notAfter: live.notAfter
|
|
1214
|
+
});
|
|
1215
|
+
const serials = [...await deps.registry.revokeAllForNode(nodeId, "revoked-by-operator", deps.crl)];
|
|
1216
|
+
const disconnected = deps.peers.dropPeer(nodeId);
|
|
1217
|
+
log.warn("Mesh certificate revoked and the node disconnected", {
|
|
1218
|
+
tags,
|
|
1219
|
+
meta: {
|
|
1220
|
+
serials,
|
|
1221
|
+
disconnected
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
return {
|
|
1225
|
+
nodeId,
|
|
1226
|
+
revokedSerials: serials,
|
|
1227
|
+
disconnected,
|
|
1228
|
+
next: "The node can no longer connect. Re-join it with a new join token (System -> Cluster -> Add agent); to remove it from the cluster use Forget node."
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
//#endregion
|
|
602
1232
|
//#region src/kernel/moleculer/mesh-mtls/mesh-peer-transport.ts
|
|
603
1233
|
var LOOKUP_SLOT = Symbol.for("camstack.cluster.mesh-peer-transport-lookup");
|
|
604
1234
|
/** Published by the hub broker owner; cleared on shutdown. */
|
|
@@ -617,6 +1247,36 @@ function getProcessMeshPeerTransportLookup() {
|
|
|
617
1247
|
function getProcessNodeMeshTransport(nodeId) {
|
|
618
1248
|
return getProcessMeshPeerTransportLookup()?.transportOf(nodeId);
|
|
619
1249
|
}
|
|
1250
|
+
/** The leaf `nodeId` presented on its live connection, or `undefined`. */
|
|
1251
|
+
function getProcessPeerLeaf(nodeId) {
|
|
1252
|
+
return getProcessMeshPeerTransportLookup()?.peerLeaf?.(nodeId);
|
|
1253
|
+
}
|
|
1254
|
+
/** Disconnect `nodeId` (closes its mutual-TLS sockets). `0` when none were open. */
|
|
1255
|
+
function dropProcessPeer(nodeId) {
|
|
1256
|
+
return getProcessMeshPeerTransportLookup()?.dropPeer?.(nodeId) ?? 0;
|
|
1257
|
+
}
|
|
1258
|
+
var REGISTRATION_SLOT = Symbol.for("camstack.cluster.node-registration-listeners");
|
|
1259
|
+
function registrationListeners() {
|
|
1260
|
+
const existing = Reflect.get(globalThis, REGISTRATION_SLOT);
|
|
1261
|
+
if (existing instanceof Set) return existing;
|
|
1262
|
+
const created = /* @__PURE__ */ new Set();
|
|
1263
|
+
Reflect.set(globalThis, REGISTRATION_SLOT, created);
|
|
1264
|
+
return created;
|
|
1265
|
+
}
|
|
1266
|
+
/** Subscribe to "node X finished registering". Returns the unsubscribe. */
|
|
1267
|
+
function onProcessNodeRegistered(listener) {
|
|
1268
|
+
const listeners = registrationListeners();
|
|
1269
|
+
listeners.add(listener);
|
|
1270
|
+
return () => {
|
|
1271
|
+
listeners.delete(listener);
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
/** Called by the hub when a node's `registerNode` has been applied. */
|
|
1275
|
+
function emitProcessNodeRegistered(nodeId) {
|
|
1276
|
+
for (const listener of [...registrationListeners()]) try {
|
|
1277
|
+
listener(nodeId);
|
|
1278
|
+
} catch {}
|
|
1279
|
+
}
|
|
620
1280
|
//#endregion
|
|
621
1281
|
//#region ../../node_modules/jose/dist/webapi/lib/buffer_utils.js
|
|
622
1282
|
var encoder = new TextEncoder(), decoder = new TextDecoder(), strictDecoder = new TextDecoder("utf-8", { fatal: !0 });
|
|
@@ -1466,6 +2126,11 @@ async function enrollMeshNode(body, deps) {
|
|
|
1466
2126
|
status: 400
|
|
1467
2127
|
};
|
|
1468
2128
|
}
|
|
2129
|
+
try {
|
|
2130
|
+
await deps.onIssued?.(leaf.commonName, leaf.certPem);
|
|
2131
|
+
} catch (err) {
|
|
2132
|
+
deps.onRecordFailed?.(leaf.commonName, err);
|
|
2133
|
+
}
|
|
1469
2134
|
return {
|
|
1470
2135
|
ok: true,
|
|
1471
2136
|
cert: leaf.certPem,
|
|
@@ -86783,6 +87448,23 @@ function peekSender(transporter, type, message) {
|
|
|
86783
87448
|
return null;
|
|
86784
87449
|
}
|
|
86785
87450
|
}
|
|
87451
|
+
function peerLeafOf(socket) {
|
|
87452
|
+
const cert = socket.getPeerCertificate(true);
|
|
87453
|
+
if (cert == null || typeof cert !== "object" || !("raw" in cert) || !Buffer.isBuffer(cert.raw)) return null;
|
|
87454
|
+
try {
|
|
87455
|
+
const x509 = new X509Certificate(cert.raw);
|
|
87456
|
+
const remote = socket.remoteAddress;
|
|
87457
|
+
return {
|
|
87458
|
+
...remote !== void 0 ? { remoteAddress: remote.replace(/^::ffff:/, "") } : {},
|
|
87459
|
+
serial: x509.serialNumber,
|
|
87460
|
+
fingerprint: x509.fingerprint256,
|
|
87461
|
+
notBefore: Date.parse(x509.validFrom),
|
|
87462
|
+
notAfter: Date.parse(x509.validTo)
|
|
87463
|
+
};
|
|
87464
|
+
} catch {
|
|
87465
|
+
return null;
|
|
87466
|
+
}
|
|
87467
|
+
}
|
|
86786
87468
|
function peerIdentity(socket) {
|
|
86787
87469
|
const cert = socket.getPeerCertificate(true);
|
|
86788
87470
|
if (cert == null || typeof cert !== "object" || !("raw" in cert) || !cert.raw) return null;
|
|
@@ -86810,6 +87492,8 @@ var TlsTcpTransporter = class extends TcpTransporterBase {
|
|
|
86810
87492
|
tlsSockets = /* @__PURE__ */ new Set();
|
|
86811
87493
|
/** Node id (certificate CN) → serial of every peer that completed a mutual-TLS handshake. */
|
|
86812
87494
|
tlsPeers = /* @__PURE__ */ new Map();
|
|
87495
|
+
/** Node id → the leaf it presented at its last handshake (hub inventory, D856). */
|
|
87496
|
+
peerLeaves = /* @__PURE__ */ new Map();
|
|
86813
87497
|
clearDisabled = false;
|
|
86814
87498
|
hubTlsOnly;
|
|
86815
87499
|
hubOnlyPeers;
|
|
@@ -86923,6 +87607,7 @@ var TlsTcpTransporter = class extends TcpTransporterBase {
|
|
|
86923
87607
|
tlsSock.meshPeerCn = identity.cn;
|
|
86924
87608
|
tlsSock.meshPeerSerial = identity.serial;
|
|
86925
87609
|
self.tlsPeers.set(identity.cn, identity.serial);
|
|
87610
|
+
self.rememberPeerLeaf(identity.cn, tlsSock);
|
|
86926
87611
|
self.tlsSockets.add(tlsSock);
|
|
86927
87612
|
tlsSock.on("close", () => {
|
|
86928
87613
|
self.tlsSockets.delete(tlsSock);
|
|
@@ -86975,6 +87660,25 @@ var TlsTcpTransporter = class extends TcpTransporterBase {
|
|
|
86975
87660
|
forgetPeer(nodeId) {
|
|
86976
87661
|
for (const socket of this.tlsSockets) if (socket.meshPeerCn === nodeId && !socket.destroyed) return;
|
|
86977
87662
|
this.tlsPeers.delete(nodeId);
|
|
87663
|
+
this.peerLeaves.delete(nodeId);
|
|
87664
|
+
}
|
|
87665
|
+
rememberPeerLeaf(nodeId, socket) {
|
|
87666
|
+
const leaf = peerLeafOf(socket);
|
|
87667
|
+
if (leaf !== null) this.peerLeaves.set(nodeId, leaf);
|
|
87668
|
+
}
|
|
87669
|
+
/** The leaf `nodeId` presented at its last mutual-TLS handshake. */
|
|
87670
|
+
peerLeaf(nodeId) {
|
|
87671
|
+
return this.peerLeaves.get(nodeId);
|
|
87672
|
+
}
|
|
87673
|
+
/** Close every live mutual-TLS socket of `nodeId` (revocation, renewal). Returns how many. */
|
|
87674
|
+
dropPeer(nodeId) {
|
|
87675
|
+
let closed = 0;
|
|
87676
|
+
for (const socket of this.tlsSockets) {
|
|
87677
|
+
if (socket.meshPeerCn !== nodeId || socket.destroyed) continue;
|
|
87678
|
+
socket.destroy();
|
|
87679
|
+
closed += 1;
|
|
87680
|
+
}
|
|
87681
|
+
return closed;
|
|
86978
87682
|
}
|
|
86979
87683
|
/** True once the mutual-TLS listener is bound. */
|
|
86980
87684
|
isTlsListening() {
|
|
@@ -87029,6 +87733,7 @@ var TlsTcpTransporter = class extends TcpTransporterBase {
|
|
|
87029
87733
|
} catch {}
|
|
87030
87734
|
this.tlsSockets.clear();
|
|
87031
87735
|
this.tlsPeers.clear();
|
|
87736
|
+
this.peerLeaves.clear();
|
|
87032
87737
|
return super.disconnect();
|
|
87033
87738
|
}
|
|
87034
87739
|
/**
|
|
@@ -87157,6 +87862,7 @@ var TlsTcpTransporter = class extends TcpTransporterBase {
|
|
|
87157
87862
|
socket.meshPeerSerial = identity.serial;
|
|
87158
87863
|
this.tlsSockets.add(socket);
|
|
87159
87864
|
this.tlsPeers.set(identity.cn, identity.serial);
|
|
87865
|
+
this.rememberPeerLeaf(identity.cn, socket);
|
|
87160
87866
|
const reader = this.reader;
|
|
87161
87867
|
socket.on("close", () => {
|
|
87162
87868
|
this.tlsSockets.delete(socket);
|
|
@@ -87377,6 +88083,8 @@ var systemConfigMeshActions = defineCustomActions({
|
|
|
87377
88083
|
});
|
|
87378
88084
|
//#endregion
|
|
87379
88085
|
//#region src/builtins/system-config/system-config.addon.ts
|
|
88086
|
+
/** How often superseded and expired mesh certificate rows are reaped. */
|
|
88087
|
+
var MESH_REAP_MS = 24 * 36e5;
|
|
87380
88088
|
/**
|
|
87381
88089
|
* Built-in `system-config` addon — Phase 4 of the settings redesign.
|
|
87382
88090
|
*
|
|
@@ -87454,6 +88162,10 @@ var SystemConfigAddon = class extends BaseAddon {
|
|
|
87454
88162
|
id = "system-config";
|
|
87455
88163
|
clusterSecretKeeper = null;
|
|
87456
88164
|
meshCaKeeper = null;
|
|
88165
|
+
meshReapTimer = null;
|
|
88166
|
+
meshGateTimers = /* @__PURE__ */ new Set();
|
|
88167
|
+
stoppedMesh = false;
|
|
88168
|
+
loggedNoSettingsStore = false;
|
|
87457
88169
|
constructor() {
|
|
87458
88170
|
super({});
|
|
87459
88171
|
}
|
|
@@ -87493,19 +88205,107 @@ var SystemConfigAddon = class extends BaseAddon {
|
|
|
87493
88205
|
}
|
|
87494
88206
|
async onShutdown() {
|
|
87495
88207
|
setProcessMeshEnforcer(null);
|
|
88208
|
+
setProcessMeshCertificateRegistry(null);
|
|
88209
|
+
setProcessMeshTlsListenerState(null);
|
|
88210
|
+
setProcessAgentHttpsNames(null);
|
|
88211
|
+
this.stoppedMesh = true;
|
|
88212
|
+
if (this.meshReapTimer !== null) clearInterval(this.meshReapTimer);
|
|
88213
|
+
this.meshReapTimer = null;
|
|
88214
|
+
for (const timer of this.meshGateTimers) clearTimeout(timer);
|
|
88215
|
+
this.meshGateTimers.clear();
|
|
87496
88216
|
this.meshCaKeeper?.stop();
|
|
87497
88217
|
this.meshCaKeeper = null;
|
|
87498
88218
|
this.clusterSecretKeeper?.stop();
|
|
87499
88219
|
this.clusterSecretKeeper = null;
|
|
87500
88220
|
}
|
|
88221
|
+
/**
|
|
88222
|
+
* The hub's record of mesh leaves (D856): declared and loaded BEFORE the TLS
|
|
88223
|
+
* listener opens, so persisted revocations are in the CRL before any node can
|
|
88224
|
+
* connect. Best effort: a hub whose settings store is not up still serves the
|
|
88225
|
+
* mesh (the registry then fills from the leaves nodes present).
|
|
88226
|
+
*/
|
|
88227
|
+
async initMeshCertificateRegistry() {
|
|
88228
|
+
const store = this.ctx.api?.settingsStore;
|
|
88229
|
+
if (!store) {
|
|
88230
|
+
if (!this.loggedNoSettingsStore) {
|
|
88231
|
+
this.loggedNoSettingsStore = true;
|
|
88232
|
+
this.ctx.logger.error("No settings store in this process: the revocation registry cannot load, so the mesh TLS listener stays CLOSED and every agent is refused (shown on Identity -> Certificates)");
|
|
88233
|
+
}
|
|
88234
|
+
return null;
|
|
88235
|
+
}
|
|
88236
|
+
this.loadAgentHttpsNames(store);
|
|
88237
|
+
try {
|
|
88238
|
+
await store.declareCollection.mutate({
|
|
88239
|
+
collection: MESH_NODE_CERTIFICATES_COLLECTION,
|
|
88240
|
+
columns: MESH_NODE_CERTIFICATES_COLUMNS,
|
|
88241
|
+
indexes: [{
|
|
88242
|
+
name: "idx_mesh_node_certificates_node",
|
|
88243
|
+
columns: ["nodeId"]
|
|
88244
|
+
}]
|
|
88245
|
+
});
|
|
88246
|
+
const registry = new MeshCertificateRegistry(store, { logger: this.ctx.logger });
|
|
88247
|
+
await registry.load();
|
|
88248
|
+
const restored = registry.restoreCrl(processMeshCrl());
|
|
88249
|
+
const settled = await registry.settleInterruptedRenewals();
|
|
88250
|
+
if (settled > 0) this.ctx.logger.warn("A mesh renewal was interrupted by a hub restart - its new leaf was set aside", { meta: { settled } });
|
|
88251
|
+
setProcessMeshCertificateRegistry(registry);
|
|
88252
|
+
if (restored > 0) this.ctx.logger.info("Mesh revocations restored into the CRL", { meta: { restored } });
|
|
88253
|
+
return registry;
|
|
88254
|
+
} catch (err) {
|
|
88255
|
+
this.ctx.logger.error("Mesh certificate registry unavailable", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
88256
|
+
return null;
|
|
88257
|
+
}
|
|
88258
|
+
}
|
|
88259
|
+
/** Which names the hub certified to which agent. Best effort; nothing waits on it. */
|
|
88260
|
+
async loadAgentHttpsNames(store) {
|
|
88261
|
+
try {
|
|
88262
|
+
await store.declareCollection.mutate({
|
|
88263
|
+
collection: AGENT_HTTPS_NAMES_COLLECTION,
|
|
88264
|
+
columns: AGENT_HTTPS_NAMES_COLUMNS
|
|
88265
|
+
});
|
|
88266
|
+
const names = new AgentHttpsNames(store);
|
|
88267
|
+
await names.load();
|
|
88268
|
+
setProcessAgentHttpsNames(names);
|
|
88269
|
+
} catch (err) {
|
|
88270
|
+
this.ctx.logger.warn("Agent HTTPS names registry unavailable (agent HTTPS issuance will be refused until it loads)", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
88271
|
+
}
|
|
88272
|
+
}
|
|
88273
|
+
/**
|
|
88274
|
+
* The mesh TLS listener opens ONLY after the revocation registry has loaded and its
|
|
88275
|
+
* revocations are in the CRL (D856): until then every agent is refused, because an
|
|
88276
|
+
* empty CRL would accept a revoked leaf. Retries until the settings store answers.
|
|
88277
|
+
*/
|
|
87501
88278
|
async onMeshCaReady(ca) {
|
|
87502
|
-
|
|
87503
|
-
|
|
87504
|
-
|
|
87505
|
-
|
|
87506
|
-
|
|
88279
|
+
await openMeshTlsWhenRevocationsLoaded({
|
|
88280
|
+
loadRegistry: () => this.initMeshCertificateRegistry(),
|
|
88281
|
+
open: async (registry) => {
|
|
88282
|
+
const hubLeaf = await issueMeshLeafForNode(ca, "hub");
|
|
88283
|
+
await registry.recordIssued("hub", hubLeaf.certPem, "boot").catch((err) => {
|
|
88284
|
+
this.ctx.logger.warn("Hub mesh leaf could not be recorded", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
88285
|
+
});
|
|
88286
|
+
await registry.reap().catch(() => void 0);
|
|
88287
|
+
if (this.meshReapTimer !== null) clearInterval(this.meshReapTimer);
|
|
88288
|
+
this.meshReapTimer = setInterval(() => void registry.reap().catch(() => void 0), MESH_REAP_MS);
|
|
88289
|
+
this.meshReapTimer.unref?.();
|
|
88290
|
+
await requestProcessMeshTlsEnable({
|
|
88291
|
+
caCertPem: ca.certPem,
|
|
88292
|
+
certPem: hubLeaf.certPem,
|
|
88293
|
+
keyPem: hubLeaf.keyPem
|
|
88294
|
+
});
|
|
88295
|
+
this.ctx.logger.info("Mesh CA ready — revocations loaded, TLS material offered to the transporter");
|
|
88296
|
+
},
|
|
88297
|
+
retry: (fn, ms) => {
|
|
88298
|
+
if (this.stoppedMesh) return;
|
|
88299
|
+
const timer = setTimeout(fn, ms);
|
|
88300
|
+
timer.unref?.();
|
|
88301
|
+
this.meshGateTimers.add(timer);
|
|
88302
|
+
},
|
|
88303
|
+
onState: (state) => setProcessMeshTlsListenerState(state),
|
|
88304
|
+
onWaiting: (attempt) => {
|
|
88305
|
+
if (attempt === 1 || attempt % 20 === 0) this.ctx.logger.error("Mesh TLS stays CLOSED: the revocation registry has not loaded, so agents are refused rather than accepted with an empty CRL", { meta: { attempt } });
|
|
88306
|
+
},
|
|
88307
|
+
onError: (err) => this.ctx.logger.error("Mesh certificate registry failed to load", { meta: { error: err instanceof Error ? err.message : String(err) } })
|
|
87507
88308
|
});
|
|
87508
|
-
this.ctx.logger.info("Mesh CA ready — TLS material offered to the transporter");
|
|
87509
88309
|
}
|
|
87510
88310
|
async mintJoinToken(input) {
|
|
87511
88311
|
const ca = requireProcessMeshCa();
|
|
@@ -87847,4 +88647,4 @@ function readJwtExp(token) {
|
|
|
87847
88647
|
return typeof exp === "number" ? exp : 0;
|
|
87848
88648
|
}
|
|
87849
88649
|
//#endregion
|
|
87850
|
-
export {
|
|
88650
|
+
export { MESH_AUTH_MODE_FIELD as $, MeshCrl as A, generateMeshKeyPair as B, MESH_NODE_CERTIFICATES_COLLECTION as C, describeMeshLeaf as D, RENEWAL_ABANDONED as E, caFingerprint as F, AGENT_UI_UNLOCK_DOMAIN as G, issueMeshLeafFromCsr as H, caFingerprintCompact as I, UnlockTicketJtiStore as J, AGENT_UI_UNLOCK_TTL_MS as K, createMeshCsrPem as L, MeshCaKeeper as M, getProcessMeshCaState as N, getProcessMeshCertificateRegistry as O, requireProcessMeshCa as P, verifyAgentUiUnlockTicket as Q, exportMeshPrivateKeyPem as R, setProcessMeshTlsListenerState as S, SYSTEM_CONFIG_CLUSTER_SECTION_ID, SystemConfigAddon, SystemConfigAddon as default, MeshCertificateRegistry as T, loadMeshCa as U, issueMeshLeafForNode as V, AGENT_DASHBOARD_ORIGINS_FIELD as W, mintAgentUiUnlockTicket as X, isAllowedAgentDashboardReturnTo as Y, parseAgentDashboardOrigins as Z, setProcessMeshPeerTransportLookup as _, ServiceBroker as a, setProcessMeshAuthMode as at, getProcessMeshTlsListenerState as b, JoinTokenJtiStore as c, createCidrMatcher as ct, verifyMeshJoinToken as d, readCidrSetting as dt, getProcessMeshAuthMode as et, dropProcessPeer as f, onProcessNodeRegistered as g, getProcessPeerLeaf as h, Errors as i, requestProcessMeshEnforcement as it, processMeshCrl as j, setProcessMeshCertificateRegistry as k, mintMeshJoinToken as l, normalizeIp as lt, getProcessNodeMeshTransport as m, MESH_TLS_PORT_HELLO_FIELD as n, isMeshAuthMode as nt, setProcessMeshTlsEnabler as o, setProcessMeshAuthStatusSource as ot, emitProcessNodeRegistered as p, AGENT_UI_UNLOCK_VERSION as q, TlsTcpTransporter as r, parseMeshAuthMode as rt, enrollMeshNode as s, setProcessMeshEnforcer as st, systemConfigMeshActions as t, getProcessMeshAuthStatus as tt, processJoinTokenJtiStore as u, parseCidrList as ut, renewMeshCertificate as v, MESH_NODE_CERTIFICATES_COLUMNS as w, openMeshTlsWhenRevocationsLoaded as x, revokeMeshCertificate as y, generateMeshCa as z };
|