@camstack/server 1.2.488 → 1.2.490

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.
@@ -81,6 +81,20 @@ function ensurePersistedNodeId(configPath, dataDir) {
81
81
  }
82
82
  return id;
83
83
  }
84
+ function parseMeshTls(value) {
85
+ if (value === null || typeof value !== 'object')
86
+ return undefined;
87
+ const certPem = Reflect.get(value, 'certPem');
88
+ const keyPem = Reflect.get(value, 'keyPem');
89
+ const caCertPem = Reflect.get(value, 'caCertPem');
90
+ if (typeof certPem !== 'string' || certPem.length === 0)
91
+ return undefined;
92
+ if (typeof keyPem !== 'string' || keyPem.length === 0)
93
+ return undefined;
94
+ if (typeof caCertPem !== 'string' || caCertPem.length === 0)
95
+ return undefined;
96
+ return { certPem, keyPem, caCertPem };
97
+ }
84
98
  function loadAgentConfig(configPath, dataDirOverride) {
85
99
  const filePath = configPath ?? 'agent.json';
86
100
  const dataDir = dataDirOverride ?? process.env.CAMSTACK_DATA ?? './camstack-data';
@@ -100,12 +114,17 @@ function loadAgentConfig(configPath, dataDirOverride) {
100
114
  let fileHubAddress;
101
115
  let fileName;
102
116
  let fileSecret;
117
+ let fileMeshTls;
118
+ let filePendingJoinToken;
103
119
  if (fs.existsSync(configFullPath)) {
104
120
  try {
105
121
  const raw = JSON.parse(fs.readFileSync(configFullPath, 'utf-8'));
106
122
  fileHubAddress = typeof raw.hubAddress === 'string' ? raw.hubAddress : undefined;
107
123
  fileName = typeof raw.name === 'string' ? raw.name : undefined;
108
124
  fileSecret = typeof raw.secret === 'string' ? raw.secret : undefined;
125
+ fileMeshTls = parseMeshTls(raw.meshTls);
126
+ filePendingJoinToken =
127
+ typeof raw.joinToken === 'string' && raw.joinToken.length > 0 ? raw.joinToken : undefined;
109
128
  }
110
129
  catch {
111
130
  /* corrupt config */
@@ -123,6 +142,8 @@ function loadAgentConfig(configPath, dataDirOverride) {
123
142
  addonsDir: path.resolve(resolvedDataDir, 'addons'),
124
143
  logLevel: process.env.CAMSTACK_LOG_LEVEL ?? 'info',
125
144
  secret: effectiveSecret,
145
+ ...(fileMeshTls !== undefined ? { meshTls: fileMeshTls } : {}),
146
+ ...(filePendingJoinToken !== undefined ? { pendingJoinToken: filePendingJoinToken } : {}),
126
147
  configPath: configFullPath,
127
148
  statusPort: Number(process.env.CAMSTACK_STATUS_PORT) || 4444,
128
149
  };
@@ -54,6 +54,7 @@ const fs = __importStar(require("node:fs"));
54
54
  const path = __importStar(require("node:path"));
55
55
  const fastify_1 = __importDefault(require("fastify"));
56
56
  const agent_http_auth_js_1 = require("./agent-http-auth.js");
57
+ const agent_mesh_enroll_js_1 = require("./agent-mesh-enroll.js");
57
58
  const agent_update_routes_js_1 = require("./agent-update-routes.js");
58
59
  const derive_hub_url_js_1 = require("./derive-hub-url.js");
59
60
  Object.defineProperty(exports, "pickIpv4", { enumerable: true, get: function () { return derive_hub_url_js_1.pickIpv4; } });
@@ -211,8 +212,12 @@ function getEffectiveConfig(configPath, nodeId) {
211
212
  name: typeof raw.name === 'string' ? raw.name : nodeId,
212
213
  hubAddress: typeof raw.hubAddress === 'string' ? raw.hubAddress : null,
213
214
  hasSecret: typeof raw.secret === 'string' && raw.secret.length > 0,
215
+ hasMeshTls: raw.meshTls !== null && typeof raw.meshTls === 'object',
214
216
  };
215
217
  }
218
+ /** Persisted fields that are credentials: never echoed by `GET /api/agent/config`. */
219
+ const NEVER_ECHOED_CONFIG_KEYS = new Set(['secret', 'meshTls', 'joinToken']);
220
+ const defaultRedeemJoinToken = (token, nodeId) => (0, agent_mesh_enroll_js_1.enrollAgentMesh)({ token, nodeId });
216
221
  // ---------------------------------------------------------------------------
217
222
  // Factory
218
223
  // ---------------------------------------------------------------------------
@@ -288,6 +293,11 @@ async function createAgentHttpServer(getBroker, config) {
288
293
  const hubConnectionState = config.getHubConnectionState
289
294
  ? config.getHubConnectionState()
290
295
  : deriveHubConnectionState(nodes, discoveryMode);
296
+ const meshTransport = config.getMeshTransport
297
+ ? config.getMeshTransport()
298
+ : eff.hasMeshTls
299
+ ? 'tls'
300
+ : 'clear';
291
301
  // The address the agent actually reaches the hub at (from the Moleculer
292
302
  // registry) — lets the UI show the real endpoint even under UDP discovery
293
303
  // where `eff.hubAddress` is null.
@@ -308,6 +318,7 @@ async function createAgentHttpServer(getBroker, config) {
308
318
  hubConnectionState,
309
319
  discoveryMode,
310
320
  hasSecret: eff.hasSecret,
321
+ meshTransport,
311
322
  discoveredNodes,
312
323
  };
313
324
  }
@@ -322,6 +333,7 @@ async function createAgentHttpServer(getBroker, config) {
322
333
  hubConnectionState,
323
334
  discoveryMode,
324
335
  hasSecret: eff.hasSecret,
336
+ meshTransport,
325
337
  discoveredNodes,
326
338
  addons: [],
327
339
  localIps: [],
@@ -360,21 +372,54 @@ async function createAgentHttpServer(getBroker, config) {
360
372
  name: eff.name,
361
373
  hubAddress: eff.hubAddress,
362
374
  hasSecret: eff.hasSecret,
375
+ hasMeshTls: eff.hasMeshTls,
363
376
  configPath: config.configPath,
364
377
  dataDir: config.dataDir,
365
- // Include all persisted fields except raw secret
366
- ...Object.fromEntries(Object.entries(persisted).filter(([k]) => k !== 'secret')),
378
+ // Include all persisted fields except credentials (secret, mesh key, pending token)
379
+ ...Object.fromEntries(Object.entries(persisted).filter(([k]) => !NEVER_ECHOED_CONFIG_KEYS.has(k))),
367
380
  };
368
381
  });
369
382
  // -- Config write (merge-patch + persist) ---------------------------
370
383
  // Fastify (not Express) natively awaits async route handlers and serializes
371
384
  // the returned value / catches rejections, so an async handler is correct here.
372
385
  // eslint-disable-next-line oxc/no-async-endpoint-handlers
373
- app.post('/api/agent/config', async (req) => {
374
- const patch = req.body ?? {};
386
+ app.post('/api/agent/config', async (req, reply) => {
387
+ // `joinToken` is redeemed, never stored; `meshTls` only ever comes from an
388
+ // enrollment (or is cleared via `clearMeshTls`), never pasted as PEM.
389
+ const body = req.body ?? {};
390
+ const joinToken = body['joinToken'];
391
+ const clearMeshTls = body['clearMeshTls'] === true;
392
+ const patch = Object.fromEntries(Object.entries(body).filter(([k]) => k !== 'joinToken' && k !== 'meshTls' && k !== 'clearMeshTls'));
375
393
  const existing = readConfigFile(config.configPath);
394
+ // Join-token enrollment (mesh mTLS Release A): the hub address comes from
395
+ // the token and wins over a `hubAddress` posted in the same form. Nothing is
396
+ // written unless the whole enrollment verified.
397
+ let enrolled = null;
398
+ if (typeof joinToken === 'string' && joinToken.trim().length > 0) {
399
+ try {
400
+ enrolled = await (config.redeemJoinToken ?? defaultRedeemJoinToken)(joinToken, config.nodeId);
401
+ }
402
+ catch (err) {
403
+ const message = err instanceof Error ? err.message : String(err);
404
+ console.error(`[Agent] join-token enrollment failed: ${message}`);
405
+ return reply
406
+ .status(err instanceof agent_mesh_enroll_js_1.MeshEnrollError ? 400 : 502)
407
+ .send({ success: false, error: message });
408
+ }
409
+ }
376
410
  const merged = { ...existing, ...patch };
411
+ if (enrolled !== null) {
412
+ merged['hubAddress'] = enrolled.hubAddress;
413
+ merged['meshTls'] = enrolled.meshTls;
414
+ }
415
+ else if (clearMeshTls) {
416
+ delete merged['meshTls'];
417
+ }
377
418
  writeConfigFile(config.configPath, merged);
419
+ if (enrolled !== null) {
420
+ console.log(`[Agent] Enrolled in the mesh CA via join token — hub ${enrolled.hubAddress}, ` +
421
+ 'mutual TLS on next reconnect');
422
+ }
378
423
  // Apply name change immediately (no reconnect needed)
379
424
  if (typeof patch.name === 'string' && patch.name.trim()) {
380
425
  try {
@@ -390,10 +435,14 @@ async function createAgentHttpServer(getBroker, config) {
390
435
  // `patch.hubAddress !== undefined` was always true and produced
391
436
  // false-positive "Restart required" prompts on no-op saves.
392
437
  const reconnectRelevant = ['hubAddress', 'secret'];
393
- const needsReconnect = reconnectRelevant.some((k) => Object.prototype.hasOwnProperty.call(patch, k) && patch[k] !== existing[k]);
438
+ const needsReconnect = enrolled !== null ||
439
+ clearMeshTls ||
440
+ reconnectRelevant.some((k) => Object.prototype.hasOwnProperty.call(patch, k) && patch[k] !== existing[k]);
394
441
  return {
395
442
  success: true,
396
443
  restartRequired: needsReconnect,
444
+ ...(enrolled !== null ? { enrolled: true } : {}),
445
+ ...(clearMeshTls ? { clearedMeshTls: true } : {}),
397
446
  };
398
447
  });
399
448
  // -- Agent reconnect (applies new hub/secret config) ----------------
@@ -0,0 +1,318 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.MeshEnrollError = void 0;
37
+ exports.decodeJoinTokenClaims = decodeJoinTokenClaims;
38
+ exports.hubHostFromMeshAddress = hubHostFromMeshAddress;
39
+ exports.enrollAgentMesh = enrollAgentMesh;
40
+ exports.persistEnrollment = persistEnrollment;
41
+ exports.discardPendingJoinToken = discardPendingJoinToken;
42
+ exports.enrollAndPersist = enrollAndPersist;
43
+ /**
44
+ * The agent's side of mesh mTLS enrollment (Release A).
45
+ *
46
+ * The operator mints a single-use join token on the hub (System → Cluster →
47
+ * Add agent) and pastes it into this agent's setup. Redeeming it:
48
+ *
49
+ * 1. decode the token's claims WITHOUT trusting them (the agent holds no CA
50
+ * key yet): `hubUrl`, `hubMeshAddress`, `caFingerprint`, optional `nodeId`;
51
+ * 2. a token pinned to another node id is refused before anything is sent;
52
+ * 3. generate this node's own key pair; the private key never leaves it;
53
+ * 4. `POST <hubUrl>/api/mesh/enroll` with `{ token, csr }`. The hub's HTTPS
54
+ * certificate is NOT the mesh CA, so the TLS peer is not what is pinned:
55
+ * 5. the answer is trusted only if its CA certificate hashes to the token's
56
+ * `caFingerprint` (a MITM with another CA fails here), the token's ES256
57
+ * signature verifies against that CA, and the issued leaf is signed by it,
58
+ * carries this node id as CN and matches the private key;
59
+ * 6. only then are the material and the hub address persisted (`agent.json`,
60
+ * 0600) — the dial port is NOT stored: with a certificate the broker dials
61
+ * the TLS port, so the address keeps the host only.
62
+ */
63
+ const node_crypto_1 = require("node:crypto");
64
+ const fs = __importStar(require("node:fs"));
65
+ const http = __importStar(require("node:http"));
66
+ const https = __importStar(require("node:https"));
67
+ const path = __importStar(require("node:path"));
68
+ const system_1 = require("@camstack/system");
69
+ class MeshEnrollError extends Error {
70
+ constructor(message) {
71
+ super(message);
72
+ this.name = 'MeshEnrollError';
73
+ }
74
+ }
75
+ exports.MeshEnrollError = MeshEnrollError;
76
+ const ENROLL_PATH = '/api/mesh/enroll';
77
+ const ENROLL_TIMEOUT_MS = 15_000;
78
+ function base64urlJson(segment) {
79
+ return JSON.parse(Buffer.from(segment, 'base64url').toString('utf8'));
80
+ }
81
+ /** Decode the payload of a compact JWS. Untrusted: nothing here is verified. */
82
+ function decodeJoinTokenClaims(token) {
83
+ const parts = token.trim().split('.');
84
+ if (parts.length !== 3 || parts[1] === undefined) {
85
+ throw new MeshEnrollError('join token is malformed (expected a compact JWT)');
86
+ }
87
+ let payload;
88
+ try {
89
+ payload = base64urlJson(parts[1]);
90
+ }
91
+ catch {
92
+ throw new MeshEnrollError('join token payload is not valid JSON');
93
+ }
94
+ if (payload === null || typeof payload !== 'object') {
95
+ throw new MeshEnrollError('join token payload is not an object');
96
+ }
97
+ const hubUrl = Reflect.get(payload, 'hubUrl');
98
+ const hubMeshAddress = Reflect.get(payload, 'hubMeshAddress');
99
+ const caFingerprint = Reflect.get(payload, 'caFingerprint');
100
+ const nodeId = Reflect.get(payload, 'nodeId');
101
+ const exp = Reflect.get(payload, 'exp');
102
+ if (typeof hubUrl !== 'string' || hubUrl.length === 0) {
103
+ throw new MeshEnrollError('join token carries no hubUrl');
104
+ }
105
+ if (typeof hubMeshAddress !== 'string' || hubMeshAddress.length === 0) {
106
+ throw new MeshEnrollError('join token carries no hubMeshAddress');
107
+ }
108
+ if (typeof caFingerprint !== 'string' || caFingerprint.length === 0) {
109
+ throw new MeshEnrollError('join token carries no caFingerprint');
110
+ }
111
+ if (typeof exp !== 'number')
112
+ throw new MeshEnrollError('join token carries no expiry');
113
+ return {
114
+ hubUrl,
115
+ hubMeshAddress,
116
+ caFingerprint: normalizeFingerprint(caFingerprint),
117
+ ...(typeof nodeId === 'string' && nodeId.length > 0 ? { nodeId } : {}),
118
+ exp,
119
+ };
120
+ }
121
+ function normalizeFingerprint(fp) {
122
+ return fp.replace(/:/g, '').toLowerCase();
123
+ }
124
+ /** `host:6000` / `[::1]:6000` / `host` → the host, port dropped. */
125
+ function hubHostFromMeshAddress(address) {
126
+ const trimmed = address.trim();
127
+ const bracketed = /^(\[[^\]]+\])(?::\d+)?$/.exec(trimmed);
128
+ if (bracketed?.[1] !== undefined)
129
+ return bracketed[1];
130
+ const hostPort = /^([^:]+):\d+$/.exec(trimmed);
131
+ return hostPort?.[1] ?? trimmed;
132
+ }
133
+ function compactFingerprint(certPem) {
134
+ return (0, node_crypto_1.createHash)('sha256').update(new node_crypto_1.X509Certificate(certPem).raw).digest('hex');
135
+ }
136
+ /** ES256 compact-JWS signature check against the CA certificate's public key. */
137
+ function tokenSignatureValid(token, caCertPem) {
138
+ const parts = token.trim().split('.');
139
+ if (parts.length !== 3)
140
+ return false;
141
+ const [header, payload, signature] = parts;
142
+ if (header === undefined || payload === undefined || signature === undefined)
143
+ return false;
144
+ try {
145
+ return (0, node_crypto_1.verify)('sha256', Buffer.from(`${header}.${payload}`), { key: new node_crypto_1.X509Certificate(caCertPem).publicKey, dsaEncoding: 'ieee-p1363' }, Buffer.from(signature, 'base64url'));
146
+ }
147
+ catch {
148
+ return false;
149
+ }
150
+ }
151
+ /** Node `http(s)` POST. The hub's HTTPS leaf is not the mesh CA, so the CA pin below is the trust. */
152
+ const nodeEnrollFetch = (url, init) => new Promise((resolve, reject) => {
153
+ const target = new URL(url);
154
+ const request = (target.protocol === 'http:' ? http : https).request(target, {
155
+ method: init.method,
156
+ headers: { ...init.headers, 'content-length': Buffer.byteLength(init.body) },
157
+ timeout: ENROLL_TIMEOUT_MS,
158
+ ...(target.protocol === 'https:' ? { rejectUnauthorized: false } : {}),
159
+ }, (res) => {
160
+ const chunks = [];
161
+ res.on('data', (chunk) => chunks.push(chunk));
162
+ res.on('end', () => {
163
+ const raw = Buffer.concat(chunks).toString('utf8');
164
+ resolve({
165
+ status: res.statusCode ?? 0,
166
+ json: () => Promise.resolve(JSON.parse(raw)),
167
+ });
168
+ });
169
+ res.on('error', reject);
170
+ });
171
+ request.on('timeout', () => request.destroy(new Error(`enroll request timed out`)));
172
+ request.on('error', reject);
173
+ request.end(init.body);
174
+ });
175
+ function readAnswer(status, body) {
176
+ const record = body !== null && typeof body === 'object' ? body : {};
177
+ if (status !== 200 || Reflect.get(record, 'ok') !== true) {
178
+ const error = Reflect.get(record, 'error');
179
+ throw new MeshEnrollError(`hub refused the enrollment (HTTP ${status}): ${typeof error === 'string' ? error : 'no reason given'}`);
180
+ }
181
+ const cert = Reflect.get(record, 'cert');
182
+ const caCert = Reflect.get(record, 'caCert');
183
+ if (typeof cert !== 'string' || typeof caCert !== 'string') {
184
+ throw new MeshEnrollError('hub answered without a certificate and CA certificate');
185
+ }
186
+ return { cert, caCert };
187
+ }
188
+ /**
189
+ * Redeem a join token. Throws {@link MeshEnrollError} with an operator-readable
190
+ * reason; nothing is persisted here.
191
+ */
192
+ async function enrollAgentMesh(input) {
193
+ const token = input.token.trim();
194
+ const claims = decodeJoinTokenClaims(token);
195
+ const nowSec = input.nowSec ?? Math.floor(Date.now() / 1000);
196
+ if (claims.exp <= nowSec) {
197
+ throw new MeshEnrollError('join token has expired — mint a new one on the hub');
198
+ }
199
+ if (claims.nodeId !== undefined && claims.nodeId !== input.nodeId) {
200
+ throw new MeshEnrollError(`join token is for node "${claims.nodeId}", but this agent is "${input.nodeId}"`);
201
+ }
202
+ const keys = await (0, system_1.generateMeshKeyPair)();
203
+ const csr = await (0, system_1.createMeshCsrPem)(keys, input.nodeId);
204
+ const keyPem = await (0, system_1.exportMeshPrivateKeyPem)(keys.privateKey);
205
+ const url = `${claims.hubUrl.replace(/\/+$/, '')}${ENROLL_PATH}`;
206
+ const fetchFn = input.fetchFn ?? nodeEnrollFetch;
207
+ let response;
208
+ try {
209
+ response = await fetchFn(url, {
210
+ method: 'POST',
211
+ headers: { 'content-type': 'application/json' },
212
+ body: JSON.stringify({ token, csr }),
213
+ });
214
+ }
215
+ catch (err) {
216
+ throw new MeshEnrollError(`could not reach the hub at ${claims.hubUrl}: ${err instanceof Error ? err.message : String(err)}`);
217
+ }
218
+ let body;
219
+ try {
220
+ body = await response.json();
221
+ }
222
+ catch {
223
+ body = null;
224
+ }
225
+ const answer = readAnswer(response.status, body);
226
+ // Trust nothing the hub said until it proves it holds the CA the token names.
227
+ let fingerprint;
228
+ try {
229
+ fingerprint = compactFingerprint(answer.caCert);
230
+ }
231
+ catch {
232
+ throw new MeshEnrollError('hub returned an unreadable CA certificate');
233
+ }
234
+ if (fingerprint !== claims.caFingerprint) {
235
+ throw new MeshEnrollError('the CA certificate returned by the hub does not match the token fingerprint — ' +
236
+ 'enrollment aborted (is something between this agent and the hub?)');
237
+ }
238
+ if (!tokenSignatureValid(token, answer.caCert)) {
239
+ throw new MeshEnrollError('join token signature does not verify against the hub CA');
240
+ }
241
+ assertLeafIsOurs(answer, keyPem, input.nodeId);
242
+ return {
243
+ meshTls: { certPem: answer.cert, keyPem, caCertPem: answer.caCert },
244
+ hubAddress: hubHostFromMeshAddress(claims.hubMeshAddress),
245
+ claims,
246
+ };
247
+ }
248
+ function assertLeafIsOurs(answer, keyPem, nodeId) {
249
+ let leaf;
250
+ try {
251
+ leaf = new node_crypto_1.X509Certificate(answer.cert);
252
+ }
253
+ catch {
254
+ throw new MeshEnrollError('hub returned an unreadable node certificate');
255
+ }
256
+ const ca = new node_crypto_1.X509Certificate(answer.caCert);
257
+ if (!leaf.verify(ca.publicKey)) {
258
+ throw new MeshEnrollError('the node certificate is not signed by the hub CA');
259
+ }
260
+ if (!new RegExp(`(?:^|\\n)CN=${escapeRegExp(nodeId)}(?:\\n|$)`).test(leaf.subject)) {
261
+ throw new MeshEnrollError(`the node certificate is not issued for "${nodeId}"`);
262
+ }
263
+ if (!leaf.checkPrivateKey((0, node_crypto_1.createPrivateKey)(keyPem))) {
264
+ throw new MeshEnrollError('the node certificate does not match this agent’s private key');
265
+ }
266
+ }
267
+ function escapeRegExp(value) {
268
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
269
+ }
270
+ function readJsonFile(file) {
271
+ if (!fs.existsSync(file))
272
+ return {};
273
+ try {
274
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
275
+ return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
276
+ ? Object.fromEntries(Object.entries(parsed))
277
+ : {};
278
+ }
279
+ catch {
280
+ return {};
281
+ }
282
+ }
283
+ function withoutJoinToken(record) {
284
+ return Object.fromEntries(Object.entries(record).filter(([key]) => key !== 'joinToken'));
285
+ }
286
+ function writeJsonFile0600(file, data) {
287
+ fs.mkdirSync(path.dirname(file), { recursive: true });
288
+ const tmp = `${file}.tmp`;
289
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2), { encoding: 'utf-8', mode: 0o600 });
290
+ fs.renameSync(tmp, file);
291
+ fs.chmodSync(file, 0o600);
292
+ }
293
+ /**
294
+ * Write the enrollment into `agent.json` (0600): `meshTls` + the host-only
295
+ * `hubAddress`; a pending `joinToken` is removed. The cluster secret, if any,
296
+ * stays — it is still a valid join proof for one release.
297
+ */
298
+ function persistEnrollment(configPath, enrolled) {
299
+ const next = {
300
+ ...withoutJoinToken(readJsonFile(configPath)),
301
+ hubAddress: enrolled.hubAddress,
302
+ meshTls: enrolled.meshTls,
303
+ };
304
+ writeJsonFile0600(configPath, next);
305
+ }
306
+ /** Drop a pending `joinToken` from `agent.json` (consumed — or dead). */
307
+ function discardPendingJoinToken(configPath) {
308
+ const existing = readJsonFile(configPath);
309
+ if (!('joinToken' in existing))
310
+ return;
311
+ writeJsonFile0600(configPath, withoutJoinToken(existing));
312
+ }
313
+ /** Redeem `token` and persist the result. Throws {@link MeshEnrollError}. */
314
+ async function enrollAndPersist(input) {
315
+ const enrolled = await enrollAgentMesh(input);
316
+ persistEnrollment(input.configPath, enrolled);
317
+ return enrolled;
318
+ }
@@ -46,6 +46,7 @@ const addon_scan_js_1 = require("./addon-scan.js");
46
46
  const agent_addon_updates_js_1 = require("./agent-addon-updates.js");
47
47
  const agent_mesh_join_js_1 = require("./agent-mesh-join.js");
48
48
  const agent_config_js_1 = require("./agent-config.js");
49
+ const agent_mesh_enroll_js_1 = require("./agent-mesh-enroll.js");
49
50
  const agent_group_runner_js_1 = require("./agent-group-runner.js");
50
51
  const agent_http_js_1 = require("./agent-http.js");
51
52
  const agent_infra_capabilities_js_1 = require("./agent-infra-capabilities.js");
@@ -78,8 +79,32 @@ const DATA_NODEID_CAPS = new Set(types_1.ALL_CAPABILITY_DEFINITIONS.filter((c) =
78
79
  // ---------------------------------------------------------------------------
79
80
  // Main
80
81
  // ---------------------------------------------------------------------------
82
+ /**
83
+ * Load `agent.json`, first redeeming a join token the setup window left there
84
+ * (mesh mTLS Release A — the desktop setup runs before the agent does). A token
85
+ * is single-use, so it is dropped whether or not the hub accepted it; a refusal
86
+ * is logged loudly and the agent still boots (clear, or with the cluster secret).
87
+ */
88
+ async function loadAgentConfigRedeemingJoinToken(configPath) {
89
+ const loaded = (0, agent_config_js_1.loadAgentConfig)(configPath);
90
+ if (loaded.pendingJoinToken === undefined)
91
+ return loaded;
92
+ try {
93
+ const enrolled = await (0, agent_mesh_enroll_js_1.enrollAndPersist)({
94
+ token: loaded.pendingJoinToken,
95
+ nodeId: loaded.nodeId,
96
+ configPath: loaded.configPath,
97
+ });
98
+ console.log(`[Agent] Enrolled in the mesh CA (hub ${enrolled.hubAddress}) — dialing over mutual TLS`);
99
+ }
100
+ catch (err) {
101
+ console.error(`[Agent] Join-token enrollment failed — staying on the previous transport: ${(0, types_1.errMsg)(err)}`);
102
+ (0, agent_mesh_enroll_js_1.discardPendingJoinToken)(loaded.configPath);
103
+ }
104
+ return (0, agent_config_js_1.loadAgentConfig)(configPath, loaded.dataDir);
105
+ }
81
106
  async function startAgent(configPath) {
82
- const config = (0, agent_config_js_1.loadAgentConfig)(configPath);
107
+ const config = await loadAgentConfigRedeemingJoinToken(configPath);
83
108
  // Derive CAMSTACK_HUB_URL from the configured hub address so remote agents
84
109
  // no longer need a second, redundant env var set by hand (the cross-node
85
110
  // source trap). An explicit operator value ALWAYS wins — capture whether it
@@ -177,12 +202,17 @@ async function startAgent(configPath) {
177
202
  }
178
203
  },
179
204
  });
205
+ // Mesh mTLS (Release A): with a certificate the broker dials the hub's TLS
206
+ // port; without one it keeps dialing clear 6000.
207
+ let currentMeshTls = config.meshTls ?? null;
208
+ console.log(`[Agent] Mesh transport: ${currentMeshTls !== null ? 'tls' : 'clear'}`);
180
209
  let broker = (0, system_1.createBroker)({
181
210
  nodeID: config.nodeId,
182
211
  mode: 'agent',
183
212
  hubAddress: config.hubAddress,
184
213
  logLevel: config.logLevel,
185
214
  middlewares: meshJoin.middlewares,
215
+ ...(currentMeshTls !== null ? { meshTls: currentMeshTls } : {}),
186
216
  });
187
217
  meshJoin.attach(broker);
188
218
  /**
@@ -200,7 +230,8 @@ async function startAgent(configPath) {
200
230
  }
201
231
  // Reload config from file (UI may have written new hubAddress/secret)
202
232
  const fresh = (0, agent_config_js_1.loadAgentConfig)(undefined, config.dataDir);
203
- console.log(`[Agent] New config: hub=${fresh.hubAddress ?? 'discovery'}, secret=${fresh.secret ? 'yes' : 'none'}`);
233
+ console.log(`[Agent] New config: hub=${fresh.hubAddress ?? 'discovery'}, secret=${fresh.secret ? 'yes' : 'none'}, ` +
234
+ `mesh=${fresh.meshTls !== undefined ? 'tls' : 'clear'}`);
204
235
  // Re-derive CAMSTACK_HUB_URL for the discovery-mode → dashboard-configured
205
236
  // flow (config file freshly written a hubAddress). Explicit operator values
206
237
  // still win via the boot-captured `hubUrlWasExplicit` flag.
@@ -214,12 +245,14 @@ async function startAgent(configPath) {
214
245
  console.log(`[Agent] Derived CAMSTACK_HUB_URL=${freshHubUrl} from hub address "${fresh.hubAddress}"`);
215
246
  }
216
247
  currentSecret = fresh.secret ?? null;
248
+ currentMeshTls = fresh.meshTls ?? null;
217
249
  broker = (0, system_1.createBroker)({
218
250
  nodeID: config.nodeId,
219
251
  mode: 'agent',
220
252
  hubAddress: fresh.hubAddress,
221
253
  logLevel: fresh.logLevel,
222
254
  middlewares: meshJoin.middlewares,
255
+ ...(currentMeshTls !== null ? { meshTls: currentMeshTls } : {}),
223
256
  });
224
257
  meshJoin.attach(broker);
225
258
  // The boot-time `$node.connected` listeners belong to the old broker: let
@@ -854,6 +887,7 @@ async function startAgent(configPath) {
854
887
  configPath: config.configPath,
855
888
  onReconnect: reconnect,
856
889
  getHubConnectionState,
890
+ getMeshTransport: () => (currentMeshTls !== null ? 'tls' : 'clear'),
857
891
  updateControls,
858
892
  });
859
893
  // ── Phase 1: Load core infrastructure addons (in-process) ──
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RevokeSkipReasonSchema = exports.RevocableCredentialKindSchema = exports.REVOKE_CREDENTIALS_MAX = void 0;
4
+ exports.createRevokeCredentialsProcedures = createRevokeCredentialsProcedures;
5
+ const server_1 = require("@trpc/server");
6
+ const zod_1 = require("zod");
7
+ const hub_sessions_js_1 = require("../../auth/hub-sessions.js");
8
+ const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
9
+ /** Upper bound of one bulk call — a page of a table, several times over. */
10
+ exports.REVOKE_CREDENTIALS_MAX = 500;
11
+ /** What a bulk revoke can target. Each has a per-row revoke on the server. */
12
+ exports.RevocableCredentialKindSchema = zod_1.z.enum(['share-link', 'admin-token', 'login-session']);
13
+ exports.RevokeSkipReasonSchema = zod_1.z.enum(['current-session', 'not-active']);
14
+ const RevokeCredentialsInputSchema = zod_1.z
15
+ .object({
16
+ kind: exports.RevocableCredentialKindSchema,
17
+ ids: zod_1.z.array(zod_1.z.string().min(1)).min(1).max(exports.REVOKE_CREDENTIALS_MAX),
18
+ /** The count the operator confirmed. Must equal `ids.length`. */
19
+ confirmCount: zod_1.z.number().int().min(1),
20
+ })
21
+ .refine((input) => new Set(input.ids).size === input.ids.length, {
22
+ message: 'Each id may be listed once',
23
+ path: ['ids'],
24
+ });
25
+ const RevokeCredentialsOutputSchema = zod_1.z.object({
26
+ revoked: zod_1.z.array(zod_1.z.string()),
27
+ skipped: zod_1.z.array(zod_1.z.object({ id: zod_1.z.string(), reason: exports.RevokeSkipReasonSchema })),
28
+ });
29
+ /** Keep the audit row bounded; the ids beyond this are counted, not listed. */
30
+ const AUDIT_IDS_MAX_CHARS = 4000;
31
+ function assertSignedInAdminSession(user) {
32
+ if (user.isScoped === true ||
33
+ user.shareView !== undefined ||
34
+ user.credential !== undefined ||
35
+ user.adminTokenId !== undefined) {
36
+ throw new server_1.TRPCError({
37
+ code: 'FORBIDDEN',
38
+ message: 'Revoking credentials in bulk needs a signed-in admin session',
39
+ });
40
+ }
41
+ }
42
+ function requireInternal(registry) {
43
+ const internal = registry === null ? null : (0, hub_sessions_js_1.userManagementInternal)(registry);
44
+ if (internal === null) {
45
+ throw new server_1.TRPCError({
46
+ code: 'PRECONDITION_FAILED',
47
+ message: 'User management is not available',
48
+ });
49
+ }
50
+ return internal;
51
+ }
52
+ /** The per-row revoke for `kind`, bound to the caller. `null` = revoked. */
53
+ function revokerFor(kind, caller, deps) {
54
+ if (kind === 'share-link') {
55
+ const service = deps.shareTokens;
56
+ if (service === null) {
57
+ throw new server_1.TRPCError({ code: 'PRECONDITION_FAILED', message: 'Share links are not available' });
58
+ }
59
+ return async (id) => (await service.revoke({ id, callerUserId: caller.id, callerIsAdmin: true }))
60
+ ? null
61
+ : 'not-active';
62
+ }
63
+ const internal = requireInternal(deps.registry);
64
+ if (kind === 'admin-token') {
65
+ return async (id) => ((await internal.adminTokens.revoke(id)) ? null : 'not-active');
66
+ }
67
+ return async (id) => {
68
+ if (id === caller.sessionId)
69
+ return 'current-session';
70
+ if (internal.sessions.check(id).state !== 'active')
71
+ return 'not-active';
72
+ return (await internal.sessions.revoke(id, 'revoked-by-admin')) ? null : 'not-active';
73
+ };
74
+ }
75
+ function auditIds(ids) {
76
+ const joined = ids.join(',');
77
+ return joined.length <= AUDIT_IDS_MAX_CHARS ? joined : `${joined.slice(0, AUDIT_IDS_MAX_CHARS)}…`;
78
+ }
79
+ function createRevokeCredentialsProcedures(deps) {
80
+ return {
81
+ /** ADMIN — revoke exactly `ids` of one kind; see the module header. */
82
+ revokeCredentials: trpc_middleware_js_1.adminProcedure
83
+ .input(RevokeCredentialsInputSchema)
84
+ .output(RevokeCredentialsOutputSchema)
85
+ .mutation(async ({ ctx, input }) => {
86
+ assertSignedInAdminSession(ctx.user);
87
+ if (input.confirmCount !== input.ids.length) {
88
+ throw new server_1.TRPCError({
89
+ code: 'BAD_REQUEST',
90
+ message: `Confirmed ${input.confirmCount} but ${input.ids.length} were listed — nothing was revoked`,
91
+ });
92
+ }
93
+ const revokeOne = revokerFor(input.kind, ctx.user, deps);
94
+ const revoked = [];
95
+ const skipped = [];
96
+ // Sequential on purpose: the stores are single-writer, and a partial
97
+ // failure must leave an exact account of what already happened.
98
+ for (const id of input.ids) {
99
+ const reason = await revokeOne(id);
100
+ if (reason === null)
101
+ revoked.push(id);
102
+ else
103
+ skipped.push({ id, reason });
104
+ }
105
+ const internal = deps.registry === null ? null : (0, hub_sessions_js_1.userManagementInternal)(deps.registry);
106
+ if (internal !== null) {
107
+ const ip = deps.network.clientAddress(ctx.req).ip;
108
+ await internal.audit.record({
109
+ action: 'credentials-revoked',
110
+ userId: ctx.user.id,
111
+ ...(ip !== null ? { ip } : {}),
112
+ detail: {
113
+ kind: input.kind,
114
+ requested: input.ids.length,
115
+ revoked: revoked.length,
116
+ skipped: skipped.length,
117
+ ids: auditIds(revoked),
118
+ },
119
+ });
120
+ }
121
+ return { revoked, skipped };
122
+ }),
123
+ };
124
+ }
@@ -35,6 +35,7 @@ const principal_visibility_js_1 = require("../trpc/principal-visibility.js");
35
35
  const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
36
36
  const auth_limits_js_1 = require("./auth-limits.js");
37
37
  const auth_admin_tokens_procedures_js_1 = require("./auth-admin-tokens.procedures.js");
38
+ const auth_revoke_credentials_procedures_js_1 = require("./auth-revoke-credentials.procedures.js");
38
39
  const auth_reauth_procedures_js_1 = require("./auth-reauth.procedures.js");
39
40
  const auth_sessions_procedures_js_1 = require("./auth-sessions.procedures.js");
40
41
  /**
@@ -1080,6 +1081,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
1080
1081
  ...(0, auth_sessions_procedures_js_1.createSessionProcedures)({ registry, minter, credentialProcedure }),
1081
1082
  ...(0, auth_reauth_procedures_js_1.createReauthProcedures)({ auth, registry, moleculer, limiters }),
1082
1083
  ...(0, auth_admin_tokens_procedures_js_1.createAdminTokenProcedures)({ auth, registry, network }),
1084
+ ...(0, auth_revoke_credentials_procedures_js_1.createRevokeCredentialsProcedures)({ registry, shareTokens, network }),
1083
1085
  // ── External sign-in identities (D782) ────────────────────────
1084
1086
  //
1085
1087
  // A signed-in user links an OIDC identity to THEIR OWN account. The
@@ -245,6 +245,11 @@ function buildToastProvider(toastService, ctx) {
245
245
  },
246
246
  };
247
247
  }
248
+ /** `{ meshTransport }` for an online agent, `{}` when the hub cannot tell. */
249
+ function meshTransportField(nodeId) {
250
+ const meshTransport = (0, system_1.getProcessNodeMeshTransport)(nodeId);
251
+ return meshTransport === undefined ? {} : { meshTransport };
252
+ }
248
253
  function getLocalIps() {
249
254
  const interfaces = os.networkInterfaces();
250
255
  const ips = [];
@@ -411,6 +416,9 @@ async function computeTopologyInternal(agentRegistry, addonRegistry, getNodeRoot
411
416
  // (hub: the running @camstack/server; agents: their registerNode
412
417
  // manifest via HubNodeRegistry). Null when unknown.
413
418
  rootPackage: getNodeRootPackage?.(node.info.id, node.isHub) ?? null,
419
+ // Mesh mTLS Release A: observed by the hub's transporter (certificate CN),
420
+ // never self-reported by the agent. Hub row and unknown: no badge.
421
+ ...(node.isHub ? {} : meshTransportField(node.info.id)),
414
422
  };
415
423
  });
416
424
  // A2: union in OFFLINE rows for every persisted node no longer present in the
@@ -18,10 +18,11 @@
18
18
  * routes the call exactly like any addon-provided cap.
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.REMOTE_PRIVILEGE_LOG_ONLY = exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE = exports.REMOTE_DENIED_NAMESPACES = void 0;
21
+ exports.REMOTE_PRIVILEGE_LOG_ONLY = exports.REMOTE_PRIVILEGE_ALLOWLIST = exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE = exports.REMOTE_DENIED_NAMESPACES = void 0;
22
22
  exports.setRemotePrivilegeLogSink = setRemotePrivilegeLogSink;
23
23
  exports.__resetRemotePrivilegeObservationForTests = __resetRemotePrivilegeObservationForTests;
24
24
  exports.observeRemotePrivilege = observeRemotePrivilege;
25
+ exports.observeRemotePrivilegeRefused = observeRemotePrivilegeRefused;
25
26
  exports.isCoreServiceCapName = isCoreServiceCapName;
26
27
  exports.describeMeshOrigin = describeMeshOrigin;
27
28
  exports.buildCoreCapService = buildCoreCapService;
@@ -76,11 +77,24 @@ exports.REMOTE_DENIED_NAMESPACES = new Set([
76
77
  /** Moleculer error `type` of a bridged call another node may not make. */
77
78
  exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE = 'CORE_CAP_REMOTE_FORBIDDEN';
78
79
  /**
79
- * When true, a remote node's allowed bridged calls are logged once per
80
- * `(nodeId, path)` per process — still admin, not refused. Granting least
81
- * privilege waits on census; this is the log-only week before that.
80
+ * Hub-core procedure paths another node may call after {@link REMOTE_DENIED_NAMESPACES}.
81
+ * Seeded from armed transport census on the live hub (2026-10): remote agents
82
+ * (`mesh:<agent>`) called none of these; hub runners used this surface only.
82
83
  */
83
- exports.REMOTE_PRIVILEGE_LOG_ONLY = true;
84
+ exports.REMOTE_PRIVILEGE_ALLOWLIST = new Set([
85
+ 'addons.listCapabilityProviders',
86
+ 'nodes.topology',
87
+ 'integrations.getByAddonId',
88
+ 'addons.list',
89
+ 'system.getLoggingSettings',
90
+ 'addons.setCapabilityProviderEnabled',
91
+ 'system.getSiteLocation',
92
+ ]);
93
+ /**
94
+ * When true, remote bridged calls outside {@link REMOTE_PRIVILEGE_ALLOWLIST} are
95
+ * logged once per `(nodeId, path)` per process — still admin, not refused.
96
+ */
97
+ exports.REMOTE_PRIVILEGE_LOG_ONLY = false;
84
98
  let remotePrivilegeLogSink = null;
85
99
  /** Attach the warn sink used by {@link observeRemotePrivilege}. */
86
100
  function setRemotePrivilegeLogSink(sink) {
@@ -95,22 +109,31 @@ function __resetRemotePrivilegeObservationForTests() {
95
109
  function remotePrivilegeKey(nodeId, path) {
96
110
  return `${nodeId}\0${path}`;
97
111
  }
98
- /**
99
- * One warn per `(nodeId, path)` while {@link REMOTE_PRIVILEGE_LOG_ONLY} is on.
100
- * Call only after {@link REMOTE_DENIED_NAMESPACES} has passed.
101
- */
102
- function observeRemotePrivilege(nodeId, path) {
103
- if (!exports.REMOTE_PRIVILEGE_LOG_ONLY)
104
- return;
112
+ function warnRemotePrivilegeOnce(nodeId, path, message) {
105
113
  const key = remotePrivilegeKey(nodeId, path);
106
114
  if (remotePrivilegeObserved.has(key))
107
115
  return;
108
116
  remotePrivilegeObserved.add(key);
109
- const message = `mesh privilege: node:${nodeId} called ${path} (log-only; still allowed)`;
110
117
  remotePrivilegeLogSink?.warn(message, {
111
118
  meta: { nodeId, path, principal: `node:${nodeId}` },
112
119
  });
113
120
  }
121
+ /**
122
+ * One warn per `(nodeId, path)` while {@link REMOTE_PRIVILEGE_LOG_ONLY} is on.
123
+ * Call only after {@link REMOTE_DENIED_NAMESPACES} has passed.
124
+ */
125
+ function observeRemotePrivilege(nodeId, path) {
126
+ if (!exports.REMOTE_PRIVILEGE_LOG_ONLY)
127
+ return;
128
+ warnRemotePrivilegeOnce(nodeId, path, `mesh privilege: node:${nodeId} called ${path} (log-only; still allowed)`);
129
+ }
130
+ /**
131
+ * One warn per `(nodeId, path)` when enforcement refuses a path not on
132
+ * {@link REMOTE_PRIVILEGE_ALLOWLIST}. Call immediately before throwing.
133
+ */
134
+ function observeRemotePrivilegeRefused(nodeId, path) {
135
+ warnRemotePrivilegeOnce(nodeId, path, `mesh privilege: node:${nodeId} called ${path} (refused; not on grant)`);
136
+ }
114
137
  /**
115
138
  * camelCase → kebab-case. Mirrors `toKebab` in `trpc-links.ts` so the
116
139
  * action name matches what `brokerTransportLink` derives from `op.path`.
@@ -259,8 +282,15 @@ function buildCoreCapService(appRouter, census = (0, http_request_census_singlet
259
282
  if (remoteNode !== null && exports.REMOTE_DENIED_NAMESPACES.has(namespace)) {
260
283
  throw new moleculer_1.Errors.MoleculerClientError(`"${path}" is not callable from another node (caller "${remoteNode}")`, 403, exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE);
261
284
  }
262
- if (remoteNode !== null)
263
- observeRemotePrivilege(remoteNode, path);
285
+ if (remoteNode !== null) {
286
+ if (exports.REMOTE_PRIVILEGE_LOG_ONLY) {
287
+ observeRemotePrivilege(remoteNode, path);
288
+ }
289
+ else if (!exports.REMOTE_PRIVILEGE_ALLOWLIST.has(path)) {
290
+ observeRemotePrivilegeRefused(remoteNode, path);
291
+ throw new moleculer_1.Errors.MoleculerClientError(`"${path}" is not on the remote privilege grant (caller "${remoteNode}")`, 403, exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE);
292
+ }
293
+ }
264
294
  const invoke = resolveInvoker(callerFor(remoteNode === null ? 'mesh' : `node:${remoteNode}`), path);
265
295
  if (invoke === null)
266
296
  throw new Error(`$core-caps: "${path}" is not invocable`);
@@ -644,6 +644,8 @@ class MoleculerService {
644
644
  this.removeNodeFromRegistry(node.id);
645
645
  // D802: a node authenticates again on its next connection.
646
646
  this.meshGuard.forget(node.id);
647
+ // Release A: its next connection decides again whether it is `tls` or `clear`.
648
+ (0, system_1.getBrokerTlsTransporter)(this.broker)?.forgetPeer(node.id);
647
649
  });
648
650
  // D8 reverse-leg snapshot-reconcile: on every agent (re)connect, pull
649
651
  // the agent's own readiness snapshot (`$agent-readiness.getSnapshot`)
@@ -702,6 +704,27 @@ class MoleculerService {
702
704
  await tx.enableMeshTls(material);
703
705
  logger.info('Mesh mTLS listener enabled (dual-port with clear TCP)');
704
706
  });
707
+ // Release A cutover: System → Cluster reads each agent's transport and may
708
+ // close the clear port through this slot (never flipped by this wiring).
709
+ (0, system_1.setProcessMeshClearPortControl)({
710
+ listOnlineAgents: () => this.listAvailableNodeIds()
711
+ .filter((id) => id !== this.brokerSafe.nodeID && !id.includes('/'))
712
+ .map((nodeId) => ({
713
+ nodeId,
714
+ transport: (0, system_1.getBrokerTlsTransporter)(this.broker)?.peerTransport(nodeId) ?? 'clear',
715
+ })),
716
+ isTlsListening: () => (0, system_1.getBrokerTlsTransporter)(this.broker)?.isTlsListening() === true,
717
+ transportOf: (nodeId) => (0, system_1.getBrokerTlsTransporter)(this.broker)?.peerTransport(nodeId),
718
+ apply: async (mode) => {
719
+ const tx = (0, system_1.getBrokerTlsTransporter)(this.broker);
720
+ if (tx === null)
721
+ return;
722
+ if (mode === 'off')
723
+ tx.disableClearTransport();
724
+ else
725
+ await tx.enableClearTransport();
726
+ },
727
+ });
705
728
  await this.brokerSafe.start();
706
729
  logger.info('Moleculer broker started (TCP transport)');
707
730
  this.meshGuard.start(() => this.listAvailableNodeIds());
@@ -1335,6 +1358,7 @@ class MoleculerService {
1335
1358
  }
1336
1359
  async onModuleDestroy() {
1337
1360
  (0, system_1.setProcessMeshTlsEnabler)(null);
1361
+ (0, system_1.setProcessMeshClearPortControl)(null);
1338
1362
  this.meshGuard.stop();
1339
1363
  if (this.agentReadinessPullTimer !== null) {
1340
1364
  clearInterval(this.agentReadinessPullTimer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.488",
3
+ "version": "1.2.490",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "files": [
@@ -37,18 +37,18 @@
37
37
  ]
38
38
  },
39
39
  "dependencies": {
40
- "@camstack/addon-admin-ui": "1.2.361",
41
- "@camstack/addon-agent-ui": "1.2.196",
40
+ "@camstack/addon-admin-ui": "1.2.363",
41
+ "@camstack/addon-agent-ui": "1.2.198",
42
42
  "@camstack/addon-auth": "1.2.135",
43
43
  "@camstack/addon-decoder-nodeav": "1.2.122",
44
44
  "@camstack/addon-notifiers": "1.2.129",
45
45
  "@camstack/addon-pipeline": "1.2.347",
46
- "@camstack/addon-pipeline-orchestrator": "1.2.300",
46
+ "@camstack/addon-pipeline-orchestrator": "1.2.301",
47
47
  "@camstack/addon-post-analysis": "1.2.348",
48
48
  "@camstack/sdk": "1.2.128",
49
- "@camstack/system": "1.2.383",
50
- "@camstack/types": "1.2.333",
51
- "@camstack/ui-library": "1.2.254",
49
+ "@camstack/system": "1.2.384",
50
+ "@camstack/types": "1.2.334",
51
+ "@camstack/ui-library": "1.2.256",
52
52
  "@fastify/compress": "^9.0.0",
53
53
  "@fastify/cookie": "^11.0.2",
54
54
  "@fastify/cors": "^11.2.0",