@camstack/server 1.2.487 → 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.
@@ -3,12 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createAgentMeshJoin = createAgentMeshJoin;
4
4
  /**
5
5
  * The agent's mesh join (D802): authenticate to the hub on every connection,
6
- * before anything else reaches it, and check that the hub is the hub.
6
+ * before anything else reaches it, and check that the hub is the hub — plus
7
+ * the agent's inbound mesh gate so only that proven hub (and this agent's own
8
+ * registered children) may call it.
7
9
  *
8
- * `MeshJoinClient` (in `@camstack/system`) holds the protocol and the call
9
- * gate; this file binds it to the agent's broker lifecycle:
10
- * - the hub connects → a join runs;
11
- * - the hub disconnects → back to `pending` (calls wait for the next join);
10
+ * `MeshJoinClient` (in `@camstack/system`) holds the outbound protocol and the
11
+ * call gate; `AgentMeshAuthenticator` + `createMeshAccessMiddleware` hold the
12
+ * inbound side. This file binds both to the agent's broker lifecycle:
13
+ * - the hub connects → a join runs; a settled mutual proof stamps the hub;
14
+ * - the hub disconnects → back to `pending` (calls wait for the next join)
15
+ * and the hub is forgotten on the inbound gate;
16
+ * - any other node disconnects → forgotten on the inbound gate;
12
17
  * - the hub says "not authenticated" (it restarted and lost us) → join again;
13
18
  * - the join settles `unavailable` (the hub cannot read its secret yet) →
14
19
  * retried every {@link REJOIN_INTERVAL_MS} while connected.
@@ -33,6 +38,18 @@ function createAgentMeshJoin(opts) {
33
38
  clearTimeout(rejoinTimer);
34
39
  rejoinTimer = null;
35
40
  };
41
+ const authenticator = new system_1.AgentMeshAuthenticator({
42
+ ownNodeId: opts.nodeId,
43
+ hubNodeId: HUB_NODE_ID,
44
+ hasSecret: () => {
45
+ const s = opts.secret();
46
+ return s !== null && s.length > 0;
47
+ },
48
+ onTolerated: (nodeId, action) => {
49
+ opts.log?.(`mesh inbound: node "${nodeId}" is calling without having authenticated — ` +
50
+ `tolerated while this agent has no cluster secret (first call: ${action})`);
51
+ },
52
+ });
36
53
  const client = new system_1.MeshJoinClient({
37
54
  nodeId: opts.nodeId,
38
55
  secret: opts.secret,
@@ -40,6 +57,7 @@ function createAgentMeshJoin(opts) {
40
57
  hubNodeId: HUB_NODE_ID,
41
58
  ...(opts.log !== undefined ? { log: opts.log } : {}),
42
59
  onSettled: (state) => {
60
+ authenticator.noteHubJoinState(state);
43
61
  opts.onState?.(state);
44
62
  clearRejoin();
45
63
  if (state === 'unavailable' && connected) {
@@ -55,11 +73,29 @@ function createAgentMeshJoin(opts) {
55
73
  };
56
74
  const rejoin = () => {
57
75
  client.reset();
76
+ authenticator.noteHubJoinState('pending');
58
77
  void runJoin();
59
78
  };
79
+ const joinMiddleware = client.middleware(rejoin);
80
+ const inboundMiddleware = (0, system_1.createMeshAccessMiddleware)({
81
+ ownNodeId: opts.nodeId,
82
+ authenticator,
83
+ logger: {
84
+ warn: (message, meta) => {
85
+ const detail = meta === undefined
86
+ ? ''
87
+ : ` ${Object.entries(meta)
88
+ .map(([k, v]) => `${k}=${String(v)}`)
89
+ .join(' ')}`;
90
+ opts.log?.(`${message}${detail}`);
91
+ },
92
+ },
93
+ });
60
94
  return {
61
95
  client,
62
- middleware: client.middleware(rejoin),
96
+ authenticator,
97
+ middleware: joinMiddleware,
98
+ middlewares: [inboundMiddleware, joinMiddleware],
63
99
  attach(broker) {
64
100
  broker.localBus.on('$node.connected', (data) => {
65
101
  if (nodeIdOf(data) !== HUB_NODE_ID)
@@ -68,11 +104,16 @@ function createAgentMeshJoin(opts) {
68
104
  rejoin();
69
105
  });
70
106
  broker.localBus.on('$node.disconnected', (data) => {
71
- if (nodeIdOf(data) !== HUB_NODE_ID)
107
+ const id = nodeIdOf(data);
108
+ if (id === undefined)
109
+ return;
110
+ authenticator.forget(id);
111
+ if (id !== HUB_NODE_ID)
72
112
  return;
73
113
  connected = false;
74
114
  clearRejoin();
75
115
  client.reset();
116
+ authenticator.noteHubJoinState('pending');
76
117
  });
77
118
  },
78
119
  stop: clearRejoin,
@@ -546,6 +546,10 @@ function createAgentService(deps) {
546
546
  if (!(0, system_1.clusterSecretMatches)(deps.expectedClusterSecretHash, params.clusterSecretHash)) {
547
547
  throw new moleculer_1.Errors.MoleculerError(`cluster secret mismatch — node "${params.nodeId}" rejected`, 403, system_1.CLUSTER_SECRET_MISMATCH_TYPE);
548
548
  }
549
+ const sender = typeof ctx.nodeID === 'string' ? ctx.nodeID : '';
550
+ if (sender.length > 0) {
551
+ deps.meshAuth?.noteChildRegistered(sender, deps.expectedClusterSecretHash !== undefined ? 'authenticated' : 'legacy');
552
+ }
549
553
  deps.onChildRegistered?.(params);
550
554
  return { ok: true };
551
555
  },
@@ -182,7 +182,7 @@ async function startAgent(configPath) {
182
182
  mode: 'agent',
183
183
  hubAddress: config.hubAddress,
184
184
  logLevel: config.logLevel,
185
- middlewares: [meshJoin.middleware],
185
+ middlewares: meshJoin.middlewares,
186
186
  });
187
187
  meshJoin.attach(broker);
188
188
  /**
@@ -219,7 +219,7 @@ async function startAgent(configPath) {
219
219
  mode: 'agent',
220
220
  hubAddress: fresh.hubAddress,
221
221
  logLevel: fresh.logLevel,
222
- middlewares: [meshJoin.middleware],
222
+ middlewares: meshJoin.middlewares,
223
223
  });
224
224
  meshJoin.attach(broker);
225
225
  // The boot-time `$node.connected` listeners belong to the old broker: let
@@ -520,6 +520,7 @@ async function startAgent(configPath) {
520
520
  triggerUpwardRegistration();
521
521
  },
522
522
  expectedClusterSecretHash: config.secret ? (0, system_1.hashClusterSecret)(config.secret) : undefined,
523
+ meshAuth: meshJoin.authenticator,
523
524
  installFromNpm: async (pkg, version) => {
524
525
  await installer.install(pkg, version);
525
526
  },
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerMeshEnrollRoute = registerMeshEnrollRoute;
4
+ const system_1 = require("@camstack/system");
5
+ function registerMeshEnrollRoute(fastify) {
6
+ fastify.post('/api/mesh/enroll', async (request, reply) => {
7
+ const caState = (0, system_1.getProcessMeshCaState)();
8
+ if (caState.state !== 'ready') {
9
+ return reply.status(503).send({
10
+ ok: false,
11
+ error: `mesh CA unavailable: ${caState.reason}`,
12
+ });
13
+ }
14
+ const body = request.body ?? {};
15
+ const result = await (0, system_1.enrollMeshNode)({ token: body.token ?? '', csr: body.csr ?? '' }, { ca: caState.ca });
16
+ if (!result.ok) {
17
+ return reply.status(result.status).send({ ok: false, error: result.error });
18
+ }
19
+ return reply.send({
20
+ ok: true,
21
+ cert: result.cert,
22
+ caCert: result.caCert,
23
+ nodeId: result.nodeId,
24
+ notAfter: result.notAfter,
25
+ serialNumber: result.serialNumber,
26
+ });
27
+ });
28
+ }
@@ -18,7 +18,10 @@
18
18
  * routes the call exactly like any addon-provided cap.
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE = exports.REMOTE_DENIED_NAMESPACES = void 0;
21
+ exports.REMOTE_PRIVILEGE_LOG_ONLY = exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE = exports.REMOTE_DENIED_NAMESPACES = void 0;
22
+ exports.setRemotePrivilegeLogSink = setRemotePrivilegeLogSink;
23
+ exports.__resetRemotePrivilegeObservationForTests = __resetRemotePrivilegeObservationForTests;
24
+ exports.observeRemotePrivilege = observeRemotePrivilege;
22
25
  exports.isCoreServiceCapName = isCoreServiceCapName;
23
26
  exports.describeMeshOrigin = describeMeshOrigin;
24
27
  exports.buildCoreCapService = buildCoreCapService;
@@ -72,6 +75,42 @@ exports.REMOTE_DENIED_NAMESPACES = new Set([
72
75
  ]);
73
76
  /** Moleculer error `type` of a bridged call another node may not make. */
74
77
  exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE = 'CORE_CAP_REMOTE_FORBIDDEN';
78
+ /**
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.
82
+ */
83
+ exports.REMOTE_PRIVILEGE_LOG_ONLY = true;
84
+ let remotePrivilegeLogSink = null;
85
+ /** Attach the warn sink used by {@link observeRemotePrivilege}. */
86
+ function setRemotePrivilegeLogSink(sink) {
87
+ remotePrivilegeLogSink = sink;
88
+ }
89
+ const remotePrivilegeObserved = new Set();
90
+ /** Clears dedupe state and the sink — tests only. */
91
+ function __resetRemotePrivilegeObservationForTests() {
92
+ remotePrivilegeObserved.clear();
93
+ remotePrivilegeLogSink = null;
94
+ }
95
+ function remotePrivilegeKey(nodeId, path) {
96
+ return `${nodeId}\0${path}`;
97
+ }
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;
105
+ const key = remotePrivilegeKey(nodeId, path);
106
+ if (remotePrivilegeObserved.has(key))
107
+ return;
108
+ remotePrivilegeObserved.add(key);
109
+ const message = `mesh privilege: node:${nodeId} called ${path} (log-only; still allowed)`;
110
+ remotePrivilegeLogSink?.warn(message, {
111
+ meta: { nodeId, path, principal: `node:${nodeId}` },
112
+ });
113
+ }
75
114
  /**
76
115
  * camelCase → kebab-case. Mirrors `toKebab` in `trpc-links.ts` so the
77
116
  * action name matches what `brokerTransportLink` derives from `op.path`.
@@ -220,6 +259,8 @@ function buildCoreCapService(appRouter, census = (0, http_request_census_singlet
220
259
  if (remoteNode !== null && exports.REMOTE_DENIED_NAMESPACES.has(namespace)) {
221
260
  throw new moleculer_1.Errors.MoleculerClientError(`"${path}" is not callable from another node (caller "${remoteNode}")`, 403, exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE);
222
261
  }
262
+ if (remoteNode !== null)
263
+ observeRemotePrivilege(remoteNode, path);
223
264
  const invoke = resolveInvoker(callerFor(remoteNode === null ? 'mesh' : `node:${remoteNode}`), path);
224
265
  if (invoke === null)
225
266
  throw new Error(`$core-caps: "${path}" is not invocable`);
@@ -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,
@@ -690,6 +690,18 @@ class MoleculerService {
690
690
  probe: (url, options) => this.streamProbe.probe(url, options),
691
691
  probeField: (key, value) => this.streamProbe.probeField(key, value),
692
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
+ });
693
705
  await this.brokerSafe.start();
694
706
  logger.info('Moleculer broker started (TCP transport)');
695
707
  this.meshGuard.start(() => this.listAvailableNodeIds());
@@ -1322,6 +1334,7 @@ class MoleculerService {
1322
1334
  }
1323
1335
  }
1324
1336
  async onModuleDestroy() {
1337
+ (0, system_1.setProcessMeshTlsEnabler)(null);
1325
1338
  this.meshGuard.stop();
1326
1339
  if (this.agentReadinessPullTimer !== null) {
1327
1340
  clearInterval(this.agentReadinessPullTimer);
package/dist/main.js CHANGED
@@ -59,6 +59,7 @@ const ws_1 = require("@trpc/server/adapters/ws");
59
59
  const ws_2 = require("ws");
60
60
  const addon_upload_1 = require("./api/addon-upload");
61
61
  const auth_whoami_1 = require("./api/auth-whoami");
62
+ const enroll_routes_js_1 = require("./api/mesh/enroll.routes.js");
62
63
  const backup_download_js_1 = require("./api/backup-download.js");
63
64
  const cap_providers_1 = require("./api/core/cap-providers");
64
65
  const logging_settings_1 = require("./api/core/logging-settings");
@@ -457,6 +458,7 @@ async function bootstrap() {
457
458
  // Mirrors the addon-upload auth chain so the CLI can ping for
458
459
  // token-still-valid without consuming a real cap.
459
460
  await (0, auth_whoami_1.registerAuthWhoamiRoute)(fastify, uploadAuthService, uploadAddonRegistry);
461
+ (0, enroll_routes_js_1.registerMeshEnrollRoute)(fastify);
460
462
  console.log('[bootstrap] Auth whoami route registered at GET /api/auth/whoami');
461
463
  }
462
464
  catch (err) {
@@ -715,7 +717,9 @@ async function bootstrap() {
715
717
  // an operator armed earlier. The logger is attached now so a restored census
716
718
  // has somewhere to report before the first request arrives.
717
719
  const httpRequestCensus = (0, http_request_census_singleton_1.getHttpRequestCensus)();
718
- httpRequestCensus.setLogger(loggingService.createLogger('tRPC'));
720
+ const trpcLogger = loggingService.createLogger('tRPC');
721
+ httpRequestCensus.setLogger(trpcLogger);
722
+ (0, core_cap_bridge_1.setRemotePrivilegeLogSink)(trpcLogger);
719
723
  try {
720
724
  const authService = app.get(auth_service_1.AuthService);
721
725
  // Normally already built and bridged onto the mesh in `beforeAddonBoot`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.487",
3
+ "version": "1.2.488",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "files": [
@@ -37,16 +37,16 @@
37
37
  ]
38
38
  },
39
39
  "dependencies": {
40
- "@camstack/addon-admin-ui": "1.2.360",
40
+ "@camstack/addon-admin-ui": "1.2.361",
41
41
  "@camstack/addon-agent-ui": "1.2.196",
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.299",
46
+ "@camstack/addon-pipeline-orchestrator": "1.2.300",
47
47
  "@camstack/addon-post-analysis": "1.2.348",
48
48
  "@camstack/sdk": "1.2.128",
49
- "@camstack/system": "1.2.382",
49
+ "@camstack/system": "1.2.383",
50
50
  "@camstack/types": "1.2.333",
51
51
  "@camstack/ui-library": "1.2.254",
52
52
  "@fastify/compress": "^9.0.0",