@camstack/server 1.2.566 → 1.2.567

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.
@@ -10,7 +10,8 @@ exports.createAgentNodeCertificates = createAgentNodeCertificates;
10
10
  * leaves it) and keeps it in memory only. When the signed leaf arrives it is persisted
11
11
  * into `agent.json` (0600) BESIDE the old one (`meshTlsPrevious`) together with the
12
12
  * swap's state (`meshSwap`: which leaf, until when), then the agent reconnects through
13
- * the same `reconnect()` the UI uses, which rebuilds the broker from the file.
13
+ * the same `reconnect()` the UI uses, which replaces the broker transport from the file
14
+ * (the broker, its services and its child runners stay).
14
15
  *
15
16
  * A swap is PENDING from install until the hub confirms. Only while it is pending may
16
17
  * the node go back, and only to a previous leaf that is still unexpired:
@@ -213,7 +213,7 @@ async function startAgent(configPath) {
213
213
  // The HTTP/HTTPS listener on the agent port (D856); set once it is up.
214
214
  let agentHttpListener = null;
215
215
  console.log(`[Agent] Mesh transport: ${currentMeshTls !== null ? 'tls' : 'clear'}`);
216
- let broker = (0, system_1.createBroker)({
216
+ const broker = (0, system_1.createBroker)({
217
217
  nodeID: config.nodeId,
218
218
  mode: 'agent',
219
219
  hubAddress: config.hubAddress,
@@ -223,32 +223,40 @@ async function startAgent(configPath) {
223
223
  });
224
224
  meshJoin.attach(broker);
225
225
  /**
226
- * Reconnect: stop the current broker, reload config from file,
227
- * create a new broker with the updated hub/secret, and restart.
228
- * The HTTP server stays alive throughout.
226
+ * Reconnect: reload the config from file and replace the broker's TRANSPORT (the new hub
227
+ * address, secret or mesh leaf) IN PLACE. The broker itself is never rebuilt: its services
228
+ * (`$agent`, `$process`, `$agent-cap-fwd`, the event bus, the addon services), its registries and
229
+ * the forked child runners all outlive the wire. Rebuilding the broker stopped `$process`, which
230
+ * SIGTERMed every runner, and left the node capless with no `$agent-cap-fwd` (D856 amendment,
231
+ * 2026-10-15 incident). The HTTP server stays alive throughout.
229
232
  */
230
233
  // ONE reconnect at a time (serial-queue.ts): the UI, a mesh leaf swap and its rollback can
231
- // all ask for one, and two interleaved stop/rebuild cycles would leave two brokers or none.
234
+ // all ask for one, and two interleaved disconnect/connect cycles would leave two wires or none.
232
235
  const reconnect = (0, serial_queue_js_1.createSerialQueue)(() => reconnectNow(), {
233
236
  deadlineMs: RECONNECT_DEADLINE_MS,
234
237
  onTimeout: () => {
235
- // A broker whose start() never settles must not hold every later reconnect (including
236
- // the rollback of a failed leaf swap): tear down what was half-built and move on.
237
- consoleLogger.error('Reconnect did not finish in time - destroying the half-built broker', {
238
+ // A transport whose connect never settles must not hold every later reconnect (including
239
+ // the rollback of a failed leaf swap): drop the wire and move on. The broker, and the
240
+ // runners behind it, stay.
241
+ consoleLogger.error('Reconnect did not finish in time - dropping the half-connected transport', {
238
242
  meta: { deadlineMs: RECONNECT_DEADLINE_MS },
239
243
  });
240
- void broker.stop().catch(() => undefined);
244
+ void (0, system_1.disconnectBrokerTransport)(broker).catch(() => undefined);
241
245
  },
242
246
  });
243
247
  // The cycle itself runs through a generation guard: a run the queue abandoned at its deadline
244
- // that resumes later builds nothing (agent-reconnect.ts), so two brokers never share a node id.
248
+ // that resumes later installs nothing (agent-reconnect.ts), so two transports never share a broker.
245
249
  let pendingConfig = null;
246
250
  const reconnectNow = (0, agent_reconnect_js_1.createReconnectRunner)({
247
- current: () => broker,
248
- assign: (next) => {
249
- broker = next;
251
+ current: () => (0, system_1.currentBrokerTransporter)(broker),
252
+ assign: (next) => (0, system_1.installBrokerTransporter)(broker, next),
253
+ stop: async (target) => {
254
+ // Only the transport installed right now: a late, abandoned run must not tear down a
255
+ // newer run's transport.
256
+ if ((0, system_1.isInstalledTransporter)(broker, target)) {
257
+ await (0, system_1.disconnectBrokerTransport)(broker);
258
+ }
250
259
  },
251
- stop: (target) => target.stop(),
252
260
  prepare: () => {
253
261
  console.log('[Agent] Reconnecting with updated config...');
254
262
  // Reload config from file (UI may have written new hubAddress/secret)
@@ -274,24 +282,21 @@ async function startAgent(configPath) {
274
282
  const fresh = pendingConfig;
275
283
  if (fresh === null)
276
284
  throw new Error('reconnect: no configuration was prepared');
277
- const next = (0, system_1.createBroker)({
285
+ return (0, system_1.buildTransporter)((0, system_1.createAgentTransport)({
278
286
  nodeID: config.nodeId,
279
287
  mode: 'agent',
280
288
  hubAddress: fresh.hubAddress,
281
289
  logLevel: fresh.logLevel,
282
- middlewares: meshJoin.middlewares,
283
290
  ...(currentMeshTls !== null ? { meshTls: currentMeshTls } : {}),
284
- });
285
- meshJoin.attach(next);
286
- return next;
291
+ }));
287
292
  },
288
- // The new broker's hub connection runs a join, and its settle re-sends
289
- // the manifest (`registration.onJoinSettled`).
290
- start: async (next) => {
291
- await next.start();
293
+ // The new wire's hub connection runs a join, and its settle re-sends the manifest
294
+ // (`registration.onJoinSettled`): the subtree union, every child included (D3).
295
+ start: async () => {
296
+ await (0, system_1.connectBrokerTransport)(broker);
292
297
  console.log('[Agent] Reconnected successfully');
293
298
  },
294
- onSuperseded: (stage) => consoleLogger.warn('A superseded reconnect stopped without building a second broker', {
299
+ onSuperseded: (stage) => consoleLogger.warn('A superseded reconnect stopped without installing a second transport', {
295
300
  meta: { stage },
296
301
  }),
297
302
  });
@@ -461,6 +461,7 @@ function createCertificatesRouter(deps) {
461
461
  crl: crl(),
462
462
  peers,
463
463
  registrations,
464
+ ...(deps.nodeAddonIds !== undefined ? { expectedAddons: deps.nodeAddonIds } : {}),
464
465
  log: deps.log,
465
466
  ...(deps.reconnectTimeoutMs !== undefined
466
467
  ? { reconnectTimeoutMs: deps.reconnectTimeoutMs }
@@ -310,6 +310,7 @@ function buildCapabilityRouters(services) {
310
310
  registry: services.capabilityRegistry,
311
311
  agentRegistry: services.agentRegistry,
312
312
  nodeClient: (nodeId) => (0, node_certificates_client_js_1.createNodeCertificatesClient)(services.moleculer, nodeId),
313
+ nodeAddonIds: (nodeId) => services.moleculer.getNodeAddonIds(nodeId),
313
314
  hubDataDir: path.resolve(services.configService.get('server.dataPath') ?? 'camstack-data'),
314
315
  log: services.loggingService.createLogger('certificates'),
315
316
  }),
@@ -929,9 +929,12 @@ class MoleculerService {
929
929
  else
930
930
  this.nodeOwners.set(params.nodeId, owner);
931
931
  this.applyNodeManifest(params, previousManifest, owner, previousOwner);
932
+ // The leaf this node presents NOW is recorded, and a renewal leaf it no longer presents (it
933
+ // reverted to its previous one) is set aside at once.
934
+ this.observePresentedMeshLeaf(params.nodeId);
932
935
  // The node's capabilities are routable now: whoever waits for this registration (a mesh
933
936
  // leaf renewal confirming the node came back WITH the new leaf) is told, with no polling.
934
- (0, system_1.emitProcessNodeRegistered)(params.nodeId);
937
+ (0, system_1.emitProcessNodeRegistered)(params.nodeId, params.addons.map((addon) => addon.addonId));
935
938
  // Notify AgentRegistryService to reconcile placement for bare-ID
936
939
  // agent nodes (no '/' = not a hub child worker, not the hub itself).
937
940
  // The handshake is the authoritative completeness signal — the full
@@ -1299,6 +1302,34 @@ class MoleculerService {
1299
1302
  isNodeRegistered(nodeId) {
1300
1303
  return this.nodeRegistry.getNodeManifest(nodeId) !== undefined;
1301
1304
  }
1305
+ /**
1306
+ * Record the mesh leaf `nodeId` presents on its live connection (D856). Fire and forget: a
1307
+ * registry that is not up, or a write that fails, must not fail the node's registration.
1308
+ */
1309
+ observePresentedMeshLeaf(nodeId) {
1310
+ const registry = (0, system_1.getProcessMeshCertificateRegistry)();
1311
+ const leaf = (0, system_1.getProcessPeerLeaf)(nodeId);
1312
+ if (registry === null || leaf === undefined)
1313
+ return;
1314
+ registry
1315
+ .recordObserved(nodeId, {
1316
+ cn: nodeId,
1317
+ serial: leaf.serial,
1318
+ fingerprint: leaf.fingerprint,
1319
+ notBefore: leaf.notBefore,
1320
+ notAfter: leaf.notAfter,
1321
+ })
1322
+ .catch((error) => {
1323
+ this.logger.warn('Could not record the mesh leaf the node presents', {
1324
+ tags: { nodeId },
1325
+ meta: { error: error instanceof Error ? error.message : String(error) },
1326
+ });
1327
+ });
1328
+ }
1329
+ /** The addon ids a node's registration holds (its own and its children's); empty when none. */
1330
+ getNodeAddonIds(nodeId) {
1331
+ return (this.nodeRegistry.getNodeManifest(nodeId) ?? []).map((entry) => entry.addonId);
1332
+ }
1302
1333
  /**
1303
1334
  * The root-package identity a node reported in its `registerNode`
1304
1335
  * manifest (runtime-updatable node packages: version visibility), or null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.566",
3
+ "version": "1.2.567",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "files": [
@@ -38,7 +38,7 @@
38
38
  ]
39
39
  },
40
40
  "dependencies": {
41
- "@camstack/addon-admin-ui": "1.2.420",
41
+ "@camstack/addon-admin-ui": "1.2.421",
42
42
  "@camstack/addon-agent-ui": "1.2.231",
43
43
  "@camstack/addon-auth": "1.2.144",
44
44
  "@camstack/addon-decoder-nodeav": "1.2.123",
@@ -48,8 +48,8 @@
48
48
  "@camstack/addon-post-analysis": "1.2.369",
49
49
  "@camstack/develop": "0.1.8",
50
50
  "@camstack/sdk": "1.2.131",
51
- "@camstack/system": "1.2.430",
52
- "@camstack/types": "1.2.366",
51
+ "@camstack/system": "1.2.431",
52
+ "@camstack/types": "1.2.367",
53
53
  "@camstack/ui-library": "1.2.288",
54
54
  "@fastify/compress": "^9.0.0",
55
55
  "@fastify/cookie": "^11.0.2",
@@ -60,7 +60,7 @@
60
60
  "@trpc/server": "^11.16.0",
61
61
  "fastify": "^5",
62
62
  "js-yaml": "^4",
63
- "moleculer": "^0.15.0",
63
+ "moleculer": "0.15.0",
64
64
  "node-av": "^6.0.0",
65
65
  "node-pty": "^1.0.0",
66
66
  "sharp": "^0.35.2",