@camstack/system 1.2.428 → 1.2.429
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/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 +3 -3
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +3 -3
- 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-DvCGZEUI.js → composition-sources-C2VNX6tF.js} +1 -1
- package/dist/{composition-sources-BQNxXQZG.mjs → composition-sources-SYtYuAgh.mjs} +1 -1
- package/dist/{constants-CR4If_lP.mjs → constants-CKuWoWvc.mjs} +1 -1
- package/dist/{constants-CCSJGnjs.js → constants-RI-hGP5S.js} +1 -1
- package/dist/device-ca-BWfdDB3k.js +462 -0
- package/dist/device-ca-R7DKKZL6.mjs +355 -0
- package/dist/{dist-BQG3QFg7.js → dist-Bgy_AmwX.js} +554 -0
- package/dist/{dist-DHSJV4Qc.mjs → dist-CSSfTM18.mjs} +507 -1
- 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-DH1Pxq_1.mjs → restore-archive-AIwMHO5_.mjs} +1 -1
- package/dist/{restore-archive-Dccm8lkA.js → restore-archive-BYUEMsog.js} +1 -1
- package/dist/{retired-settings-keys-BfGaSeQI.js → retired-settings-keys-BTPkRLTS.js} +1 -1
- package/dist/{retired-settings-keys-CPz7zpza.mjs → retired-settings-keys-H0rYXT27.mjs} +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-BSGsxKW7.js} +0 -0
- package/dist/{wal-maintenance-Drqhcjit.mjs → wal-maintenance-DW0FfGV7.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
|
@@ -3,10 +3,11 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-Bgy_AmwX.js");
|
|
7
7
|
const require_safe_buffer$1 = require("../../safe-buffer-D3aoa-7X.js");
|
|
8
|
-
const require_constants$8 = require("../../constants-CCSJGnjs.js");
|
|
9
8
|
const require_x509_es = require("../../x509.es-Dzl0BbmG.js");
|
|
9
|
+
const require_agent_https_names = require("../../agent-https-names-CpUSuP4w.js");
|
|
10
|
+
const require_constants$8 = require("../../constants-RI-hGP5S.js");
|
|
10
11
|
const require_src$1 = require("../../src-CnAqG6EF.js");
|
|
11
12
|
let zod = require("zod");
|
|
12
13
|
let node_crypto = require("node:crypto");
|
|
@@ -425,7 +426,7 @@ async function exportMeshPrivateKeyPem(key) {
|
|
|
425
426
|
}
|
|
426
427
|
//#endregion
|
|
427
428
|
//#region src/kernel/moleculer/mesh-mtls/mesh-ca-keeper.ts
|
|
428
|
-
var SLOT$
|
|
429
|
+
var SLOT$3 = Symbol.for("camstack.mesh.ca");
|
|
429
430
|
var NOT_LOADED = {
|
|
430
431
|
state: "unavailable",
|
|
431
432
|
reason: "not-loaded"
|
|
@@ -437,10 +438,10 @@ function isState(value) {
|
|
|
437
438
|
return state === "unavailable" && typeof Reflect.get(value, "reason") === "string";
|
|
438
439
|
}
|
|
439
440
|
function setProcessMeshCa(state) {
|
|
440
|
-
Reflect.set(globalThis, SLOT$
|
|
441
|
+
Reflect.set(globalThis, SLOT$3, state);
|
|
441
442
|
}
|
|
442
443
|
function getProcessMeshCaState() {
|
|
443
|
-
const value = Reflect.get(globalThis, SLOT$
|
|
444
|
+
const value = Reflect.get(globalThis, SLOT$3);
|
|
444
445
|
return isState(value) ? value : NOT_LOADED;
|
|
445
446
|
}
|
|
446
447
|
function requireProcessMeshCa() {
|
|
@@ -582,10 +583,10 @@ async function loadOrGenerateMeshCa(deps) {
|
|
|
582
583
|
var MeshCrl = class {
|
|
583
584
|
serials = /* @__PURE__ */ new Set();
|
|
584
585
|
revoke(serialNumber) {
|
|
585
|
-
this.serials.add(normalizeSerial(serialNumber));
|
|
586
|
+
this.serials.add(normalizeSerial$1(serialNumber));
|
|
586
587
|
}
|
|
587
588
|
isRevoked(serialNumber) {
|
|
588
|
-
return this.serials.has(normalizeSerial(serialNumber));
|
|
589
|
+
return this.serials.has(normalizeSerial$1(serialNumber));
|
|
589
590
|
}
|
|
590
591
|
list() {
|
|
591
592
|
return [...this.serials];
|
|
@@ -593,21 +594,650 @@ var MeshCrl = class {
|
|
|
593
594
|
/** Restore from persisted serials (system-settings). */
|
|
594
595
|
replaceAll(serials) {
|
|
595
596
|
this.serials.clear();
|
|
596
|
-
for (const s of serials) this.serials.add(normalizeSerial(s));
|
|
597
|
+
for (const s of serials) this.serials.add(normalizeSerial$1(s));
|
|
597
598
|
}
|
|
598
599
|
};
|
|
599
|
-
function normalizeSerial(serial) {
|
|
600
|
+
function normalizeSerial$1(serial) {
|
|
600
601
|
return serial.replace(/^0x/i, "").replace(/:/g, "").toLowerCase();
|
|
601
602
|
}
|
|
602
|
-
var SLOT$
|
|
603
|
+
var SLOT$2 = Symbol.for("camstack.mesh.crl");
|
|
603
604
|
function processMeshCrl() {
|
|
604
|
-
const existing = Reflect.get(globalThis, SLOT$
|
|
605
|
+
const existing = Reflect.get(globalThis, SLOT$2);
|
|
605
606
|
if (existing instanceof MeshCrl) return existing;
|
|
606
607
|
const crl = new MeshCrl();
|
|
607
|
-
Reflect.set(globalThis, SLOT$
|
|
608
|
+
Reflect.set(globalThis, SLOT$2, crl);
|
|
608
609
|
return crl;
|
|
609
610
|
}
|
|
610
611
|
//#endregion
|
|
612
|
+
//#region src/kernel/moleculer/mesh-mtls/mesh-certificate-registry.ts
|
|
613
|
+
/**
|
|
614
|
+
* The hub's record of every mesh leaf (D856): which certificate each node holds,
|
|
615
|
+
* when it expires, and which serials are revoked.
|
|
616
|
+
*
|
|
617
|
+
* Three ways a row appears:
|
|
618
|
+
* - `enroll` / `renewal` / `boot` - the hub issued it and wrote it down at the
|
|
619
|
+
* moment of issuing (the authoritative record);
|
|
620
|
+
* - `observed` - a node enrolled before this record existed (or by a path that
|
|
621
|
+
* did not write it) presented this leaf on its LIVE mutual-TLS connection.
|
|
622
|
+
*
|
|
623
|
+
* Revoked serials are persisted here and replayed into the in-memory CRL at boot,
|
|
624
|
+
* so a revocation survives a hub restart. Hub only: agents persist none of it.
|
|
625
|
+
*/
|
|
626
|
+
/**
|
|
627
|
+
* @durable class=config owner=system-config
|
|
628
|
+
* write="the hub records every mesh leaf it issues (enroll, renewal, boot) and every
|
|
629
|
+
* leaf a connected node is observed presenting; a revocation stamps revokedAt"
|
|
630
|
+
* retention="a revoked or expired row is kept 90 days past its end, then reaped; a
|
|
631
|
+
* live row is never deleted. Forgetting a node does not delete its rows."
|
|
632
|
+
*/
|
|
633
|
+
var MESH_NODE_CERTIFICATES_COLLECTION = "mesh_node_certificates";
|
|
634
|
+
var MESH_NODE_CERTIFICATES_COLUMNS = [
|
|
635
|
+
{
|
|
636
|
+
name: "id",
|
|
637
|
+
type: "TEXT",
|
|
638
|
+
primaryKey: true,
|
|
639
|
+
notNull: true
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
name: "nodeId",
|
|
643
|
+
type: "TEXT",
|
|
644
|
+
notNull: true
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: "serial",
|
|
648
|
+
type: "TEXT",
|
|
649
|
+
notNull: true
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
name: "fingerprint",
|
|
653
|
+
type: "TEXT",
|
|
654
|
+
notNull: true
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
name: "notBefore",
|
|
658
|
+
type: "INTEGER",
|
|
659
|
+
notNull: true
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
name: "notAfter",
|
|
663
|
+
type: "INTEGER",
|
|
664
|
+
notNull: true
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
name: "issuedAt",
|
|
668
|
+
type: "INTEGER",
|
|
669
|
+
notNull: true
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
name: "issuedVia",
|
|
673
|
+
type: "TEXT",
|
|
674
|
+
notNull: true
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
name: "revokedAt",
|
|
678
|
+
type: "INTEGER"
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
name: "revokedReason",
|
|
682
|
+
type: "TEXT"
|
|
683
|
+
}
|
|
684
|
+
];
|
|
685
|
+
var RETENTION_MS = 90 * 864e5;
|
|
686
|
+
/**
|
|
687
|
+
* A leaf the hub signed for a renewal that did not complete. It must not be the node's
|
|
688
|
+
* `current` leaf, but it is NOT on the CRL: the node may still hold it (the swap
|
|
689
|
+
* could have landed without the hub seeing it), and cutting it off would be the
|
|
690
|
+
* outage the hand-over exists to prevent. If the node is later seen presenting it, it
|
|
691
|
+
* is reinstated.
|
|
692
|
+
*/
|
|
693
|
+
var RENEWAL_ABANDONED = "renewal-abandoned";
|
|
694
|
+
var VIAS = [
|
|
695
|
+
"enroll",
|
|
696
|
+
"renewal",
|
|
697
|
+
"boot",
|
|
698
|
+
"observed"
|
|
699
|
+
];
|
|
700
|
+
/** CN, serial, fingerprint and validity of a leaf PEM (or DER). */
|
|
701
|
+
function describeMeshLeaf(cert) {
|
|
702
|
+
const x509 = new node_crypto.X509Certificate(cert);
|
|
703
|
+
return {
|
|
704
|
+
cn: subjectCn(x509.subject) ?? "",
|
|
705
|
+
serial: x509.serialNumber,
|
|
706
|
+
fingerprint: x509.fingerprint256,
|
|
707
|
+
notBefore: Date.parse(x509.validFrom),
|
|
708
|
+
notAfter: Date.parse(x509.validTo)
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
function normalizeSerial(serial) {
|
|
712
|
+
return serial.replace(/^0x/i, "").replace(/:/g, "").toLowerCase();
|
|
713
|
+
}
|
|
714
|
+
function parse$1(data) {
|
|
715
|
+
const str = (k) => {
|
|
716
|
+
const v = data[k];
|
|
717
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
718
|
+
};
|
|
719
|
+
const num = (k) => {
|
|
720
|
+
const v = data[k];
|
|
721
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
722
|
+
};
|
|
723
|
+
const nodeId = str("nodeId");
|
|
724
|
+
const serial = str("serial");
|
|
725
|
+
const fingerprint = str("fingerprint");
|
|
726
|
+
const notBefore = num("notBefore");
|
|
727
|
+
const notAfter = num("notAfter");
|
|
728
|
+
const issuedAt = num("issuedAt");
|
|
729
|
+
if (nodeId === null || serial === null || fingerprint === null || notBefore === null || notAfter === null || issuedAt === null) return null;
|
|
730
|
+
return {
|
|
731
|
+
nodeId,
|
|
732
|
+
serial,
|
|
733
|
+
fingerprint,
|
|
734
|
+
notBefore,
|
|
735
|
+
notAfter,
|
|
736
|
+
issuedAt,
|
|
737
|
+
issuedVia: VIAS.find((v) => v === data["issuedVia"]) ?? "observed",
|
|
738
|
+
revokedAt: num("revokedAt"),
|
|
739
|
+
revokedReason: str("revokedReason")
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
var MeshCertificateRegistry = class {
|
|
743
|
+
store;
|
|
744
|
+
deps;
|
|
745
|
+
rows = /* @__PURE__ */ new Map();
|
|
746
|
+
constructor(store, deps = {}) {
|
|
747
|
+
this.store = store;
|
|
748
|
+
this.deps = deps;
|
|
749
|
+
}
|
|
750
|
+
now() {
|
|
751
|
+
return (this.deps.now ?? Date.now)();
|
|
752
|
+
}
|
|
753
|
+
async load() {
|
|
754
|
+
const rows = await this.store.query.query({ collection: MESH_NODE_CERTIFICATES_COLLECTION });
|
|
755
|
+
for (const row of rows) {
|
|
756
|
+
const record = parse$1(row.data);
|
|
757
|
+
if (record !== null) this.rows.set(record.fingerprint, record);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
/** Write down a leaf the hub just issued. Idempotent per fingerprint. */
|
|
761
|
+
async recordIssued(nodeId, certPem, via) {
|
|
762
|
+
const leaf = describeMeshLeaf(certPem);
|
|
763
|
+
const existing = this.rows.get(leaf.fingerprint);
|
|
764
|
+
if (existing !== void 0) return existing;
|
|
765
|
+
const record = {
|
|
766
|
+
nodeId,
|
|
767
|
+
serial: leaf.serial,
|
|
768
|
+
fingerprint: leaf.fingerprint,
|
|
769
|
+
notBefore: leaf.notBefore,
|
|
770
|
+
notAfter: leaf.notAfter,
|
|
771
|
+
issuedAt: this.now(),
|
|
772
|
+
issuedVia: via,
|
|
773
|
+
revokedAt: null,
|
|
774
|
+
revokedReason: null
|
|
775
|
+
};
|
|
776
|
+
await this.insert(record);
|
|
777
|
+
this.deps.logger?.info("Mesh certificate recorded", {
|
|
778
|
+
tags: { nodeId },
|
|
779
|
+
meta: {
|
|
780
|
+
via,
|
|
781
|
+
serial: leaf.serial,
|
|
782
|
+
notAfter: new Date(leaf.notAfter).toISOString()
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
return record;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* A node was seen presenting `leaf` on its live connection. A leaf the hub already
|
|
789
|
+
* knows is left alone; an unknown one is recorded `observed` (a node enrolled
|
|
790
|
+
* before this record existed).
|
|
791
|
+
*/
|
|
792
|
+
async recordObserved(nodeId, leaf) {
|
|
793
|
+
const known = this.rows.get(leaf.fingerprint);
|
|
794
|
+
if (known !== void 0) {
|
|
795
|
+
if (known.revokedReason !== "renewal-abandoned") return false;
|
|
796
|
+
const reinstated = {
|
|
797
|
+
...known,
|
|
798
|
+
revokedAt: null,
|
|
799
|
+
revokedReason: null
|
|
800
|
+
};
|
|
801
|
+
this.rows.set(known.fingerprint, reinstated);
|
|
802
|
+
await this.persist(reinstated);
|
|
803
|
+
this.deps.logger?.warn("An abandoned renewal leaf is in use - reinstated", {
|
|
804
|
+
tags: { nodeId },
|
|
805
|
+
meta: { serial: known.serial }
|
|
806
|
+
});
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
const record = {
|
|
810
|
+
nodeId,
|
|
811
|
+
serial: leaf.serial,
|
|
812
|
+
fingerprint: leaf.fingerprint,
|
|
813
|
+
notBefore: leaf.notBefore,
|
|
814
|
+
notAfter: leaf.notAfter,
|
|
815
|
+
issuedAt: this.now(),
|
|
816
|
+
issuedVia: "observed",
|
|
817
|
+
revokedAt: null,
|
|
818
|
+
revokedReason: null
|
|
819
|
+
};
|
|
820
|
+
await this.insert(record);
|
|
821
|
+
this.deps.logger?.info("Mesh certificate observed on a live connection and recorded", {
|
|
822
|
+
tags: { nodeId },
|
|
823
|
+
meta: {
|
|
824
|
+
serial: leaf.serial,
|
|
825
|
+
fingerprint: leaf.fingerprint
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
list(nodeId) {
|
|
831
|
+
return [...this.rows.values()].filter((r) => nodeId === void 0 || r.nodeId === nodeId).toSorted((a, b) => b.issuedAt - a.issuedAt);
|
|
832
|
+
}
|
|
833
|
+
/** The newest non-revoked leaf of a node, or `undefined`. */
|
|
834
|
+
currentFor(nodeId) {
|
|
835
|
+
return this.list(nodeId).find((r) => r.revokedAt === null);
|
|
836
|
+
}
|
|
837
|
+
/** Every non-revoked leaf of a node (a renewal in flight has two). */
|
|
838
|
+
liveFor(nodeId) {
|
|
839
|
+
return this.list(nodeId).filter((r) => r.revokedAt === null);
|
|
840
|
+
}
|
|
841
|
+
/** Stamp a leaf revoked (and mirror it into `crl`). `false` when unknown or already revoked. */
|
|
842
|
+
async revoke(fingerprint, reason, crl) {
|
|
843
|
+
const record = this.rows.get(fingerprint);
|
|
844
|
+
if (record === void 0 || record.revokedAt !== null) return false;
|
|
845
|
+
const revoked = {
|
|
846
|
+
...record,
|
|
847
|
+
revokedAt: this.now(),
|
|
848
|
+
revokedReason: reason
|
|
849
|
+
};
|
|
850
|
+
this.rows.set(fingerprint, revoked);
|
|
851
|
+
crl?.revoke(record.serial);
|
|
852
|
+
await this.persist(revoked);
|
|
853
|
+
this.deps.logger?.warn("Mesh certificate revoked", {
|
|
854
|
+
tags: { nodeId: record.nodeId },
|
|
855
|
+
meta: {
|
|
856
|
+
serial: record.serial,
|
|
857
|
+
reason
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Operator revocation of a whole node: EVERY leaf of the node that is not already on the
|
|
864
|
+
* CRL goes on it - the current one, an observed one, a renewal one, and a leaf that was set
|
|
865
|
+
* aside (`renewal-abandoned`: a copy of it may sit on a stolen box, and it must not stay
|
|
866
|
+
* valid just because the hub once set it aside). Returns the serials put on the CRL.
|
|
867
|
+
* A leaf revoked this way is never reinstated by `recordObserved`.
|
|
868
|
+
*/
|
|
869
|
+
async revokeAllForNode(nodeId, reason, crl) {
|
|
870
|
+
const serials = [];
|
|
871
|
+
for (const record of this.list(nodeId)) {
|
|
872
|
+
if (record.revokedAt !== null && record.revokedReason !== "renewal-abandoned") continue;
|
|
873
|
+
const revoked = {
|
|
874
|
+
...record,
|
|
875
|
+
revokedAt: record.revokedAt ?? this.now(),
|
|
876
|
+
revokedReason: reason
|
|
877
|
+
};
|
|
878
|
+
this.rows.set(record.fingerprint, revoked);
|
|
879
|
+
crl.revoke(record.serial);
|
|
880
|
+
await this.persist(revoked);
|
|
881
|
+
serials.push(record.serial);
|
|
882
|
+
}
|
|
883
|
+
if (serials.length > 0) this.deps.logger?.warn("Every mesh certificate of the node revoked", {
|
|
884
|
+
tags: { nodeId },
|
|
885
|
+
meta: {
|
|
886
|
+
serials,
|
|
887
|
+
reason
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
return serials;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* At boot: a node with TWO live leaves had a renewal in flight when the hub stopped. The
|
|
894
|
+
* newer (renewal) one is set aside - it is not the node's current leaf until the node is
|
|
895
|
+
* seen presenting it, which reinstates it. It is NOT put on the CRL.
|
|
896
|
+
*/
|
|
897
|
+
async settleInterruptedRenewals() {
|
|
898
|
+
let settled = 0;
|
|
899
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
900
|
+
for (const record of this.rows.values()) {
|
|
901
|
+
if (record.revokedAt !== null) continue;
|
|
902
|
+
byNode.set(record.nodeId, [...byNode.get(record.nodeId) ?? [], record]);
|
|
903
|
+
}
|
|
904
|
+
for (const records of byNode.values()) {
|
|
905
|
+
if (records.length < 2) continue;
|
|
906
|
+
for (const record of records) if (record.issuedVia === "renewal" && await this.abandon(record.fingerprint)) settled += 1;
|
|
907
|
+
}
|
|
908
|
+
return settled;
|
|
909
|
+
}
|
|
910
|
+
/** Set a never-completed renewal leaf aside (not current, NOT on the CRL). */
|
|
911
|
+
async abandon(fingerprint) {
|
|
912
|
+
const record = this.rows.get(fingerprint);
|
|
913
|
+
if (record === void 0 || record.revokedAt !== null) return false;
|
|
914
|
+
const abandoned = {
|
|
915
|
+
...record,
|
|
916
|
+
revokedAt: this.now(),
|
|
917
|
+
revokedReason: RENEWAL_ABANDONED
|
|
918
|
+
};
|
|
919
|
+
this.rows.set(fingerprint, abandoned);
|
|
920
|
+
await this.persist(abandoned);
|
|
921
|
+
this.deps.logger?.warn("Renewal leaf set aside (not current, not on the CRL)", {
|
|
922
|
+
tags: { nodeId: record.nodeId },
|
|
923
|
+
meta: { serial: record.serial }
|
|
924
|
+
});
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
/** Replay every persisted revocation into `crl` (hub boot). Set-aside leaves are not revocations. */
|
|
928
|
+
restoreCrl(crl) {
|
|
929
|
+
let restored = 0;
|
|
930
|
+
for (const record of this.rows.values()) {
|
|
931
|
+
if (record.revokedAt === null || record.revokedReason === "renewal-abandoned") continue;
|
|
932
|
+
crl.revoke(record.serial);
|
|
933
|
+
restored += 1;
|
|
934
|
+
}
|
|
935
|
+
return restored;
|
|
936
|
+
}
|
|
937
|
+
/** Is `serial` listed as revoked here? */
|
|
938
|
+
isRevoked(serial) {
|
|
939
|
+
const wanted = normalizeSerial(serial);
|
|
940
|
+
for (const record of this.rows.values()) if (record.revokedAt !== null && record.revokedReason !== "renewal-abandoned" && normalizeSerial(record.serial) === wanted) return true;
|
|
941
|
+
return false;
|
|
942
|
+
}
|
|
943
|
+
async reap() {
|
|
944
|
+
const cutoff = this.now() - RETENTION_MS;
|
|
945
|
+
const dead = [...this.rows.values()].filter((r) => (r.revokedAt ?? r.notAfter) < cutoff);
|
|
946
|
+
for (const r of dead) {
|
|
947
|
+
this.rows.delete(r.fingerprint);
|
|
948
|
+
await this.store.delete.mutate({
|
|
949
|
+
collection: MESH_NODE_CERTIFICATES_COLLECTION,
|
|
950
|
+
key: r.fingerprint
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
return dead.length;
|
|
954
|
+
}
|
|
955
|
+
async insert(record) {
|
|
956
|
+
this.rows.set(record.fingerprint, record);
|
|
957
|
+
await this.store.insert.mutate({
|
|
958
|
+
collection: MESH_NODE_CERTIFICATES_COLLECTION,
|
|
959
|
+
record: {
|
|
960
|
+
id: record.fingerprint,
|
|
961
|
+
data: { ...record }
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
async persist(record) {
|
|
966
|
+
try {
|
|
967
|
+
await this.store.update.mutate({
|
|
968
|
+
collection: MESH_NODE_CERTIFICATES_COLLECTION,
|
|
969
|
+
id: record.fingerprint,
|
|
970
|
+
data: { ...record }
|
|
971
|
+
});
|
|
972
|
+
} catch (error) {
|
|
973
|
+
this.deps.logger?.error("Mesh certificate write failed - the change holds until restart", {
|
|
974
|
+
tags: { nodeId: record.nodeId },
|
|
975
|
+
meta: { error: error instanceof Error ? error.message : String(error) }
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
var SLOT$1 = Symbol.for("camstack.mesh.certificate-registry");
|
|
981
|
+
function setProcessMeshCertificateRegistry(registry) {
|
|
982
|
+
if (registry === null) Reflect.deleteProperty(globalThis, SLOT$1);
|
|
983
|
+
else Reflect.set(globalThis, SLOT$1, registry);
|
|
984
|
+
}
|
|
985
|
+
function getProcessMeshCertificateRegistry() {
|
|
986
|
+
const value = Reflect.get(globalThis, SLOT$1);
|
|
987
|
+
return value !== null && typeof value === "object" && typeof Reflect.get(value, "recordIssued") === "function" ? value : null;
|
|
988
|
+
}
|
|
989
|
+
//#endregion
|
|
990
|
+
//#region src/kernel/moleculer/mesh-mtls/mesh-tls-gate.ts
|
|
991
|
+
var STATE_SLOT = Symbol.for("camstack.mesh.tls-listener-state");
|
|
992
|
+
function setProcessMeshTlsListenerState(state) {
|
|
993
|
+
if (state === null) Reflect.deleteProperty(globalThis, STATE_SLOT);
|
|
994
|
+
else Reflect.set(globalThis, STATE_SLOT, state);
|
|
995
|
+
}
|
|
996
|
+
/** `null` before the gate has run at all (no mesh CA yet). */
|
|
997
|
+
function getProcessMeshTlsListenerState() {
|
|
998
|
+
const value = Reflect.get(globalThis, STATE_SLOT);
|
|
999
|
+
if (value === null || typeof value !== "object") return null;
|
|
1000
|
+
const state = Reflect.get(value, "state");
|
|
1001
|
+
return state === "open" || state === "closed" ? value : null;
|
|
1002
|
+
}
|
|
1003
|
+
var DEFAULT_RETRY_MS = 15e3;
|
|
1004
|
+
/**
|
|
1005
|
+
* Nothing in `run` can reject: a load or an open that throws is reported and retried, so a
|
|
1006
|
+
* failure can never become an unhandled rejection that leaves the listener closed in silence.
|
|
1007
|
+
*/
|
|
1008
|
+
function openMeshTlsWhenRevocationsLoaded(deps) {
|
|
1009
|
+
let attempt = 0;
|
|
1010
|
+
let opened = false;
|
|
1011
|
+
const now = deps.now ?? Date.now;
|
|
1012
|
+
let closedSince = null;
|
|
1013
|
+
const closed = (reason) => {
|
|
1014
|
+
closedSince ??= now();
|
|
1015
|
+
deps.onState?.({
|
|
1016
|
+
state: "closed",
|
|
1017
|
+
reason,
|
|
1018
|
+
since: closedSince
|
|
1019
|
+
});
|
|
1020
|
+
};
|
|
1021
|
+
const run = async () => {
|
|
1022
|
+
if (opened) return true;
|
|
1023
|
+
attempt += 1;
|
|
1024
|
+
let registry = null;
|
|
1025
|
+
try {
|
|
1026
|
+
registry = await deps.loadRegistry();
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
deps.onError?.(error);
|
|
1029
|
+
}
|
|
1030
|
+
if (registry === null) {
|
|
1031
|
+
closed("the revocation registry has not loaded - agents are refused");
|
|
1032
|
+
deps.onWaiting?.(attempt);
|
|
1033
|
+
deps.retry(() => void run(), deps.retryMs ?? DEFAULT_RETRY_MS);
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
try {
|
|
1037
|
+
await deps.open(registry);
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
deps.onError?.(error);
|
|
1040
|
+
closed("the listener failed to open - retrying");
|
|
1041
|
+
deps.retry(() => void run(), deps.retryMs ?? DEFAULT_RETRY_MS);
|
|
1042
|
+
return false;
|
|
1043
|
+
}
|
|
1044
|
+
opened = true;
|
|
1045
|
+
deps.onState?.({ state: "open" });
|
|
1046
|
+
return true;
|
|
1047
|
+
};
|
|
1048
|
+
return run();
|
|
1049
|
+
}
|
|
1050
|
+
//#endregion
|
|
1051
|
+
//#region src/kernel/moleculer/mesh-mtls/mesh-renewal.ts
|
|
1052
|
+
var DEFAULT_RECONNECT_TIMEOUT_MS = 45e3;
|
|
1053
|
+
var DEFAULT_CONFIRM_RETRY_MS = 500;
|
|
1054
|
+
function defaultSetTimer(fn, ms) {
|
|
1055
|
+
const timer = setTimeout(fn, ms);
|
|
1056
|
+
timer.unref?.();
|
|
1057
|
+
return { cancel: () => clearTimeout(timer) };
|
|
1058
|
+
}
|
|
1059
|
+
function defaultSleep(ms) {
|
|
1060
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1061
|
+
}
|
|
1062
|
+
function message$1(error) {
|
|
1063
|
+
return error instanceof Error ? error.message : String(error);
|
|
1064
|
+
}
|
|
1065
|
+
async function renewMeshCertificate(deps) {
|
|
1066
|
+
const { nodeId, log } = deps;
|
|
1067
|
+
const tags = { nodeId };
|
|
1068
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
1069
|
+
const confirmRetryMs = deps.confirmRetryMs ?? DEFAULT_CONFIRM_RETRY_MS;
|
|
1070
|
+
const setTimer = deps.setTimer ?? defaultSetTimer;
|
|
1071
|
+
const timeoutMs = deps.reconnectTimeoutMs ?? DEFAULT_RECONNECT_TIMEOUT_MS;
|
|
1072
|
+
const now = deps.now ?? Date.now;
|
|
1073
|
+
const live = deps.peers.peerLeaf(nodeId);
|
|
1074
|
+
const previous = live ?? deps.registry.currentFor(nodeId);
|
|
1075
|
+
if (live !== void 0) await deps.registry.recordObserved(nodeId, {
|
|
1076
|
+
cn: nodeId,
|
|
1077
|
+
serial: live.serial,
|
|
1078
|
+
fingerprint: live.fingerprint,
|
|
1079
|
+
notBefore: live.notBefore,
|
|
1080
|
+
notAfter: live.notAfter
|
|
1081
|
+
});
|
|
1082
|
+
log.info("Mesh renewal: starting", {
|
|
1083
|
+
tags,
|
|
1084
|
+
meta: {
|
|
1085
|
+
currentSerial: previous?.serial ?? null,
|
|
1086
|
+
connected: live !== void 0
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
const fail = (stage, reason, keptPreviousLeaf) => {
|
|
1090
|
+
log.error("Mesh renewal: failed", {
|
|
1091
|
+
tags,
|
|
1092
|
+
meta: {
|
|
1093
|
+
stage,
|
|
1094
|
+
reason,
|
|
1095
|
+
keptPreviousLeaf
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
return {
|
|
1099
|
+
ok: false,
|
|
1100
|
+
stage,
|
|
1101
|
+
reason,
|
|
1102
|
+
keptPreviousLeaf
|
|
1103
|
+
};
|
|
1104
|
+
};
|
|
1105
|
+
let csrPem;
|
|
1106
|
+
try {
|
|
1107
|
+
csrPem = (await deps.client.prepareMeshRenewal()).csrPem;
|
|
1108
|
+
log.info("Mesh renewal: the node generated a new key and sent its CSR", { tags });
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
return fail("prepare", message$1(error), true);
|
|
1111
|
+
}
|
|
1112
|
+
let issued;
|
|
1113
|
+
try {
|
|
1114
|
+
issued = await issueMeshLeafFromCsr(deps.ca, csrPem, { expectedNodeId: nodeId });
|
|
1115
|
+
await deps.registry.recordIssued(nodeId, issued.certPem, "renewal");
|
|
1116
|
+
log.info("Mesh renewal: new leaf signed and recorded", {
|
|
1117
|
+
tags,
|
|
1118
|
+
meta: {
|
|
1119
|
+
serial: issued.serialNumber,
|
|
1120
|
+
notAfter: issued.notAfter.toISOString()
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
return fail("issue", message$1(error), true);
|
|
1125
|
+
}
|
|
1126
|
+
const newLeaf = describeMeshLeaf(issued.certPem);
|
|
1127
|
+
let resolveRegistered = () => void 0;
|
|
1128
|
+
const registeredPromise = new Promise((resolve) => {
|
|
1129
|
+
resolveRegistered = resolve;
|
|
1130
|
+
});
|
|
1131
|
+
const unsubscribe = deps.registrations.subscribe((id) => {
|
|
1132
|
+
if (id === nodeId && deps.peers.peerLeaf(nodeId)?.fingerprint === newLeaf.fingerprint) resolveRegistered(true);
|
|
1133
|
+
});
|
|
1134
|
+
const deadlineTimer = setTimer(() => resolveRegistered(false), timeoutMs);
|
|
1135
|
+
registeredPromise.then(() => deadlineTimer.cancel());
|
|
1136
|
+
try {
|
|
1137
|
+
await deps.client.installMeshLeaf({
|
|
1138
|
+
certPem: issued.certPem,
|
|
1139
|
+
caCertPem: deps.ca.certPem,
|
|
1140
|
+
confirmWithinMs: timeoutMs + 3e4
|
|
1141
|
+
});
|
|
1142
|
+
log.info("Mesh renewal: the node installed the new leaf and is reconnecting", { tags });
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
unsubscribe();
|
|
1145
|
+
deadlineTimer.cancel();
|
|
1146
|
+
await deps.client.rollbackMeshLeaf().catch(() => void 0);
|
|
1147
|
+
await deps.registry.abandon(newLeaf.fingerprint);
|
|
1148
|
+
return fail("install", message$1(error), true);
|
|
1149
|
+
}
|
|
1150
|
+
const deadline = now() + timeoutMs;
|
|
1151
|
+
const registered = await registeredPromise;
|
|
1152
|
+
unsubscribe();
|
|
1153
|
+
if (!registered) {
|
|
1154
|
+
const back = await deps.client.rollbackMeshLeaf().catch(() => ({ rolledBack: false }));
|
|
1155
|
+
log.warn("Mesh renewal: the node was not back and registered with the new leaf in time", {
|
|
1156
|
+
tags,
|
|
1157
|
+
meta: {
|
|
1158
|
+
timeoutMs,
|
|
1159
|
+
rolledBack: back.rolledBack
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
await deps.registry.abandon(newLeaf.fingerprint);
|
|
1163
|
+
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);
|
|
1164
|
+
}
|
|
1165
|
+
log.info("Mesh renewal: the node registered presenting the new leaf", { tags });
|
|
1166
|
+
let confirmError = "the node did not confirm the new leaf";
|
|
1167
|
+
let confirmedByNode = false;
|
|
1168
|
+
for (;;) {
|
|
1169
|
+
try {
|
|
1170
|
+
if ((await deps.client.confirmMeshLeaf({ fingerprintSha256: newLeaf.fingerprint })).confirmed) {
|
|
1171
|
+
confirmedByNode = true;
|
|
1172
|
+
break;
|
|
1173
|
+
}
|
|
1174
|
+
confirmError = "the node did not confirm the new leaf";
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
confirmError = message$1(error);
|
|
1177
|
+
log.warn("Mesh renewal: confirm not accepted yet", {
|
|
1178
|
+
tags,
|
|
1179
|
+
meta: { error: confirmError }
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
if (now() >= deadline) break;
|
|
1183
|
+
await sleep(confirmRetryMs);
|
|
1184
|
+
}
|
|
1185
|
+
if (!confirmedByNode) return fail("confirm", confirmError, false);
|
|
1186
|
+
if (previous !== void 0 && previous.serial !== newLeaf.serial) {
|
|
1187
|
+
await deps.registry.revoke(previous.fingerprint, "replaced-by-renewal", deps.crl);
|
|
1188
|
+
deps.crl.revoke(previous.serial);
|
|
1189
|
+
log.info("Mesh renewal: the previous leaf is revoked", {
|
|
1190
|
+
tags,
|
|
1191
|
+
meta: { oldSerial: previous.serial }
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
log.info("Mesh renewal: done", {
|
|
1195
|
+
tags,
|
|
1196
|
+
meta: {
|
|
1197
|
+
newSerial: newLeaf.serial,
|
|
1198
|
+
notAfter: new Date(newLeaf.notAfter).toISOString()
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
return {
|
|
1202
|
+
ok: true,
|
|
1203
|
+
oldSerial: previous?.serial ?? null,
|
|
1204
|
+
newSerial: newLeaf.serial,
|
|
1205
|
+
newNotAfter: newLeaf.notAfter
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Revoke every live mesh leaf of `nodeId` and cut its connection. The node cannot
|
|
1210
|
+
* reconnect: it must re-join with a new join token. The node is NOT forgotten
|
|
1211
|
+
* (that stays `clusterNodes.forgetNode`).
|
|
1212
|
+
*/
|
|
1213
|
+
async function revokeMeshCertificate(deps) {
|
|
1214
|
+
const { nodeId, log } = deps;
|
|
1215
|
+
const tags = { nodeId };
|
|
1216
|
+
const live = deps.peers.peerLeaf(nodeId);
|
|
1217
|
+
if (live !== void 0) await deps.registry.recordObserved(nodeId, {
|
|
1218
|
+
cn: nodeId,
|
|
1219
|
+
serial: live.serial,
|
|
1220
|
+
fingerprint: live.fingerprint,
|
|
1221
|
+
notBefore: live.notBefore,
|
|
1222
|
+
notAfter: live.notAfter
|
|
1223
|
+
});
|
|
1224
|
+
const serials = [...await deps.registry.revokeAllForNode(nodeId, "revoked-by-operator", deps.crl)];
|
|
1225
|
+
const disconnected = deps.peers.dropPeer(nodeId);
|
|
1226
|
+
log.warn("Mesh certificate revoked and the node disconnected", {
|
|
1227
|
+
tags,
|
|
1228
|
+
meta: {
|
|
1229
|
+
serials,
|
|
1230
|
+
disconnected
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
return {
|
|
1234
|
+
nodeId,
|
|
1235
|
+
revokedSerials: serials,
|
|
1236
|
+
disconnected,
|
|
1237
|
+
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."
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
//#endregion
|
|
611
1241
|
//#region src/kernel/moleculer/mesh-mtls/mesh-peer-transport.ts
|
|
612
1242
|
var LOOKUP_SLOT = Symbol.for("camstack.cluster.mesh-peer-transport-lookup");
|
|
613
1243
|
/** Published by the hub broker owner; cleared on shutdown. */
|
|
@@ -626,6 +1256,36 @@ function getProcessMeshPeerTransportLookup() {
|
|
|
626
1256
|
function getProcessNodeMeshTransport(nodeId) {
|
|
627
1257
|
return getProcessMeshPeerTransportLookup()?.transportOf(nodeId);
|
|
628
1258
|
}
|
|
1259
|
+
/** The leaf `nodeId` presented on its live connection, or `undefined`. */
|
|
1260
|
+
function getProcessPeerLeaf(nodeId) {
|
|
1261
|
+
return getProcessMeshPeerTransportLookup()?.peerLeaf?.(nodeId);
|
|
1262
|
+
}
|
|
1263
|
+
/** Disconnect `nodeId` (closes its mutual-TLS sockets). `0` when none were open. */
|
|
1264
|
+
function dropProcessPeer(nodeId) {
|
|
1265
|
+
return getProcessMeshPeerTransportLookup()?.dropPeer?.(nodeId) ?? 0;
|
|
1266
|
+
}
|
|
1267
|
+
var REGISTRATION_SLOT = Symbol.for("camstack.cluster.node-registration-listeners");
|
|
1268
|
+
function registrationListeners() {
|
|
1269
|
+
const existing = Reflect.get(globalThis, REGISTRATION_SLOT);
|
|
1270
|
+
if (existing instanceof Set) return existing;
|
|
1271
|
+
const created = /* @__PURE__ */ new Set();
|
|
1272
|
+
Reflect.set(globalThis, REGISTRATION_SLOT, created);
|
|
1273
|
+
return created;
|
|
1274
|
+
}
|
|
1275
|
+
/** Subscribe to "node X finished registering". Returns the unsubscribe. */
|
|
1276
|
+
function onProcessNodeRegistered(listener) {
|
|
1277
|
+
const listeners = registrationListeners();
|
|
1278
|
+
listeners.add(listener);
|
|
1279
|
+
return () => {
|
|
1280
|
+
listeners.delete(listener);
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
/** Called by the hub when a node's `registerNode` has been applied. */
|
|
1284
|
+
function emitProcessNodeRegistered(nodeId) {
|
|
1285
|
+
for (const listener of [...registrationListeners()]) try {
|
|
1286
|
+
listener(nodeId);
|
|
1287
|
+
} catch {}
|
|
1288
|
+
}
|
|
629
1289
|
//#endregion
|
|
630
1290
|
//#region ../../node_modules/jose/dist/webapi/lib/buffer_utils.js
|
|
631
1291
|
var encoder = new TextEncoder(), decoder = new TextDecoder(), strictDecoder = new TextDecoder("utf-8", { fatal: !0 });
|
|
@@ -1475,6 +2135,11 @@ async function enrollMeshNode(body, deps) {
|
|
|
1475
2135
|
status: 400
|
|
1476
2136
|
};
|
|
1477
2137
|
}
|
|
2138
|
+
try {
|
|
2139
|
+
await deps.onIssued?.(leaf.commonName, leaf.certPem);
|
|
2140
|
+
} catch (err) {
|
|
2141
|
+
deps.onRecordFailed?.(leaf.commonName, err);
|
|
2142
|
+
}
|
|
1478
2143
|
return {
|
|
1479
2144
|
ok: true,
|
|
1480
2145
|
cert: leaf.certPem,
|
|
@@ -86792,6 +87457,23 @@ function peekSender(transporter, type, message) {
|
|
|
86792
87457
|
return null;
|
|
86793
87458
|
}
|
|
86794
87459
|
}
|
|
87460
|
+
function peerLeafOf(socket) {
|
|
87461
|
+
const cert = socket.getPeerCertificate(true);
|
|
87462
|
+
if (cert == null || typeof cert !== "object" || !("raw" in cert) || !Buffer.isBuffer(cert.raw)) return null;
|
|
87463
|
+
try {
|
|
87464
|
+
const x509 = new node_crypto.X509Certificate(cert.raw);
|
|
87465
|
+
const remote = socket.remoteAddress;
|
|
87466
|
+
return {
|
|
87467
|
+
...remote !== void 0 ? { remoteAddress: remote.replace(/^::ffff:/, "") } : {},
|
|
87468
|
+
serial: x509.serialNumber,
|
|
87469
|
+
fingerprint: x509.fingerprint256,
|
|
87470
|
+
notBefore: Date.parse(x509.validFrom),
|
|
87471
|
+
notAfter: Date.parse(x509.validTo)
|
|
87472
|
+
};
|
|
87473
|
+
} catch {
|
|
87474
|
+
return null;
|
|
87475
|
+
}
|
|
87476
|
+
}
|
|
86795
87477
|
function peerIdentity(socket) {
|
|
86796
87478
|
const cert = socket.getPeerCertificate(true);
|
|
86797
87479
|
if (cert == null || typeof cert !== "object" || !("raw" in cert) || !cert.raw) return null;
|
|
@@ -86819,6 +87501,8 @@ var TlsTcpTransporter = class extends TcpTransporterBase {
|
|
|
86819
87501
|
tlsSockets = /* @__PURE__ */ new Set();
|
|
86820
87502
|
/** Node id (certificate CN) → serial of every peer that completed a mutual-TLS handshake. */
|
|
86821
87503
|
tlsPeers = /* @__PURE__ */ new Map();
|
|
87504
|
+
/** Node id → the leaf it presented at its last handshake (hub inventory, D856). */
|
|
87505
|
+
peerLeaves = /* @__PURE__ */ new Map();
|
|
86822
87506
|
clearDisabled = false;
|
|
86823
87507
|
hubTlsOnly;
|
|
86824
87508
|
hubOnlyPeers;
|
|
@@ -86932,6 +87616,7 @@ var TlsTcpTransporter = class extends TcpTransporterBase {
|
|
|
86932
87616
|
tlsSock.meshPeerCn = identity.cn;
|
|
86933
87617
|
tlsSock.meshPeerSerial = identity.serial;
|
|
86934
87618
|
self.tlsPeers.set(identity.cn, identity.serial);
|
|
87619
|
+
self.rememberPeerLeaf(identity.cn, tlsSock);
|
|
86935
87620
|
self.tlsSockets.add(tlsSock);
|
|
86936
87621
|
tlsSock.on("close", () => {
|
|
86937
87622
|
self.tlsSockets.delete(tlsSock);
|
|
@@ -86984,6 +87669,25 @@ var TlsTcpTransporter = class extends TcpTransporterBase {
|
|
|
86984
87669
|
forgetPeer(nodeId) {
|
|
86985
87670
|
for (const socket of this.tlsSockets) if (socket.meshPeerCn === nodeId && !socket.destroyed) return;
|
|
86986
87671
|
this.tlsPeers.delete(nodeId);
|
|
87672
|
+
this.peerLeaves.delete(nodeId);
|
|
87673
|
+
}
|
|
87674
|
+
rememberPeerLeaf(nodeId, socket) {
|
|
87675
|
+
const leaf = peerLeafOf(socket);
|
|
87676
|
+
if (leaf !== null) this.peerLeaves.set(nodeId, leaf);
|
|
87677
|
+
}
|
|
87678
|
+
/** The leaf `nodeId` presented at its last mutual-TLS handshake. */
|
|
87679
|
+
peerLeaf(nodeId) {
|
|
87680
|
+
return this.peerLeaves.get(nodeId);
|
|
87681
|
+
}
|
|
87682
|
+
/** Close every live mutual-TLS socket of `nodeId` (revocation, renewal). Returns how many. */
|
|
87683
|
+
dropPeer(nodeId) {
|
|
87684
|
+
let closed = 0;
|
|
87685
|
+
for (const socket of this.tlsSockets) {
|
|
87686
|
+
if (socket.meshPeerCn !== nodeId || socket.destroyed) continue;
|
|
87687
|
+
socket.destroy();
|
|
87688
|
+
closed += 1;
|
|
87689
|
+
}
|
|
87690
|
+
return closed;
|
|
86987
87691
|
}
|
|
86988
87692
|
/** True once the mutual-TLS listener is bound. */
|
|
86989
87693
|
isTlsListening() {
|
|
@@ -87038,6 +87742,7 @@ var TlsTcpTransporter = class extends TcpTransporterBase {
|
|
|
87038
87742
|
} catch {}
|
|
87039
87743
|
this.tlsSockets.clear();
|
|
87040
87744
|
this.tlsPeers.clear();
|
|
87745
|
+
this.peerLeaves.clear();
|
|
87041
87746
|
return super.disconnect();
|
|
87042
87747
|
}
|
|
87043
87748
|
/**
|
|
@@ -87166,6 +87871,7 @@ var TlsTcpTransporter = class extends TcpTransporterBase {
|
|
|
87166
87871
|
socket.meshPeerSerial = identity.serial;
|
|
87167
87872
|
this.tlsSockets.add(socket);
|
|
87168
87873
|
this.tlsPeers.set(identity.cn, identity.serial);
|
|
87874
|
+
this.rememberPeerLeaf(identity.cn, socket);
|
|
87169
87875
|
const reader = this.reader;
|
|
87170
87876
|
socket.on("close", () => {
|
|
87171
87877
|
this.tlsSockets.delete(socket);
|
|
@@ -87387,6 +88093,8 @@ var systemConfigMeshActions = require_dist.defineCustomActions({
|
|
|
87387
88093
|
});
|
|
87388
88094
|
//#endregion
|
|
87389
88095
|
//#region src/builtins/system-config/system-config.addon.ts
|
|
88096
|
+
/** How often superseded and expired mesh certificate rows are reaped. */
|
|
88097
|
+
var MESH_REAP_MS = 24 * 36e5;
|
|
87390
88098
|
/**
|
|
87391
88099
|
* Built-in `system-config` addon — Phase 4 of the settings redesign.
|
|
87392
88100
|
*
|
|
@@ -87464,6 +88172,10 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
|
|
|
87464
88172
|
id = "system-config";
|
|
87465
88173
|
clusterSecretKeeper = null;
|
|
87466
88174
|
meshCaKeeper = null;
|
|
88175
|
+
meshReapTimer = null;
|
|
88176
|
+
meshGateTimers = /* @__PURE__ */ new Set();
|
|
88177
|
+
stoppedMesh = false;
|
|
88178
|
+
loggedNoSettingsStore = false;
|
|
87467
88179
|
constructor() {
|
|
87468
88180
|
super({});
|
|
87469
88181
|
}
|
|
@@ -87503,19 +88215,107 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
|
|
|
87503
88215
|
}
|
|
87504
88216
|
async onShutdown() {
|
|
87505
88217
|
setProcessMeshEnforcer(null);
|
|
88218
|
+
setProcessMeshCertificateRegistry(null);
|
|
88219
|
+
setProcessMeshTlsListenerState(null);
|
|
88220
|
+
require_agent_https_names.setProcessAgentHttpsNames(null);
|
|
88221
|
+
this.stoppedMesh = true;
|
|
88222
|
+
if (this.meshReapTimer !== null) clearInterval(this.meshReapTimer);
|
|
88223
|
+
this.meshReapTimer = null;
|
|
88224
|
+
for (const timer of this.meshGateTimers) clearTimeout(timer);
|
|
88225
|
+
this.meshGateTimers.clear();
|
|
87506
88226
|
this.meshCaKeeper?.stop();
|
|
87507
88227
|
this.meshCaKeeper = null;
|
|
87508
88228
|
this.clusterSecretKeeper?.stop();
|
|
87509
88229
|
this.clusterSecretKeeper = null;
|
|
87510
88230
|
}
|
|
88231
|
+
/**
|
|
88232
|
+
* The hub's record of mesh leaves (D856): declared and loaded BEFORE the TLS
|
|
88233
|
+
* listener opens, so persisted revocations are in the CRL before any node can
|
|
88234
|
+
* connect. Best effort: a hub whose settings store is not up still serves the
|
|
88235
|
+
* mesh (the registry then fills from the leaves nodes present).
|
|
88236
|
+
*/
|
|
88237
|
+
async initMeshCertificateRegistry() {
|
|
88238
|
+
const store = this.ctx.api?.settingsStore;
|
|
88239
|
+
if (!store) {
|
|
88240
|
+
if (!this.loggedNoSettingsStore) {
|
|
88241
|
+
this.loggedNoSettingsStore = true;
|
|
88242
|
+
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)");
|
|
88243
|
+
}
|
|
88244
|
+
return null;
|
|
88245
|
+
}
|
|
88246
|
+
this.loadAgentHttpsNames(store);
|
|
88247
|
+
try {
|
|
88248
|
+
await store.declareCollection.mutate({
|
|
88249
|
+
collection: MESH_NODE_CERTIFICATES_COLLECTION,
|
|
88250
|
+
columns: MESH_NODE_CERTIFICATES_COLUMNS,
|
|
88251
|
+
indexes: [{
|
|
88252
|
+
name: "idx_mesh_node_certificates_node",
|
|
88253
|
+
columns: ["nodeId"]
|
|
88254
|
+
}]
|
|
88255
|
+
});
|
|
88256
|
+
const registry = new MeshCertificateRegistry(store, { logger: this.ctx.logger });
|
|
88257
|
+
await registry.load();
|
|
88258
|
+
const restored = registry.restoreCrl(processMeshCrl());
|
|
88259
|
+
const settled = await registry.settleInterruptedRenewals();
|
|
88260
|
+
if (settled > 0) this.ctx.logger.warn("A mesh renewal was interrupted by a hub restart - its new leaf was set aside", { meta: { settled } });
|
|
88261
|
+
setProcessMeshCertificateRegistry(registry);
|
|
88262
|
+
if (restored > 0) this.ctx.logger.info("Mesh revocations restored into the CRL", { meta: { restored } });
|
|
88263
|
+
return registry;
|
|
88264
|
+
} catch (err) {
|
|
88265
|
+
this.ctx.logger.error("Mesh certificate registry unavailable", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
88266
|
+
return null;
|
|
88267
|
+
}
|
|
88268
|
+
}
|
|
88269
|
+
/** Which names the hub certified to which agent. Best effort; nothing waits on it. */
|
|
88270
|
+
async loadAgentHttpsNames(store) {
|
|
88271
|
+
try {
|
|
88272
|
+
await store.declareCollection.mutate({
|
|
88273
|
+
collection: require_agent_https_names.AGENT_HTTPS_NAMES_COLLECTION,
|
|
88274
|
+
columns: require_agent_https_names.AGENT_HTTPS_NAMES_COLUMNS
|
|
88275
|
+
});
|
|
88276
|
+
const names = new require_agent_https_names.AgentHttpsNames(store);
|
|
88277
|
+
await names.load();
|
|
88278
|
+
require_agent_https_names.setProcessAgentHttpsNames(names);
|
|
88279
|
+
} catch (err) {
|
|
88280
|
+
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) } });
|
|
88281
|
+
}
|
|
88282
|
+
}
|
|
88283
|
+
/**
|
|
88284
|
+
* The mesh TLS listener opens ONLY after the revocation registry has loaded and its
|
|
88285
|
+
* revocations are in the CRL (D856): until then every agent is refused, because an
|
|
88286
|
+
* empty CRL would accept a revoked leaf. Retries until the settings store answers.
|
|
88287
|
+
*/
|
|
87511
88288
|
async onMeshCaReady(ca) {
|
|
87512
|
-
|
|
87513
|
-
|
|
87514
|
-
|
|
87515
|
-
|
|
87516
|
-
|
|
88289
|
+
await openMeshTlsWhenRevocationsLoaded({
|
|
88290
|
+
loadRegistry: () => this.initMeshCertificateRegistry(),
|
|
88291
|
+
open: async (registry) => {
|
|
88292
|
+
const hubLeaf = await issueMeshLeafForNode(ca, "hub");
|
|
88293
|
+
await registry.recordIssued("hub", hubLeaf.certPem, "boot").catch((err) => {
|
|
88294
|
+
this.ctx.logger.warn("Hub mesh leaf could not be recorded", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
88295
|
+
});
|
|
88296
|
+
await registry.reap().catch(() => void 0);
|
|
88297
|
+
if (this.meshReapTimer !== null) clearInterval(this.meshReapTimer);
|
|
88298
|
+
this.meshReapTimer = setInterval(() => void registry.reap().catch(() => void 0), MESH_REAP_MS);
|
|
88299
|
+
this.meshReapTimer.unref?.();
|
|
88300
|
+
await requestProcessMeshTlsEnable({
|
|
88301
|
+
caCertPem: ca.certPem,
|
|
88302
|
+
certPem: hubLeaf.certPem,
|
|
88303
|
+
keyPem: hubLeaf.keyPem
|
|
88304
|
+
});
|
|
88305
|
+
this.ctx.logger.info("Mesh CA ready — revocations loaded, TLS material offered to the transporter");
|
|
88306
|
+
},
|
|
88307
|
+
retry: (fn, ms) => {
|
|
88308
|
+
if (this.stoppedMesh) return;
|
|
88309
|
+
const timer = setTimeout(fn, ms);
|
|
88310
|
+
timer.unref?.();
|
|
88311
|
+
this.meshGateTimers.add(timer);
|
|
88312
|
+
},
|
|
88313
|
+
onState: (state) => setProcessMeshTlsListenerState(state),
|
|
88314
|
+
onWaiting: (attempt) => {
|
|
88315
|
+
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 } });
|
|
88316
|
+
},
|
|
88317
|
+
onError: (err) => this.ctx.logger.error("Mesh certificate registry failed to load", { meta: { error: err instanceof Error ? err.message : String(err) } })
|
|
87517
88318
|
});
|
|
87518
|
-
this.ctx.logger.info("Mesh CA ready — TLS material offered to the transporter");
|
|
87519
88319
|
}
|
|
87520
88320
|
async mintJoinToken(input) {
|
|
87521
88321
|
const ca = requireProcessMeshCa();
|
|
@@ -87864,9 +88664,13 @@ exports.AGENT_UI_UNLOCK_VERSION = AGENT_UI_UNLOCK_VERSION;
|
|
|
87864
88664
|
exports.Errors = Errors;
|
|
87865
88665
|
exports.JoinTokenJtiStore = JoinTokenJtiStore;
|
|
87866
88666
|
exports.MESH_AUTH_MODE_FIELD = MESH_AUTH_MODE_FIELD;
|
|
88667
|
+
exports.MESH_NODE_CERTIFICATES_COLLECTION = MESH_NODE_CERTIFICATES_COLLECTION;
|
|
88668
|
+
exports.MESH_NODE_CERTIFICATES_COLUMNS = MESH_NODE_CERTIFICATES_COLUMNS;
|
|
87867
88669
|
exports.MESH_TLS_PORT_HELLO_FIELD = MESH_TLS_PORT_HELLO_FIELD;
|
|
87868
88670
|
exports.MeshCaKeeper = MeshCaKeeper;
|
|
88671
|
+
exports.MeshCertificateRegistry = MeshCertificateRegistry;
|
|
87869
88672
|
exports.MeshCrl = MeshCrl;
|
|
88673
|
+
exports.RENEWAL_ABANDONED = RENEWAL_ABANDONED;
|
|
87870
88674
|
exports.SYSTEM_CONFIG_CLUSTER_SECTION_ID = SYSTEM_CONFIG_CLUSTER_SECTION_ID;
|
|
87871
88675
|
exports.ServiceBroker = ServiceBroker;
|
|
87872
88676
|
exports.SystemConfigAddon = SystemConfigAddon;
|
|
@@ -87877,6 +88681,9 @@ exports.caFingerprint = caFingerprint;
|
|
|
87877
88681
|
exports.caFingerprintCompact = caFingerprintCompact;
|
|
87878
88682
|
exports.createCidrMatcher = createCidrMatcher;
|
|
87879
88683
|
exports.createMeshCsrPem = createMeshCsrPem;
|
|
88684
|
+
exports.describeMeshLeaf = describeMeshLeaf;
|
|
88685
|
+
exports.dropProcessPeer = dropProcessPeer;
|
|
88686
|
+
exports.emitProcessNodeRegistered = emitProcessNodeRegistered;
|
|
87880
88687
|
exports.enrollMeshNode = enrollMeshNode;
|
|
87881
88688
|
exports.exportMeshPrivateKeyPem = exportMeshPrivateKeyPem;
|
|
87882
88689
|
exports.generateMeshCa = generateMeshCa;
|
|
@@ -87884,7 +88691,10 @@ exports.generateMeshKeyPair = generateMeshKeyPair;
|
|
|
87884
88691
|
exports.getProcessMeshAuthMode = getProcessMeshAuthMode;
|
|
87885
88692
|
exports.getProcessMeshAuthStatus = getProcessMeshAuthStatus;
|
|
87886
88693
|
exports.getProcessMeshCaState = getProcessMeshCaState;
|
|
88694
|
+
exports.getProcessMeshCertificateRegistry = getProcessMeshCertificateRegistry;
|
|
88695
|
+
exports.getProcessMeshTlsListenerState = getProcessMeshTlsListenerState;
|
|
87887
88696
|
exports.getProcessNodeMeshTransport = getProcessNodeMeshTransport;
|
|
88697
|
+
exports.getProcessPeerLeaf = getProcessPeerLeaf;
|
|
87888
88698
|
exports.isAllowedAgentDashboardReturnTo = isAllowedAgentDashboardReturnTo;
|
|
87889
88699
|
exports.isMeshAuthMode = isMeshAuthMode;
|
|
87890
88700
|
exports.issueMeshLeafForNode = issueMeshLeafForNode;
|
|
@@ -87893,19 +88703,25 @@ exports.loadMeshCa = loadMeshCa;
|
|
|
87893
88703
|
exports.mintAgentUiUnlockTicket = mintAgentUiUnlockTicket;
|
|
87894
88704
|
exports.mintMeshJoinToken = mintMeshJoinToken;
|
|
87895
88705
|
exports.normalizeIp = normalizeIp;
|
|
88706
|
+
exports.onProcessNodeRegistered = onProcessNodeRegistered;
|
|
88707
|
+
exports.openMeshTlsWhenRevocationsLoaded = openMeshTlsWhenRevocationsLoaded;
|
|
87896
88708
|
exports.parseAgentDashboardOrigins = parseAgentDashboardOrigins;
|
|
87897
88709
|
exports.parseCidrList = parseCidrList;
|
|
87898
88710
|
exports.parseMeshAuthMode = parseMeshAuthMode;
|
|
87899
88711
|
exports.processJoinTokenJtiStore = processJoinTokenJtiStore;
|
|
87900
88712
|
exports.processMeshCrl = processMeshCrl;
|
|
87901
88713
|
exports.readCidrSetting = readCidrSetting;
|
|
88714
|
+
exports.renewMeshCertificate = renewMeshCertificate;
|
|
87902
88715
|
exports.requestProcessMeshEnforcement = requestProcessMeshEnforcement;
|
|
87903
88716
|
exports.requireProcessMeshCa = requireProcessMeshCa;
|
|
88717
|
+
exports.revokeMeshCertificate = revokeMeshCertificate;
|
|
87904
88718
|
exports.setProcessMeshAuthMode = setProcessMeshAuthMode;
|
|
87905
88719
|
exports.setProcessMeshAuthStatusSource = setProcessMeshAuthStatusSource;
|
|
88720
|
+
exports.setProcessMeshCertificateRegistry = setProcessMeshCertificateRegistry;
|
|
87906
88721
|
exports.setProcessMeshEnforcer = setProcessMeshEnforcer;
|
|
87907
88722
|
exports.setProcessMeshPeerTransportLookup = setProcessMeshPeerTransportLookup;
|
|
87908
88723
|
exports.setProcessMeshTlsEnabler = setProcessMeshTlsEnabler;
|
|
88724
|
+
exports.setProcessMeshTlsListenerState = setProcessMeshTlsListenerState;
|
|
87909
88725
|
exports.systemConfigMeshActions = systemConfigMeshActions;
|
|
87910
88726
|
exports.verifyAgentUiUnlockTicket = verifyAgentUiUnlockTicket;
|
|
87911
88727
|
exports.verifyMeshJoinToken = verifyMeshJoinToken;
|