@camstack/server 1.2.687 → 1.2.689

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/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.689] - 2026-10-29
11
+
12
+ ### Added
13
+
14
+ - A node's free memory comes from the metrics provider instead of os.freemem(), which undercounts on macOS
15
+ - device-export gains optional methods to list, expose and unexpose actions and notification targets beside devices.
16
+ - Forget node now works on an online node too: its certificates are revoked, its connection is cut and calls pinned to it fail at once.
17
+ - Generated routers for the pet-fountain capability
18
+ - Agents report their own process memory in the topology (the core process showed 0), and an unreadable memory shows as unknown
19
+
20
+ ## [1.2.688] - 2026-10-29
21
+
22
+ ### Changed
23
+
24
+ - Forgetting a node now clears its per-node capability overrides and tells every node-removal-hook owner
25
+ - Forgetting an offline node now revokes its mesh certificates, so it can no longer rejoin by itself; a revoked agent learns it was removed and resets to unpaired.
26
+
10
27
  ## [1.2.687] - 2026-10-29
11
28
 
12
29
  ### Changed
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AgentMembershipWatch = exports.DEFAULT_MEMBERSHIP_CONFIRM_DELAY_MS = exports.DEFAULT_MEMBERSHIP_ASK_INTERVAL_MS = exports.DEFAULT_MEMBERSHIP_GRACE_MS = void 0;
4
+ exports.DEFAULT_MEMBERSHIP_GRACE_MS = 90_000;
5
+ exports.DEFAULT_MEMBERSHIP_ASK_INTERVAL_MS = 60_000;
6
+ exports.DEFAULT_MEMBERSHIP_CONFIRM_DELAY_MS = 15_000;
7
+ /** `revoked` answers in a row that destroy the pairing. */
8
+ const REVOKED_READS_REQUIRED = 2;
9
+ class AgentMembershipWatch {
10
+ deps;
11
+ disconnectedSince = null;
12
+ lastAskAt = null;
13
+ revokedReads = 0;
14
+ asking = false;
15
+ constructor(deps) {
16
+ this.deps = deps;
17
+ }
18
+ /** One step. Resolves once any question this step asked has been handled. */
19
+ async tick(now) {
20
+ if (this.asking)
21
+ return;
22
+ if (!this.deps.isPaired() || this.deps.isConnected()) {
23
+ this.reset();
24
+ return;
25
+ }
26
+ this.disconnectedSince ??= now;
27
+ if (now - this.disconnectedSince < (this.deps.graceMs ?? exports.DEFAULT_MEMBERSHIP_GRACE_MS))
28
+ return;
29
+ const interval = this.revokedReads > 0
30
+ ? (this.deps.confirmDelayMs ?? exports.DEFAULT_MEMBERSHIP_CONFIRM_DELAY_MS)
31
+ : (this.deps.askIntervalMs ?? exports.DEFAULT_MEMBERSHIP_ASK_INTERVAL_MS);
32
+ if (this.lastAskAt !== null && now - this.lastAskAt < interval)
33
+ return;
34
+ this.lastAskAt = now;
35
+ this.asking = true;
36
+ try {
37
+ await this.handle(await this.deps.ask());
38
+ }
39
+ finally {
40
+ this.asking = false;
41
+ }
42
+ }
43
+ reset() {
44
+ this.disconnectedSince = null;
45
+ this.lastAskAt = null;
46
+ this.revokedReads = 0;
47
+ }
48
+ async handle(result) {
49
+ const tags = { nodeId: this.deps.nodeId };
50
+ if (result.kind === 'unreachable') {
51
+ this.revokedReads = 0;
52
+ this.deps.log.info('could not ask the hub whether this agent is still a member - keeping the pairing', {
53
+ tags,
54
+ meta: { error: result.error },
55
+ });
56
+ return;
57
+ }
58
+ const { status, reason } = result.answer;
59
+ if (status !== 'revoked') {
60
+ this.revokedReads = 0;
61
+ this.deps.log.info(`the hub answered "${status}" to the membership question - keeping the pairing`, {
62
+ tags,
63
+ });
64
+ return;
65
+ }
66
+ this.revokedReads += 1;
67
+ const why = reason ?? 'revoked';
68
+ if (this.revokedReads < REVOKED_READS_REQUIRED) {
69
+ this.deps.log.warn(`the hub says this agent's certificate is revoked (${why}) - asking once more before resetting`, {
70
+ tags,
71
+ meta: { reason: why },
72
+ });
73
+ return;
74
+ }
75
+ this.deps.log.warn(`the hub confirmed twice that this agent's certificate is revoked (${why}) - discarding the pairing and becoming pairable again`, { tags, meta: { reason: why } });
76
+ this.reset();
77
+ await this.deps.onRevoked(why);
78
+ }
79
+ }
80
+ exports.AgentMembershipWatch = AgentMembershipWatch;
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.isAgentPaired = isAgentPaired;
37
37
  exports.pinnedMeshCaCert = pinnedMeshCaCert;
38
38
  exports.persistPairing = persistPairing;
39
+ exports.readPairedHubIdentity = readPairedHubIdentity;
39
40
  exports.forgetPairing = forgetPairing;
40
41
  exports.pairedLeafFingerprint = pairedLeafFingerprint;
41
42
  /**
@@ -48,7 +49,13 @@ const fs = __importStar(require("node:fs"));
48
49
  const system_1 = require("@camstack/system");
49
50
  const agent_json_file_js_1 = require("./agent-json-file.js");
50
51
  /** Keys that exist only while this agent is paired. */
51
- const PAIRED_KEYS = ['meshTls', 'meshTlsPrevious', 'meshSwap', 'hubAddress'];
52
+ const PAIRED_KEYS = [
53
+ 'meshTls',
54
+ 'meshTlsPrevious',
55
+ 'meshSwap',
56
+ 'hubAddress',
57
+ 'hubUrl',
58
+ ];
52
59
  /** `agent.json` holds mesh material: the agent is paired. */
53
60
  function isAgentPaired(configPath) {
54
61
  const meshTls = (0, agent_json_file_js_1.readJsonFile)(configPath)['meshTls'];
@@ -72,9 +79,32 @@ function persistPairing(configPath, pairing) {
72
79
  (0, agent_json_file_js_1.writeJsonFile0600)(configPath, {
73
80
  ...existing,
74
81
  hubAddress: pairing.hubAddress,
82
+ // The hub's API address the hub itself gave: where a refused agent asks "am I still a member?".
83
+ hubUrl: pairing.hubUrl,
75
84
  meshTls: pairing.meshTls,
76
85
  });
77
86
  }
87
+ function readPairedHubIdentity(configPath) {
88
+ const config = (0, agent_json_file_js_1.readJsonFile)(configPath);
89
+ const meshTls = config['meshTls'];
90
+ if (meshTls === null || typeof meshTls !== 'object')
91
+ return null;
92
+ const certPem = Reflect.get(meshTls, 'certPem');
93
+ const keyPem = Reflect.get(meshTls, 'keyPem');
94
+ const caCertPem = Reflect.get(meshTls, 'caCertPem');
95
+ if (typeof certPem !== 'string' || typeof keyPem !== 'string' || typeof caCertPem !== 'string') {
96
+ return null;
97
+ }
98
+ const hubUrl = config['hubUrl'];
99
+ const hubAddress = config['hubAddress'];
100
+ return {
101
+ hubUrl: typeof hubUrl === 'string' && hubUrl.length > 0 ? hubUrl : null,
102
+ hubAddress: typeof hubAddress === 'string' && hubAddress.length > 0 ? hubAddress : null,
103
+ certPem,
104
+ keyPem,
105
+ caCertPem,
106
+ };
107
+ }
78
108
  /**
79
109
  * Leave the hub: erase the mesh material, the hub address and the HTTPS bundle
80
110
  * the hub issued. The agent is unpaired afterwards and accepts offers again.
@@ -240,7 +240,8 @@ function createAgentService(deps) {
240
240
  const { broker } = ctx;
241
241
  const cpus = os.cpus();
242
242
  let cpuPercent = 0;
243
- let memoryPercent = 0;
243
+ let memoryPercent = null;
244
+ let freeMemoryBytes = null;
244
245
  const metrics = deps.getMetricsProvider?.();
245
246
  if (metrics) {
246
247
  // Bounded so $agent.status never hangs (and drops the node from
@@ -249,6 +250,7 @@ function createAgentService(deps) {
249
250
  if (snapshot) {
250
251
  cpuPercent = snapshot.cpu.total;
251
252
  memoryPercent = snapshot.memory.percent;
253
+ freeMemoryBytes = snapshot.memory.availableBytes;
252
254
  }
253
255
  }
254
256
  const mem = process.memoryUsage();
@@ -262,9 +264,12 @@ function createAgentService(deps) {
262
264
  cpuCores: cpus.length,
263
265
  cpuModel: cpus[0]?.model,
264
266
  totalMemoryMB: Math.round(os.totalmem() / 1024 / 1024),
265
- freeMemoryMB: Math.round(os.freemem() / 1024 / 1024),
267
+ freeMemoryMB: freeMemoryBytes === null ? null : Math.round(freeMemoryBytes / 1024 / 1024),
266
268
  cpuPercent,
267
269
  memoryPercent,
270
+ // The agent's own (core) process, so a topology row can attribute
271
+ // the largest process on the node instead of showing 0.
272
+ processRssBytes: process.memoryUsage().rss,
268
273
  uptime: os.uptime(),
269
274
  // The agent *process* itself (distinct from the host `uptime` /
270
275
  // memory above) — surfaced so the UI can show how long THIS agent
@@ -298,7 +303,7 @@ function createAgentService(deps) {
298
303
  handler: async (ctx) => {
299
304
  const { broker } = ctx;
300
305
  let cpuPercent = 0;
301
- let memoryPercent = 0;
306
+ let memoryPercent = null;
302
307
  const metrics = deps.getMetricsProvider?.();
303
308
  if (metrics) {
304
309
  try {
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.membershipFetch = void 0;
4
+ exports.askHubMembership = askHubMembership;
5
+ /**
6
+ * The agent asks its hub "am I still a member?" (D857).
7
+ *
8
+ * Two plain HTTPS calls (`MEMBERSHIP_PATHS`): fetch a nonce, then prove possession of the mesh leaf's
9
+ * private key by signing it. The hub's HTTPS certificate is NOT what this trusts - the hub may serve a
10
+ * private one - the answer is a JWS signed by the mesh CA key, verified here against the CA this agent
11
+ * pinned at pairing, bound to the nonce and the leaf. The key never leaves the process: it signs, and
12
+ * only the signature travels (no argv, no env).
13
+ *
14
+ * Every doubt is `unreachable`, never an answer: a hub that cannot be reached, a status other than
15
+ * 200, a token that does not verify. The caller changes nothing on `unreachable` (D49).
16
+ */
17
+ const node_crypto_1 = require("node:crypto");
18
+ const system_1 = require("@camstack/system");
19
+ const undici_1 = require("undici");
20
+ const REQUEST_TIMEOUT_MS = 8_000;
21
+ /** Production transport: JSON POST with a timeout, TLS not verified (the answer is CA-signed). */
22
+ const membershipFetch = (url, init) => (0, undici_1.fetch)(url, {
23
+ method: 'POST',
24
+ headers: { 'content-type': 'application/json' },
25
+ ...(init.body !== undefined ? { body: init.body } : {}),
26
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
27
+ dispatcher: new undici_1.Agent({ connect: { rejectUnauthorized: false } }),
28
+ });
29
+ exports.membershipFetch = membershipFetch;
30
+ function errorMessage(err) {
31
+ return err instanceof Error ? err.message : String(err);
32
+ }
33
+ async function askHubMembership(query, doFetch = exports.membershipFetch) {
34
+ try {
35
+ const base = query.hubUrl.replace(/\/+$/, '');
36
+ const challenge = await doFetch(`${base}${system_1.MEMBERSHIP_PATHS.challenge}`, {
37
+ body: '{}',
38
+ });
39
+ if (challenge.status !== 200) {
40
+ return {
41
+ kind: 'unreachable',
42
+ error: `challenge answered HTTP ${challenge.status}`,
43
+ };
44
+ }
45
+ const nonce = Reflect.get(Object(await challenge.json()), 'nonce');
46
+ if (typeof nonce !== 'string' || nonce.length === 0) {
47
+ return { kind: 'unreachable', error: 'challenge carried no nonce' };
48
+ }
49
+ const response = await doFetch(`${base}${system_1.MEMBERSHIP_PATHS.query}`, {
50
+ body: JSON.stringify({
51
+ nodeId: query.nodeId,
52
+ certPem: query.certPem,
53
+ nonce,
54
+ signature: (0, system_1.signMembershipProof)(query.keyPem, query.nodeId, nonce),
55
+ }),
56
+ });
57
+ if (response.status !== 200) {
58
+ return {
59
+ kind: 'unreachable',
60
+ error: `membership answered HTTP ${response.status}`,
61
+ };
62
+ }
63
+ const token = Reflect.get(Object(await response.json()), 'token');
64
+ if (typeof token !== 'string')
65
+ return { kind: 'unreachable', error: 'answer carried no token' };
66
+ const answer = (0, system_1.verifyMembershipAnswer)(query.caCertPem, token, {
67
+ nodeId: query.nodeId,
68
+ nonce,
69
+ leafFingerprint: new node_crypto_1.X509Certificate(query.certPem).fingerprint256,
70
+ });
71
+ if (answer === null) {
72
+ return {
73
+ kind: 'unreachable',
74
+ error: 'the answer is not signed by the pinned mesh CA',
75
+ };
76
+ }
77
+ return { kind: 'answer', answer };
78
+ }
79
+ catch (err) {
80
+ return { kind: 'unreachable', error: errorMessage(err) };
81
+ }
82
+ }
@@ -49,7 +49,9 @@ const agent_addon_updates_js_1 = require("./agent-addon-updates.js");
49
49
  const agent_hub_registration_js_1 = require("./agent-hub-registration.js");
50
50
  const agent_mesh_gate_js_1 = require("./agent-mesh-gate.js");
51
51
  const agent_pairing_local_js_1 = require("./agent-pairing-local.js");
52
+ const agent_membership_watch_js_1 = require("./agent-membership-watch.js");
52
53
  const agent_pairing_store_js_1 = require("./agent-pairing-store.js");
54
+ const hub_membership_client_js_1 = require("./hub-membership-client.js");
53
55
  const agent_config_js_1 = require("./agent-config.js");
54
56
  const agent_group_runner_js_1 = require("./agent-group-runner.js");
55
57
  const agent_http_js_1 = require("./agent-http.js");
@@ -76,6 +78,8 @@ const register_agent_cap_dispatch_js_1 = require("./register-agent-cap-dispatch.
76
78
  const system_2 = require("@camstack/system");
77
79
  /** A reconnect (stop + rebuild + start) that takes longer than this is abandoned. */
78
80
  const RECONNECT_DEADLINE_MS = 90_000;
81
+ /** How often the agent re-evaluates whether to ask its hub "am I still a member?" (D857). */
82
+ const MEMBERSHIP_TICK_MS = 15_000;
79
83
  const agentLogManager = new system_2.LogManager(5000);
80
84
  /**
81
85
  * Caps whose `nodeId` arg is provider DATA (`nodeIdMode: 'data'` —
@@ -992,6 +996,41 @@ async function startAgent(configPath) {
992
996
  advertise();
993
997
  };
994
998
  advertise();
999
+ // A hub that forgot or unpaired this agent refuses its leaf without a word. While paired but not
1000
+ // connected, ask the hub whether the leaf is still a member; two CA-signed `revoked` answers in a
1001
+ // row discard the pairing, anything else changes nothing (D857, D49).
1002
+ const membershipWatch = new agent_membership_watch_js_1.AgentMembershipWatch({
1003
+ nodeId: config.nodeId,
1004
+ isPaired: () => (0, agent_pairing_store_js_1.isAgentPaired)(config.configPath),
1005
+ isConnected: () => getHubConnectionState() === 'connected',
1006
+ ask: async () => {
1007
+ const identity = (0, agent_pairing_store_js_1.readPairedHubIdentity)(config.configPath);
1008
+ const hubUrl = identity?.hubUrl ??
1009
+ (identity?.hubAddress != null ? (0, derive_hub_url_js_1.deriveHubUrlFromHubAddress)(identity.hubAddress) : undefined);
1010
+ if (identity === null || hubUrl === undefined) {
1011
+ return { kind: 'unreachable', error: 'no hub address on record' };
1012
+ }
1013
+ return (0, hub_membership_client_js_1.askHubMembership)({
1014
+ hubUrl,
1015
+ nodeId: config.nodeId,
1016
+ certPem: identity.certPem,
1017
+ keyPem: identity.keyPem,
1018
+ caCertPem: identity.caCertPem,
1019
+ });
1020
+ },
1021
+ onRevoked: (reason) => forgetHubNow(`the hub revoked this agent's certificate (${reason})`),
1022
+ log: {
1023
+ // The console line has no tag column: the node id rides in the text as well.
1024
+ warn: (message, extras) => consoleLogger.warn(`${message} [nodeId=${config.nodeId}]`, extras),
1025
+ info: (message, extras) => consoleLogger.info(`${message} [nodeId=${config.nodeId}]`, extras),
1026
+ },
1027
+ });
1028
+ const membershipTimer = setInterval(() => {
1029
+ void membershipWatch
1030
+ .tick(Date.now())
1031
+ .catch((err) => consoleLogger.warn(`membership check failed: ${(0, types_2.errMsg)(err)}`));
1032
+ }, MEMBERSHIP_TICK_MS);
1033
+ membershipTimer.unref();
995
1034
  void (0, agent_http_js_1.startAgentHttpServer)(getBrokerFn, {
996
1035
  log: {
997
1036
  info: (message, extras) => consoleLogger.info(message, extras),
@@ -349,7 +349,9 @@ async function computeTopologyInternal(agentRegistry, addonRegistry, getNodeRoot
349
349
  name: `${node.info.id} (core)`,
350
350
  state: 'running',
351
351
  cpuPercent: node.status?.cpuPercent ?? 0,
352
- memoryRss: 0,
352
+ // The agent's own process RSS from $agent.status; 0 only for an agent
353
+ // that predates the field.
354
+ memoryRss: node.status?.processRssBytes ?? 0,
353
355
  uptimeSeconds: Math.floor((Date.now() - node.connectedSince) / 1000),
354
356
  services: mainProcessServices,
355
357
  };
@@ -414,7 +416,7 @@ async function computeTopologyInternal(agentRegistry, addonRegistry, getNodeRoot
414
416
  isHub: node.isHub,
415
417
  isOnline: node.isOnline !== false,
416
418
  cpuPercent: node.status?.cpuPercent ?? 0,
417
- memoryPercent: node.status?.memoryPercent ?? 0,
419
+ memoryPercent: node.status?.memoryPercent ?? null,
418
420
  uptime: Date.now() - node.connectedSince,
419
421
  // Fix (was hardcoded `new Date().toISOString()` — always "now", a bug
420
422
  // even for online nodes): report the persisted `lastActive` when we have
@@ -456,7 +458,7 @@ async function computeTopologyInternal(agentRegistry, addonRegistry, getNodeRoot
456
458
  isHub: persisted.isHub,
457
459
  isOnline: false,
458
460
  cpuPercent: 0,
459
- memoryPercent: 0,
461
+ memoryPercent: null,
460
462
  uptime: 0,
461
463
  lastSeen: new Date(persisted.lastActive).toISOString(),
462
464
  localIps: [...persisted.localIps],
@@ -35,6 +35,13 @@ const ForgetNodeResultSchema = zod_1.z.object({
35
35
  registryRemoved: zod_1.z.boolean(),
36
36
  /** Read back AFTER the attempt: the node is still in the cluster registry. */
37
37
  stillListed: zod_1.z.boolean(),
38
+ /** What happened to the node's mesh certificates: `registry-unavailable` / `failed` = NOT revoked. */
39
+ certificates: zod_1.z.discriminatedUnion('outcome', [
40
+ zod_1.z.object({ outcome: zod_1.z.literal('not-attempted') }),
41
+ zod_1.z.object({ outcome: zod_1.z.literal('revoked'), serials: zod_1.z.array(zod_1.z.string()) }),
42
+ zod_1.z.object({ outcome: zod_1.z.literal('registry-unavailable') }),
43
+ zod_1.z.object({ outcome: zod_1.z.literal('failed'), error: zod_1.z.string() }),
44
+ ]),
38
45
  /** One operator-grade sentence naming what happened. */
39
46
  message: zod_1.z.string(),
40
47
  });
@@ -62,9 +69,9 @@ function createClusterNodesRouter(agentRegistry) {
62
69
  await agentRegistry.repairNodeAddons(input.nodeId);
63
70
  return { success: true };
64
71
  }),
65
- // Purge one node's persisted offline history. Pairs with
66
- // `pipelineOrchestrator.removeAgentSettings` on the frontend to fully
67
- // forget an offline node (history row + per-node pipeline assignments).
72
+ // Forget a node, online or offline: revoke its certificates, cut it from routing, purge its
73
+ // persisted history. Pairs with `pipelineOrchestrator.removeAgentSettings` on the frontend
74
+ // (per-node pipeline assignments).
68
75
  forgetNode: trpc_middleware_js_1.adminProcedure
69
76
  .input(ForgetNodeInputSchema)
70
77
  .output(ForgetNodeResultSchema)
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createProcessMembershipResponder = createProcessMembershipResponder;
4
+ exports.registerMeshMembershipRoutes = registerMeshMembershipRoutes;
5
+ const system_1 = require("@camstack/system");
6
+ const auth_rate_limit_1 = require("../auth/auth-rate-limit");
7
+ const WINDOW_MS = 60_000;
8
+ const DEFAULT_PER_MINUTE = 60;
9
+ function createProcessMembershipResponder(logger) {
10
+ return new system_1.MembershipResponder({
11
+ registry: system_1.getProcessMeshCertificateRegistry,
12
+ crl: system_1.processMeshCrl,
13
+ caKeyPem: () => {
14
+ const state = (0, system_1.getProcessMeshCaState)();
15
+ return state.state === 'ready' ? state.ca.keyPem : null;
16
+ },
17
+ log: (message, meta) => logger.warn(message, { tags: { nodeId: meta.nodeId } }),
18
+ });
19
+ }
20
+ function registerMeshMembershipRoutes(fastify, options) {
21
+ const responder = options.responder ?? createProcessMembershipResponder(options.logger);
22
+ const limiter = (0, auth_rate_limit_1.createRateLimiter)({
23
+ windowMs: WINDOW_MS,
24
+ max: options.perMinute ?? DEFAULT_PER_MINUTE,
25
+ ...(options.now !== undefined ? { now: options.now } : {}),
26
+ });
27
+ fastify.post(system_1.MEMBERSHIP_PATHS.challenge, async (request, reply) => {
28
+ const verdict = limiter.check(request.socket.remoteAddress ?? 'unknown');
29
+ if (!verdict.allowed) {
30
+ return reply
31
+ .status(429)
32
+ .header('retry-after', String(verdict.retryAfterSeconds))
33
+ .send({ ok: false });
34
+ }
35
+ return { nonce: responder.issueChallenge() };
36
+ });
37
+ fastify.post(system_1.MEMBERSHIP_PATHS.query, async (request, reply) => {
38
+ const verdict = limiter.check(request.socket.remoteAddress ?? 'unknown');
39
+ if (!verdict.allowed) {
40
+ return reply
41
+ .status(429)
42
+ .header('retry-after', String(verdict.retryAfterSeconds))
43
+ .send({ ok: false });
44
+ }
45
+ const outcome = responder.answer(request.body);
46
+ if (!outcome.ok)
47
+ return reply.status(outcome.status).send({ ok: false });
48
+ return { token: outcome.token };
49
+ });
50
+ }
@@ -2,7 +2,7 @@
2
2
  // AUTO-GENERATED by scripts/generate-cap-mounts.ts — DO NOT EDIT
3
3
  // Re-run: npx tsx scripts/generate-cap-mounts.ts
4
4
  //
5
- // Mounted: 187 Skipped (legacy): 3
5
+ // Mounted: 189 Skipped (legacy): 3
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.LEGACY_SHAPE_SKIP = void 0;
8
8
  exports.mountAllCaps = mountAllCaps;
@@ -231,6 +231,8 @@ function mountAllCaps(services) {
231
231
  ...first,
232
232
  listSupportedDeviceKinds: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listSupportedDeviceKinds'),
233
233
  listExposedDevices: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listExposedDevices'),
234
+ listSupportedEntityKinds: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listSupportedEntityKinds'),
235
+ listExposedEntities: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listExposedEntities'),
234
236
  };
235
237
  }, remoteCapProxy),
236
238
  deviceExtension: (0, generated_cap_routers_js_1.createCapRouter_deviceExtension)((_ctx) => (0, device_collection_dispatch_js_1.requireDeviceCollection)(reg, 'device-extension'), remoteCapProxy),
@@ -456,6 +458,15 @@ function mountAllCaps(services) {
456
458
  null, remoteCapProxy),
457
459
  nodeCertificates: (0, generated_cap_routers_js_1.createCapRouter_nodeCertificates)((_ctx) => reg?.getSingleton('node-certificates') ??
458
460
  null, remoteCapProxy),
461
+ nodeRemovalHook: (0, generated_cap_routers_js_1.createCapRouter_nodeRemovalHook)((_ctx, addonId) => {
462
+ if (!reg)
463
+ return null;
464
+ if (addonId !== undefined) {
465
+ return reg.getProviderByAddonId('node-removal-hook', addonId);
466
+ }
467
+ const entries = reg.getCollectionEntries('node-removal-hook');
468
+ return entries[0]?.[1] ?? null;
469
+ }, remoteCapProxy),
459
470
  nodes: (0, generated_cap_routers_js_1.createCapRouter_nodes)((_ctx) => reg?.getSingleton('nodes') ?? null, remoteCapProxy),
460
471
  notificationOutput: (0, generated_cap_routers_js_1.createCapRouter_notificationOutput)((_ctx, addonId) => {
461
472
  if (!reg)
@@ -492,6 +503,7 @@ function mountAllCaps(services) {
492
503
  osd: (0, generated_cap_routers_js_1.createCapRouter_osd)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'osd'), remoteCapProxy),
493
504
  osdManager: (0, generated_cap_routers_js_1.createCapRouter_osdManager)((_ctx) => reg?.getSingleton('osd-manager') ?? null, remoteCapProxy),
494
505
  petFeeder: (0, generated_cap_routers_js_1.createCapRouter_petFeeder)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'pet-feeder'), remoteCapProxy),
506
+ petFountain: (0, generated_cap_routers_js_1.createCapRouter_petFountain)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'pet-fountain'), remoteCapProxy),
495
507
  pipelineAnalytics: (0, generated_cap_routers_js_1.createCapRouter_pipelineAnalytics)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'pipeline-analytics'), remoteCapProxy),
496
508
  pipelineExecutor: (0, generated_cap_routers_js_1.createCapRouter_pipelineExecutor)((_ctx) => reg?.getSingleton('pipeline-executor') ??
497
509
  null, remoteCapProxy),