@camstack/server 1.2.486 → 1.2.488
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/agent-mesh-join.js +121 -0
- package/dist/agent/agent-service.js +4 -0
- package/dist/agent/main.js +62 -2
- package/dist/api/auth-whoami.js +1 -1
- package/dist/api/core/auth-admin-tokens.procedures.js +138 -0
- package/dist/api/core/auth-limits.js +77 -0
- package/dist/api/core/auth-reauth.procedures.js +108 -0
- package/dist/api/core/auth-sessions.procedures.js +166 -0
- package/dist/api/core/auth.router.js +281 -80
- package/dist/api/health/adaptive-probe.routes.js +1 -1
- package/dist/api/mesh/enroll.routes.js +28 -0
- package/dist/api/trpc/core-cap-bridge.js +88 -5
- package/dist/api/trpc/generated-cap-mounts.js +19 -1
- package/dist/api/trpc/generated-cap-routers.js +512 -205
- package/dist/api/trpc/trpc.context.js +86 -34
- package/dist/api/trpc/trpc.middleware.js +8 -11
- package/dist/api/trpc/trpc.router.js +6 -1
- package/dist/auth/auth-network-policy.js +75 -0
- package/dist/auth/auth-rate-limit.js +25 -20
- package/dist/auth/client-address.js +68 -0
- package/dist/auth/external-link-intents.js +12 -3
- package/dist/auth/hub-sessions.js +74 -0
- package/dist/auth/native-link-completions.js +68 -0
- package/dist/auth/native-return.js +33 -0
- package/dist/auth/session-lifetimes.js +46 -0
- package/dist/auth/session-mint.js +110 -13
- package/dist/auth/sso-finish-deps.js +56 -1
- package/dist/auth/sso-finish.js +132 -17
- package/dist/auth/sso-routes.js +95 -0
- package/dist/auth/used-bridge-ids.js +52 -0
- package/dist/core/addon/addon-registry.service.js +5 -0
- package/dist/core/agent/agent-registry.service.js +23 -0
- package/dist/core/auth/auth.service.js +50 -1
- package/dist/core/auth/handoff-code.service.js +9 -3
- package/dist/core/moleculer/hub-mesh-guard.js +132 -0
- package/dist/core/moleculer/moleculer.service.js +46 -0
- package/dist/main.js +97 -29
- package/package.json +13 -13
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UsedBridgeIds = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The `jti`s of login bridges already swapped for a session or a link.
|
|
6
|
+
*
|
|
7
|
+
* A bridge lives 5 minutes; until it expires a second presentation is refused.
|
|
8
|
+
* Entries are kept for {@link RETENTION_MS} (longer than any bridge), and the
|
|
9
|
+
* set is bounded — only a bridge that verified AND matched its browser's
|
|
10
|
+
* state cookie is ever recorded, so filling it needs that many real sign-ins.
|
|
11
|
+
*
|
|
12
|
+
* In memory: a hub restart within a bridge's 5 minutes forgets it, and the
|
|
13
|
+
* browser binding (the bridge is useless without the state cookie of the
|
|
14
|
+
* browser that started the sign-in) is what still stands then.
|
|
15
|
+
*/
|
|
16
|
+
const RETENTION_MS = 10 * 60_000;
|
|
17
|
+
const DEFAULT_MAX_ENTRIES = 10_000;
|
|
18
|
+
class UsedBridgeIds {
|
|
19
|
+
used = new Map();
|
|
20
|
+
now;
|
|
21
|
+
maxEntries;
|
|
22
|
+
constructor(opts = {}) {
|
|
23
|
+
this.now = opts.now ?? Date.now;
|
|
24
|
+
this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
25
|
+
}
|
|
26
|
+
/** Record `jti`; `true` the first time, `false` for every replay. */
|
|
27
|
+
consume(jti) {
|
|
28
|
+
const at = this.now();
|
|
29
|
+
this.prune(at);
|
|
30
|
+
if (this.used.has(jti))
|
|
31
|
+
return false;
|
|
32
|
+
while (this.used.size >= this.maxEntries) {
|
|
33
|
+
const oldest = this.used.keys().next();
|
|
34
|
+
if (oldest.done)
|
|
35
|
+
break;
|
|
36
|
+
this.used.delete(oldest.value);
|
|
37
|
+
}
|
|
38
|
+
this.used.set(jti, at + RETENTION_MS);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
size() {
|
|
42
|
+
return this.used.size;
|
|
43
|
+
}
|
|
44
|
+
prune(at) {
|
|
45
|
+
for (const [jti, until] of this.used) {
|
|
46
|
+
if (until > at)
|
|
47
|
+
break; // insertion order = expiry order
|
|
48
|
+
this.used.delete(jti);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.UsedBridgeIds = UsedBridgeIds;
|
|
@@ -180,11 +180,15 @@ function parseSerializableRouteDescriptors(raw) {
|
|
|
180
180
|
throw new Error('addon-routes: route descriptor missing method/path');
|
|
181
181
|
}
|
|
182
182
|
const description = Reflect.get(entry, 'description');
|
|
183
|
+
// An unknown limit name is dropped, never widened into "no limit" silently
|
|
184
|
+
// by a typo on the hub side: only names the hub implements are kept.
|
|
185
|
+
const rateLimit = Reflect.get(entry, 'rateLimit') === 'auth' ? 'auth' : undefined;
|
|
183
186
|
return {
|
|
184
187
|
method: asRouteMethod(method),
|
|
185
188
|
path: routePath,
|
|
186
189
|
access: asRouteAccess(Reflect.get(entry, 'access')),
|
|
187
190
|
...(typeof description === 'string' ? { description } : {}),
|
|
191
|
+
...(rateLimit !== undefined ? { rateLimit } : {}),
|
|
188
192
|
};
|
|
189
193
|
});
|
|
190
194
|
}
|
|
@@ -2686,6 +2690,7 @@ class AddonRegistryService {
|
|
|
2686
2690
|
path: route.path,
|
|
2687
2691
|
access: route.access,
|
|
2688
2692
|
...(route.description !== undefined ? { description: route.description } : {}),
|
|
2693
|
+
...(route.rateLimit !== undefined ? { rateLimit: route.rateLimit } : {}),
|
|
2689
2694
|
handler: async (req, reply) => {
|
|
2690
2695
|
const envelope = await invoker.invoke({
|
|
2691
2696
|
method: route.method,
|
|
@@ -385,6 +385,19 @@ class AgentRegistryService {
|
|
|
385
385
|
tags: { nodeId },
|
|
386
386
|
meta: { historyOutcome, historyRemoved, registryRemoved, stillListed },
|
|
387
387
|
});
|
|
388
|
+
// Topology is push-only (`cluster.topology-snapshot`). Without this event
|
|
389
|
+
// the Cluster page kept the forgotten row until the 30 s heartbeat — the
|
|
390
|
+
// operator had to refresh by hand. `TopologyEmitterService` listens for
|
|
391
|
+
// `AgentUnregistered` and re-emits immediately.
|
|
392
|
+
if (historyRemoved || registryRemoved) {
|
|
393
|
+
this.eventBus.emit({
|
|
394
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
395
|
+
timestamp: new Date(),
|
|
396
|
+
source: { type: 'core', id: 'agent-registry' },
|
|
397
|
+
category: types_1.EventCategory.AgentUnregistered,
|
|
398
|
+
data: { agentId: nodeId },
|
|
399
|
+
});
|
|
400
|
+
}
|
|
388
401
|
return {
|
|
389
402
|
forgotten: historyRemoved || registryRemoved,
|
|
390
403
|
historyRemoved,
|
|
@@ -448,6 +461,16 @@ class AgentRegistryService {
|
|
|
448
461
|
this.moleculer.setOnAgentRegistered((agentId) => {
|
|
449
462
|
void this.reconcileAgentAddons(agentId, 'node-registration');
|
|
450
463
|
});
|
|
464
|
+
// D802: the mesh `transition` ends only once every agent this hub has
|
|
465
|
+
// EVER seen has authenticated — offline ones included. `null` (store not
|
|
466
|
+
// wired or not answering) never counts as "no agents".
|
|
467
|
+
this.moleculer.setKnownAgentsProvider(async () => {
|
|
468
|
+
const store = this.historyStore;
|
|
469
|
+
if (!store)
|
|
470
|
+
return null;
|
|
471
|
+
const rows = await store.tryGetAll();
|
|
472
|
+
return rows === null ? null : rows.filter((r) => !r.isHub).map((r) => r.id);
|
|
473
|
+
});
|
|
451
474
|
this.broker.localBus.on('$node.connected', ({ node }) => {
|
|
452
475
|
const kind = classifyNode(node.id);
|
|
453
476
|
if (!kind)
|
|
@@ -1,7 +1,56 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AuthService = void 0;
|
|
3
|
+
exports.AuthService = exports.SessionRevokedError = void 0;
|
|
4
4
|
const system_1 = require("@camstack/system");
|
|
5
|
+
/**
|
|
6
|
+
* A session-grade token whose server-side session is gone (logged out,
|
|
7
|
+
* revoked, expired) or cannot be checked right now. Every caller of
|
|
8
|
+
* `verifyToken` already treats a throw as "not authenticated".
|
|
9
|
+
*/
|
|
10
|
+
class SessionRevokedError extends Error {
|
|
11
|
+
reason;
|
|
12
|
+
constructor(reason) {
|
|
13
|
+
super(`Session is no longer valid (${reason})`);
|
|
14
|
+
this.reason = reason;
|
|
15
|
+
this.name = 'SessionRevokedError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.SessionRevokedError = SessionRevokedError;
|
|
19
|
+
/** A v2 session token: string `userId`, boolean `isAdmin`, no `kind` (not a bridge/challenge). */
|
|
20
|
+
function isSessionGrade(payload) {
|
|
21
|
+
return (Reflect.get(payload, 'kind') === undefined &&
|
|
22
|
+
typeof payload.userId === 'string' &&
|
|
23
|
+
typeof payload.isAdmin === 'boolean');
|
|
24
|
+
}
|
|
5
25
|
class AuthService extends system_1.AuthManager {
|
|
26
|
+
sessionCheck = null;
|
|
27
|
+
/**
|
|
28
|
+
* Wire the login-session check (`hub-sessions.ts`). From then on EVERY
|
|
29
|
+
* `verifyToken` of a session-grade token — the tRPC boundary, addon routes,
|
|
30
|
+
* the session cookie, the OAuth authorize page, health, uploads — also
|
|
31
|
+
* requires its session to be live, so a revocation applies everywhere at
|
|
32
|
+
* once and no surface can forget to ask.
|
|
33
|
+
*/
|
|
34
|
+
setSessionCheck(check) {
|
|
35
|
+
this.sessionCheck = check;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Why the session behind `payload` is no longer valid, or `null` when it is
|
|
39
|
+
* (or when no check is wired). For a principal resolved earlier — a
|
|
40
|
+
* WebSocket re-checks its session on every call without re-verifying an
|
|
41
|
+
* access token that may have expired since connect.
|
|
42
|
+
*/
|
|
43
|
+
checkSession(payload) {
|
|
44
|
+
return this.sessionCheck === null ? null : this.sessionCheck(payload);
|
|
45
|
+
}
|
|
46
|
+
verifyToken(token) {
|
|
47
|
+
const payload = super.verifyToken(token);
|
|
48
|
+
if (this.sessionCheck !== null && isSessionGrade(payload)) {
|
|
49
|
+
const refusal = this.sessionCheck(payload);
|
|
50
|
+
if (refusal !== null)
|
|
51
|
+
throw new SessionRevokedError(refusal);
|
|
52
|
+
}
|
|
53
|
+
return payload;
|
|
54
|
+
}
|
|
6
55
|
}
|
|
7
56
|
exports.AuthService = AuthService;
|
|
@@ -79,14 +79,19 @@ class HandoffCodeService {
|
|
|
79
79
|
/** Mint a single-use code bound to `userId`. Throws when the pending
|
|
80
80
|
* store is full even after pruning expired entries (fail fast — a
|
|
81
81
|
* legitimate flow never has anywhere near this many in flight). */
|
|
82
|
-
create(userId) {
|
|
82
|
+
create(userId, options = {}) {
|
|
83
83
|
this.prune();
|
|
84
84
|
if (this.pending.size >= exports.HANDOFF_CODE_MAX_PENDING) {
|
|
85
85
|
throw new Error('Too many pending handoff codes — try again shortly');
|
|
86
86
|
}
|
|
87
87
|
const code = `${exports.HANDOFF_CODE_PREFIX}${crypto.randomBytes(32).toString('hex')}`;
|
|
88
88
|
const expiresAt = this.now() + this.ttlMs;
|
|
89
|
-
this.pending.set(hashCode(code), {
|
|
89
|
+
this.pending.set(hashCode(code), {
|
|
90
|
+
userId,
|
|
91
|
+
...(options.sessionId !== undefined ? { sessionId: options.sessionId } : {}),
|
|
92
|
+
purpose: options.purpose ?? 'renew',
|
|
93
|
+
expiresAt,
|
|
94
|
+
});
|
|
90
95
|
return { code, expiresAt };
|
|
91
96
|
}
|
|
92
97
|
/** Redeem a raw code. Consumes the entry on the FIRST attempt no
|
|
@@ -99,7 +104,8 @@ class HandoffCodeService {
|
|
|
99
104
|
this.pending.delete(hashCode(code));
|
|
100
105
|
if (this.now() > entry.expiresAt)
|
|
101
106
|
return null;
|
|
102
|
-
|
|
107
|
+
const { expiresAt: _expiresAt, ...grant } = entry;
|
|
108
|
+
return grant;
|
|
103
109
|
}
|
|
104
110
|
/** Number of not-yet-redeemed (possibly expired) codes. Test/diag aid. */
|
|
105
111
|
get pendingCount() {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HubMeshGuard = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The hub's mesh guard (D802) — one object owning everything that decides
|
|
6
|
+
* whether another node may talk to the hub:
|
|
7
|
+
*
|
|
8
|
+
* - the {@link MeshAuthenticator} (who proved what, on which connection);
|
|
9
|
+
* - the broker middleware built on it (installed at broker construction);
|
|
10
|
+
* - the System → Cluster status slot;
|
|
11
|
+
* - the automatic end of `transition` ({@link decideMeshAutoEnforce}), checked
|
|
12
|
+
* after every successful join and once a minute.
|
|
13
|
+
*
|
|
14
|
+
* Kept out of `moleculer.service.ts` so that file only wires it.
|
|
15
|
+
*/
|
|
16
|
+
const system_1 = require("@camstack/system");
|
|
17
|
+
const types_1 = require("@camstack/types");
|
|
18
|
+
/** How often `transition` is re-checked when nothing joins. */
|
|
19
|
+
const AUTO_ENFORCE_INTERVAL_MS = 60_000;
|
|
20
|
+
class HubMeshGuard {
|
|
21
|
+
ownNodeId;
|
|
22
|
+
logger;
|
|
23
|
+
authenticator;
|
|
24
|
+
startedAt = Date.now();
|
|
25
|
+
knownAgents = null;
|
|
26
|
+
listConnected = () => [];
|
|
27
|
+
timer = null;
|
|
28
|
+
evaluating = false;
|
|
29
|
+
lastWaitingFor = '';
|
|
30
|
+
constructor(ownNodeId, logger) {
|
|
31
|
+
this.ownNodeId = ownNodeId;
|
|
32
|
+
this.logger = logger;
|
|
33
|
+
this.authenticator = new system_1.MeshAuthenticator({
|
|
34
|
+
ownNodeId,
|
|
35
|
+
gate: system_1.getProcessClusterSecretGate,
|
|
36
|
+
mode: system_1.getProcessMeshAuthMode,
|
|
37
|
+
onAuthenticated: (nodeId) => {
|
|
38
|
+
this.logger.info('node authenticated to the mesh (challenge–response)', {
|
|
39
|
+
meta: { nodeId },
|
|
40
|
+
});
|
|
41
|
+
void this.evaluateAutoEnforce();
|
|
42
|
+
},
|
|
43
|
+
onTolerated: (nodeId, action) => {
|
|
44
|
+
this.logger.warn('node is calling the hub WITHOUT having authenticated — tolerated while System → ' +
|
|
45
|
+
'Cluster is in Transition. Enter the cluster secret in this agent.', { meta: { nodeId, firstCall: action } });
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
(0, system_1.setProcessMeshAuthStatusSource)(() => this.authenticator.snapshot());
|
|
49
|
+
}
|
|
50
|
+
/** The middleware the hub broker must be constructed with. */
|
|
51
|
+
middleware() {
|
|
52
|
+
return (0, system_1.createMeshAccessMiddleware)({
|
|
53
|
+
ownNodeId: this.ownNodeId,
|
|
54
|
+
authenticator: this.authenticator,
|
|
55
|
+
logger: {
|
|
56
|
+
warn: (message, meta) => this.logger.warn(message, meta ? { meta } : undefined),
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** Start the periodic auto-enforce check; `listConnected` reads the live node list. */
|
|
61
|
+
start(listConnected) {
|
|
62
|
+
this.listConnected = listConnected;
|
|
63
|
+
if (this.timer !== null)
|
|
64
|
+
return;
|
|
65
|
+
this.timer = setInterval(() => void this.evaluateAutoEnforce(), AUTO_ENFORCE_INTERVAL_MS);
|
|
66
|
+
this.timer.unref?.();
|
|
67
|
+
}
|
|
68
|
+
/** Wired by `AgentRegistryService`, which owns the cluster-node history. */
|
|
69
|
+
setKnownAgentsProvider(provider) {
|
|
70
|
+
this.knownAgents = provider;
|
|
71
|
+
}
|
|
72
|
+
/** A node's connection dropped. */
|
|
73
|
+
forget(nodeId) {
|
|
74
|
+
this.authenticator.forget(nodeId);
|
|
75
|
+
}
|
|
76
|
+
stop() {
|
|
77
|
+
if (this.timer !== null)
|
|
78
|
+
clearInterval(this.timer);
|
|
79
|
+
this.timer = null;
|
|
80
|
+
(0, system_1.setProcessMeshAuthStatusSource)(null);
|
|
81
|
+
}
|
|
82
|
+
/** Public for the spec; the timer and every join call it. */
|
|
83
|
+
async evaluateAutoEnforce() {
|
|
84
|
+
if (this.evaluating || (0, system_1.getProcessMeshAuthMode)() !== 'transition')
|
|
85
|
+
return;
|
|
86
|
+
this.evaluating = true;
|
|
87
|
+
try {
|
|
88
|
+
let knownAgents = null;
|
|
89
|
+
if (this.knownAgents !== null) {
|
|
90
|
+
try {
|
|
91
|
+
knownAgents = await this.knownAgents();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
knownAgents = null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const decision = (0, system_1.decideMeshAutoEnforce)({
|
|
98
|
+
mode: (0, system_1.getProcessMeshAuthMode)(),
|
|
99
|
+
gate: (0, system_1.getProcessClusterSecretGate)().kind,
|
|
100
|
+
uptimeMs: Date.now() - this.startedAt,
|
|
101
|
+
knownAgents,
|
|
102
|
+
connected: this.listConnected(),
|
|
103
|
+
toleratedEver: this.authenticator.everToleratedNodeIds(),
|
|
104
|
+
authenticatedEver: this.authenticator.everAuthenticatedNodeIds(),
|
|
105
|
+
ownNodeId: this.ownNodeId,
|
|
106
|
+
});
|
|
107
|
+
if (!decision.enforce) {
|
|
108
|
+
const waiting = decision.waitingFor.join(',');
|
|
109
|
+
if (waiting.length > 0 && waiting !== this.lastWaitingFor) {
|
|
110
|
+
this.logger.info('mesh transition: still waiting for agents to authenticate', {
|
|
111
|
+
meta: { waitingFor: decision.waitingFor },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
this.lastWaitingFor = waiting;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const done = await (0, system_1.requestProcessMeshEnforcement)(decision.reason);
|
|
118
|
+
if (done) {
|
|
119
|
+
this.logger.info('mesh authentication switched to ENFORCED', {
|
|
120
|
+
meta: { reason: decision.reason },
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
this.logger.warn('mesh auto-enforce check failed', { meta: { error: (0, types_1.errMsg)(err) } });
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
this.evaluating = false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
exports.HubMeshGuard = HubMeshGuard;
|
|
@@ -9,6 +9,7 @@ const types_1 = require("@camstack/types");
|
|
|
9
9
|
const cap_router_runtime_js_1 = require("../../api/trpc/cap-router-runtime.js");
|
|
10
10
|
const core_cap_bridge_js_1 = require("../../api/trpc/core-cap-bridge.js");
|
|
11
11
|
const agent_readiness_pull_js_1 = require("./agent-readiness-pull.js");
|
|
12
|
+
const hub_mesh_guard_js_1 = require("./hub-mesh-guard.js");
|
|
12
13
|
const cap_call_fn_js_1 = require("./cap-call-fn.js");
|
|
13
14
|
const cap_route_authority_js_1 = require("./cap-route-authority.js");
|
|
14
15
|
class MoleculerService {
|
|
@@ -23,6 +24,12 @@ class MoleculerService {
|
|
|
23
24
|
return this.broker;
|
|
24
25
|
}
|
|
25
26
|
logger;
|
|
27
|
+
/**
|
|
28
|
+
* D802 — who may talk to the hub over the mesh: the challenge–response join,
|
|
29
|
+
* the broker middleware that refuses / tolerates unauthenticated nodes and
|
|
30
|
+
* strips forged identities, and the automatic end of `transition`.
|
|
31
|
+
*/
|
|
32
|
+
meshGuard;
|
|
26
33
|
/**
|
|
27
34
|
* D3 authority: union of every node's manifest delivered via
|
|
28
35
|
* `$hub.registerNode`. Populated by `onRegisterNode` in `hubDeps`.
|
|
@@ -190,9 +197,12 @@ class MoleculerService {
|
|
|
190
197
|
// can answer, and the Moleculer namespace was never authentication (TCP
|
|
191
198
|
// ignores it; UDP discovery broadcasts it in clear). The secret gates
|
|
192
199
|
// `$hub.registerNode`, read from the keychain at call time.
|
|
200
|
+
this.meshGuard = new hub_mesh_guard_js_1.HubMeshGuard('hub', this.logging.createLogger('mesh-auth'));
|
|
193
201
|
const broker = (0, system_1.createBroker)({
|
|
194
202
|
nodeID: 'hub',
|
|
195
203
|
mode: 'hub',
|
|
204
|
+
// D802: every action and event another node sends passes the mesh gate.
|
|
205
|
+
middlewares: [this.meshGuard.middleware()],
|
|
196
206
|
logLevel: this.config.get('moleculer.logLevel') ?? 'warn',
|
|
197
207
|
...(tcpPort && !Number.isNaN(tcpPort) ? { tcpPort } : {}),
|
|
198
208
|
...(udpPort && !Number.isNaN(udpPort) ? { udpPort } : {}),
|
|
@@ -217,6 +227,23 @@ class MoleculerService {
|
|
|
217
227
|
setOnAgentRegistered(cb) {
|
|
218
228
|
this.onAgentRegisteredCb = cb;
|
|
219
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* D802: the cluster-node history, for the automatic end of the mesh
|
|
232
|
+
* `transition` — wired by `AgentRegistryService`, which owns the history.
|
|
233
|
+
*/
|
|
234
|
+
setKnownAgentsProvider(provider) {
|
|
235
|
+
this.meshGuard.setKnownAgentsProvider(provider);
|
|
236
|
+
}
|
|
237
|
+
/** Every node Moleculer currently lists as available (this hub included). */
|
|
238
|
+
listAvailableNodeIds() {
|
|
239
|
+
const registry = this.broker.registry;
|
|
240
|
+
try {
|
|
241
|
+
return (registry?.getNodeList?.({ onlyAvailable: true }) ?? []).map((n) => n.id);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
220
247
|
async onModuleInit() {
|
|
221
248
|
const logger = this.logging.createLogger('moleculer');
|
|
222
249
|
const hubDeps = {
|
|
@@ -270,6 +297,8 @@ class MoleculerService {
|
|
|
270
297
|
},
|
|
271
298
|
// System → Cluster, from the keychain (D797): off / on / unknown.
|
|
272
299
|
clusterSecretGate: system_1.getProcessClusterSecretGate,
|
|
300
|
+
// D802: challenge–response join + who may register.
|
|
301
|
+
meshAuth: this.meshGuard.authenticator,
|
|
273
302
|
};
|
|
274
303
|
const hubService = (0, system_1.createHubService)(hubDeps);
|
|
275
304
|
this.brokerSafe.createService(hubService);
|
|
@@ -613,6 +642,8 @@ class MoleculerService {
|
|
|
613
642
|
// the incarnation) and this call is refused if the id has been reclaimed;
|
|
614
643
|
// remote nodes register no owner at all, so they clean up as before.
|
|
615
644
|
this.removeNodeFromRegistry(node.id);
|
|
645
|
+
// D802: a node authenticates again on its next connection.
|
|
646
|
+
this.meshGuard.forget(node.id);
|
|
616
647
|
});
|
|
617
648
|
// D8 reverse-leg snapshot-reconcile: on every agent (re)connect, pull
|
|
618
649
|
// the agent's own readiness snapshot (`$agent-readiness.getSnapshot`)
|
|
@@ -659,8 +690,21 @@ class MoleculerService {
|
|
|
659
690
|
probe: (url, options) => this.streamProbe.probe(url, options),
|
|
660
691
|
probeField: (key, value) => this.streamProbe.probeField(key, value),
|
|
661
692
|
}));
|
|
693
|
+
// Release A: when system-config's MeshCaKeeper becomes ready it offers
|
|
694
|
+
// hub leaf material here; the transporter starts TLS on :6443 alongside
|
|
695
|
+
// clear :6000 so enrolled agents can dual-dial without splitting the cluster.
|
|
696
|
+
(0, system_1.setProcessMeshTlsEnabler)(async (material) => {
|
|
697
|
+
const tx = (0, system_1.getBrokerTlsTransporter)(this.broker);
|
|
698
|
+
if (tx === null) {
|
|
699
|
+
logger.warn('Mesh TLS material ready but broker transporter is not TlsTcpTransporter');
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
await tx.enableMeshTls(material);
|
|
703
|
+
logger.info('Mesh mTLS listener enabled (dual-port with clear TCP)');
|
|
704
|
+
});
|
|
662
705
|
await this.brokerSafe.start();
|
|
663
706
|
logger.info('Moleculer broker started (TCP transport)');
|
|
707
|
+
this.meshGuard.start(() => this.listAvailableNodeIds());
|
|
664
708
|
// Construct the CapRouteResolver now that both the broker and
|
|
665
709
|
// localChildRegistry are ready. The resolver reads live registry state
|
|
666
710
|
// via closure accessors on every call (not a frozen snapshot), so new
|
|
@@ -1290,6 +1334,8 @@ class MoleculerService {
|
|
|
1290
1334
|
}
|
|
1291
1335
|
}
|
|
1292
1336
|
async onModuleDestroy() {
|
|
1337
|
+
(0, system_1.setProcessMeshTlsEnabler)(null);
|
|
1338
|
+
this.meshGuard.stop();
|
|
1293
1339
|
if (this.agentReadinessPullTimer !== null) {
|
|
1294
1340
|
clearInterval(this.agentReadinessPullTimer);
|
|
1295
1341
|
this.agentReadinessPullTimer = null;
|