@camstack/server 1.2.563 → 1.2.565

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.
@@ -0,0 +1,505 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCertificatesRouter = createCertificatesRouter;
4
+ /**
5
+ * Certificates inventory and management - fixed core API for Identity ->
6
+ * Certificates (D855, D856).
7
+ *
8
+ * Hand-written (not a `*.cap.ts`) like `cluster-nodes.router.ts`: it joins the node
9
+ * list (agent registry), the `local-network` provider (the hub's HTTPS leaf), the
10
+ * mesh CA and the registry of mesh leaves, and calls each agent's
11
+ * `node-certificates` capability. Device credentials are the `device-credentials`
12
+ * capability; this router never touches them.
13
+ *
14
+ * - HTTPS: the hub serves its own leaf (renewed with `regenerateCertificate`);
15
+ * every agent serves a leaf the HUB issued from its local CA, on the same port
16
+ * as its plain HTTP. An agent without one yet says so (`not-served`, with the
17
+ * reason), never an empty row that reads as "no certificate".
18
+ * - Mesh: every leaf the hub issues is recorded; a node enrolled earlier is
19
+ * recorded from the leaf it presents on its live connection (`observed`).
20
+ * Renewal is a handover that never leaves a node without a working leaf
21
+ * (`mesh-renewal.ts`); revocation puts the serial on the CRL and disconnects
22
+ * the node, which must re-join with a new join token. Neither forgets a node:
23
+ * that stays `clusterNodes.forgetNode`.
24
+ */
25
+ const node_crypto_1 = require("node:crypto");
26
+ const system_1 = require("@camstack/system");
27
+ const server_1 = require("@trpc/server");
28
+ const zod_1 = require("zod");
29
+ const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
30
+ const HUB_NODE_ID = 'hub';
31
+ const EXPIRING_SOON_MS = 7 * 86_400_000;
32
+ const TlsStatusRowSchema = zod_1.z.object({
33
+ mode: zod_1.z.enum(['generated', 'uploaded', 'disabled']),
34
+ leafFingerprintSha256: zod_1.z.string().nullable(),
35
+ caFingerprintSha256: zod_1.z.string().nullable(),
36
+ validTo: zod_1.z.string().nullable(),
37
+ sans: zod_1.z.array(zod_1.z.string()),
38
+ reissueError: zod_1.z.string().nullable(),
39
+ restartRequired: zod_1.z.boolean(),
40
+ });
41
+ const NodeHttpsSchema = zod_1.z.object({
42
+ nodeId: zod_1.z.string(),
43
+ isHub: zod_1.z.boolean(),
44
+ online: zod_1.z.boolean(),
45
+ https: zod_1.z.discriminatedUnion('kind', [
46
+ zod_1.z.object({ kind: zod_1.z.literal('served'), status: TlsStatusRowSchema }),
47
+ zod_1.z.object({ kind: zod_1.z.literal('not-served'), reason: zod_1.z.string() }),
48
+ /** The hub could not read it right now: say so, never present it as "none". */
49
+ zod_1.z.object({ kind: zod_1.z.literal('unreadable'), reason: zod_1.z.string() }),
50
+ ]),
51
+ });
52
+ const MeshLeafRowSchema = zod_1.z.object({
53
+ serial: zod_1.z.string(),
54
+ fingerprintSha256: zod_1.z.string(),
55
+ notBefore: zod_1.z.number(),
56
+ notAfter: zod_1.z.number(),
57
+ issuedAt: zod_1.z.number(),
58
+ /** `enroll` | `renewal` | `boot` were issued by the hub; `observed` was read off a live connection. */
59
+ issuedVia: zod_1.z.enum(['enroll', 'renewal', 'boot', 'observed']),
60
+ status: zod_1.z.enum(['valid', 'expiring', 'expired', 'revoked']),
61
+ revokedAt: zod_1.z.number().nullable(),
62
+ revokedReason: zod_1.z.string().nullable(),
63
+ });
64
+ const MeshNodeSchema = zod_1.z.object({
65
+ nodeId: zod_1.z.string(),
66
+ online: zod_1.z.boolean(),
67
+ /** The leaf the node holds now: the newest non-revoked one. `null` when none is known. */
68
+ current: MeshLeafRowSchema.nullable(),
69
+ /** The node is connected and presents exactly `current`. `null` when it is offline. */
70
+ presentingCurrent: zod_1.z.boolean().nullable(),
71
+ /** Older and revoked leaves, newest first. */
72
+ history: zod_1.z.array(MeshLeafRowSchema),
73
+ });
74
+ const MeshInventorySchema = zod_1.z.object({
75
+ ca: zod_1.z.discriminatedUnion('state', [
76
+ zod_1.z.object({
77
+ state: zod_1.z.literal('ready'),
78
+ subject: zod_1.z.string(),
79
+ fingerprintSha256: zod_1.z.string(),
80
+ validTo: zod_1.z.string(),
81
+ }),
82
+ zod_1.z.object({ state: zod_1.z.literal('unavailable'), reason: zod_1.z.string() }),
83
+ ]),
84
+ /** Serials the hub refuses at the mesh handshake. Persisted with the registry and replayed at boot. */
85
+ revokedSerials: zod_1.z.array(zod_1.z.string()),
86
+ /** `false` while the registry is not up (no settings store): the node list is then incomplete. */
87
+ registryAvailable: zod_1.z.boolean(),
88
+ /** The hub's mesh TLS listener: `closed` means every agent is refused (e.g. revocations not loaded). */
89
+ listener: zod_1.z.discriminatedUnion('state', [
90
+ zod_1.z.object({ state: zod_1.z.literal('open') }),
91
+ zod_1.z.object({ state: zod_1.z.literal('closed'), reason: zod_1.z.string(), since: zod_1.z.number() }),
92
+ zod_1.z.object({ state: zod_1.z.literal('not-started') }),
93
+ ]),
94
+ nodes: zod_1.z.array(MeshNodeSchema),
95
+ });
96
+ const MeshRenewResultSchema = zod_1.z.discriminatedUnion('ok', [
97
+ zod_1.z.object({
98
+ ok: zod_1.z.literal(true),
99
+ nodeId: zod_1.z.string(),
100
+ oldSerial: zod_1.z.string().nullable(),
101
+ newSerial: zod_1.z.string(),
102
+ newNotAfter: zod_1.z.number(),
103
+ }),
104
+ zod_1.z.object({
105
+ ok: zod_1.z.literal(false),
106
+ nodeId: zod_1.z.string(),
107
+ stage: zod_1.z.enum(['prepare', 'issue', 'install', 'reconnect', 'confirm']),
108
+ reason: zod_1.z.string(),
109
+ /** The node is on its previous leaf (or never left it): nothing was broken. */
110
+ keptPreviousLeaf: zod_1.z.boolean(),
111
+ }),
112
+ ]);
113
+ const MeshRevokeResultSchema = zod_1.z.object({
114
+ nodeId: zod_1.z.string(),
115
+ revokedSerials: zod_1.z.array(zod_1.z.string()),
116
+ disconnected: zod_1.z.number().int(),
117
+ /** What the operator does next. Revoking does not forget the node. */
118
+ next: zod_1.z.string(),
119
+ });
120
+ function leafStatus(record, at) {
121
+ if (record.revokedAt !== null)
122
+ return 'revoked';
123
+ if (record.notAfter <= at)
124
+ return 'expired';
125
+ return record.notAfter - at < EXPIRING_SOON_MS ? 'expiring' : 'valid';
126
+ }
127
+ function createCertificatesRouter(deps) {
128
+ const localNetwork = () => {
129
+ const provider = deps.registry?.getSingleton('local-network');
130
+ if (!provider) {
131
+ throw new server_1.TRPCError({
132
+ code: 'PRECONDITION_FAILED',
133
+ message: 'The local-network capability is not available yet',
134
+ });
135
+ }
136
+ return provider;
137
+ };
138
+ const peers = deps.peers ?? {
139
+ peerLeaf: (nodeId) => (0, system_1.getProcessPeerLeaf)(nodeId),
140
+ dropPeer: (nodeId) => (0, system_1.dropProcessPeer)(nodeId),
141
+ };
142
+ const meshRegistry = deps.meshRegistry ?? system_1.getProcessMeshCertificateRegistry;
143
+ const crl = deps.crl ?? system_1.processMeshCrl;
144
+ const meshCa = deps.meshCa ??
145
+ (() => {
146
+ const state = (0, system_1.getProcessMeshCaState)();
147
+ return state.state === 'ready' ? state.ca : null;
148
+ });
149
+ const issueHttps = deps.issueHttps ?? system_1.issueAgentHttpsLeaf;
150
+ const listenerState = deps.listenerState ??
151
+ (() => (0, system_1.getProcessMeshTlsListenerState)() ?? { state: 'not-started' });
152
+ const httpsNames = deps.httpsNames ?? system_1.getProcessAgentHttpsNames;
153
+ const registrations = deps.registrations ?? {
154
+ subscribe: (listener) => (0, system_1.onProcessNodeRegistered)(listener),
155
+ };
156
+ /** One certificate operation per node at a time: a second one is refused, not interleaved. */
157
+ const busy = new Set();
158
+ const exclusive = async (key, what, run) => {
159
+ if (busy.has(key)) {
160
+ deps.log.warn('Certificate operation refused: another one is running on this node', {
161
+ tags: { nodeId: key },
162
+ meta: { what },
163
+ });
164
+ throw new server_1.TRPCError({
165
+ code: 'CONFLICT',
166
+ message: `Another certificate operation is already running on ${key}. Wait for it to finish.`,
167
+ });
168
+ }
169
+ busy.add(key);
170
+ try {
171
+ return await run();
172
+ }
173
+ finally {
174
+ busy.delete(key);
175
+ }
176
+ };
177
+ const requireAgent = (nodeId) => {
178
+ if (nodeId === HUB_NODE_ID) {
179
+ throw new server_1.TRPCError({
180
+ code: 'PRECONDITION_FAILED',
181
+ message: 'The hub is not an agent: its HTTPS leaf is renewed with renewHttpsOnNode on "hub", and its mesh leaf is re-issued at every boot.',
182
+ });
183
+ }
184
+ const client = deps.nodeClient(nodeId);
185
+ if (client === null) {
186
+ throw new server_1.TRPCError({
187
+ code: 'PRECONDITION_FAILED',
188
+ message: `Node ${nodeId} is not reachable, or runs an agent too old to manage its certificates (update it).`,
189
+ });
190
+ }
191
+ return client;
192
+ };
193
+ /** Record the leaf each connected node presents, for nodes enrolled before the registry existed. */
194
+ const observeConnectedNodes = async (registry) => {
195
+ for (const node of deps.agentRegistry.listNodeLiveness()) {
196
+ if (node.id === HUB_NODE_ID || !node.isOnline)
197
+ continue;
198
+ const leaf = peers.peerLeaf(node.id);
199
+ if (leaf === undefined)
200
+ continue;
201
+ await registry.recordObserved(node.id, {
202
+ cn: node.id,
203
+ serial: leaf.serial,
204
+ fingerprint: leaf.fingerprint,
205
+ notBefore: leaf.notBefore,
206
+ notAfter: leaf.notAfter,
207
+ });
208
+ }
209
+ };
210
+ const statusOfLeaf = (leaf) => ({
211
+ mode: 'generated',
212
+ leafFingerprintSha256: leaf.fingerprintSha256,
213
+ caFingerprintSha256: null,
214
+ validTo: new Date(leaf.notAfter).toISOString(),
215
+ sans: leaf.sans,
216
+ reissueError: null,
217
+ restartRequired: false,
218
+ });
219
+ return (0, trpc_middleware_js_1.trpcRouter)({
220
+ /** HTTPS certificate state per node. The hub serves its own; an agent serves a hub-issued leaf. */
221
+ listHttpsCertificates: trpc_middleware_js_1.adminProcedure
222
+ .input(zod_1.z.void())
223
+ .output(zod_1.z.array(NodeHttpsSchema))
224
+ .query(async () => {
225
+ const nodes = deps.agentRegistry.listNodeLiveness();
226
+ const entries = nodes.some((n) => n.id === HUB_NODE_ID)
227
+ ? nodes
228
+ : [{ id: HUB_NODE_ID, isOnline: true }, ...nodes];
229
+ const rows = [];
230
+ for (const node of entries) {
231
+ const isHub = node.id === HUB_NODE_ID;
232
+ if (isHub) {
233
+ try {
234
+ const status = await localNetwork().getTlsStatus();
235
+ const { caCertPem: _pem, ...rest } = status;
236
+ rows.push({
237
+ nodeId: node.id,
238
+ isHub,
239
+ online: true,
240
+ https: { kind: 'served', status: rest },
241
+ });
242
+ }
243
+ catch (error) {
244
+ rows.push({
245
+ nodeId: node.id,
246
+ isHub,
247
+ online: true,
248
+ https: {
249
+ kind: 'unreadable',
250
+ reason: error instanceof Error ? error.message : String(error),
251
+ },
252
+ });
253
+ }
254
+ continue;
255
+ }
256
+ if (!node.isOnline) {
257
+ rows.push({
258
+ nodeId: node.id,
259
+ isHub,
260
+ online: false,
261
+ https: {
262
+ kind: 'unreadable',
263
+ reason: 'The node is offline - its certificate cannot be read.',
264
+ },
265
+ });
266
+ continue;
267
+ }
268
+ const client = deps.nodeClient(node.id);
269
+ if (client === null) {
270
+ rows.push({
271
+ nodeId: node.id,
272
+ isHub,
273
+ online: true,
274
+ https: {
275
+ kind: 'unreadable',
276
+ reason: 'The node does not provide node-certificates (update the agent).',
277
+ },
278
+ });
279
+ continue;
280
+ }
281
+ try {
282
+ const answer = await client.getCertificates();
283
+ rows.push({
284
+ nodeId: node.id,
285
+ isHub,
286
+ online: true,
287
+ https: answer.https.state === 'served'
288
+ ? { kind: 'served', status: statusOfLeaf(answer.https.leaf) }
289
+ : { kind: 'not-served', reason: answer.https.reason },
290
+ });
291
+ }
292
+ catch (error) {
293
+ rows.push({
294
+ nodeId: node.id,
295
+ isHub,
296
+ online: true,
297
+ https: {
298
+ kind: 'unreadable',
299
+ reason: error instanceof Error ? error.message : String(error),
300
+ },
301
+ });
302
+ }
303
+ }
304
+ return rows;
305
+ }),
306
+ /**
307
+ * Issue a new HTTPS leaf. On the hub: a new leaf under the local CA
308
+ * (`regenerateCertificate`; refused while an uploaded certificate is served). On an
309
+ * agent: the agent makes a key + CSR, the hub signs the public key under the same
310
+ * local CA for the names the agent reports (private addresses and local-only host
311
+ * names), and the agent hot-swaps it - no restart, no private key on the mesh.
312
+ */
313
+ renewHttpsOnNode: trpc_middleware_js_1.adminProcedure
314
+ .input(zod_1.z.object({ nodeId: zod_1.z.string().min(1), reason: zod_1.z.string().max(200).optional() }))
315
+ .output(TlsStatusRowSchema)
316
+ .mutation(async ({ input }) => {
317
+ if (input.nodeId === HUB_NODE_ID) {
318
+ const status = await localNetwork().regenerateCertificate({
319
+ ...(input.reason !== undefined ? { reason: input.reason } : {}),
320
+ });
321
+ const { caCertPem: _pem, ...rest } = status;
322
+ return rest;
323
+ }
324
+ const client = requireAgent(input.nodeId);
325
+ const tags = { nodeId: input.nodeId };
326
+ return exclusive(input.nodeId, 'https', async () => {
327
+ try {
328
+ const prepared = await client.prepareHttpsCertificate();
329
+ // The hub signs what it knows by itself: the node id and the address of the node's
330
+ // LIVE mesh socket. Whatever the agent reports is dropped (and logged below).
331
+ const observedAddress = peers.peerLeaf(input.nodeId)?.remoteAddress;
332
+ if (observedAddress === undefined) {
333
+ throw new Error('the node has no live mesh connection the hub can read its address from - connect it first');
334
+ }
335
+ // Without the names registry the hub cannot tell whether a name is already another
336
+ // node's, so it signs nothing.
337
+ const names = httpsNames();
338
+ if (names === null) {
339
+ throw new Error('the agent HTTPS names registry is not loaded yet - try again shortly');
340
+ }
341
+ const issued = await issueHttps({
342
+ hubDataDir: deps.hubDataDir,
343
+ nodeId: input.nodeId,
344
+ csrPem: prepared.csrPem,
345
+ reportedNames: [...prepared.hostnames, ...prepared.addresses],
346
+ observedAddress,
347
+ takenByOthers: names.takenByOthers(input.nodeId),
348
+ });
349
+ // Written down BEFORE the install answer: if the answer is lost the names stay taken
350
+ // (the node may hold the leaf), never silently free for another node.
351
+ await names.record(input.nodeId, issued.accepted);
352
+ // The hub's local CA was rotated (D227) when the node pinned another one: say so, over
353
+ // the authenticated install call, so the node re-pins instead of refusing for ever.
354
+ const caRotation = prepared.pinnedCaFingerprintSha256 !== null &&
355
+ prepared.pinnedCaFingerprintSha256 !==
356
+ new node_crypto_1.X509Certificate(issued.caCertPem).fingerprint256;
357
+ deps.log.info('Agent HTTPS leaf issued', {
358
+ tags,
359
+ meta: { accepted: issued.accepted, rejected: issued.rejected },
360
+ });
361
+ const leaf = await client.installHttpsCertificate({
362
+ certPem: issued.certPem,
363
+ caCertPem: issued.caCertPem,
364
+ ...(caRotation ? { caRotation: true } : {}),
365
+ });
366
+ deps.log.info('Agent HTTPS leaf installed', {
367
+ tags,
368
+ meta: { fingerprint: leaf.fingerprintSha256 },
369
+ });
370
+ return statusOfLeaf(leaf);
371
+ }
372
+ catch (error) {
373
+ const message = error instanceof Error ? error.message : String(error);
374
+ deps.log.error('Agent HTTPS renewal failed', { tags, meta: { error: message } });
375
+ throw new server_1.TRPCError({
376
+ code: 'INTERNAL_SERVER_ERROR',
377
+ message: `HTTPS renewal on ${input.nodeId} failed: ${message}. The node keeps serving what it had.`,
378
+ });
379
+ }
380
+ });
381
+ }),
382
+ /** The mesh CA, the revocation list, and every node's recorded leaves. */
383
+ getMeshCertificates: trpc_middleware_js_1.adminProcedure
384
+ .input(zod_1.z.void())
385
+ .output(MeshInventorySchema)
386
+ .query(async () => {
387
+ const state = (0, system_1.getProcessMeshCaState)();
388
+ const ca = state.state === 'ready'
389
+ ? {
390
+ state: 'ready',
391
+ subject: new node_crypto_1.X509Certificate(state.ca.certPem).subject,
392
+ fingerprintSha256: (0, system_1.caFingerprint)(state.ca.certPem),
393
+ validTo: new node_crypto_1.X509Certificate(state.ca.certPem).validTo,
394
+ }
395
+ : { state: 'unavailable', reason: state.reason };
396
+ const registry = meshRegistry();
397
+ const at = Date.now();
398
+ const nodes = [];
399
+ if (registry !== null) {
400
+ await observeConnectedNodes(registry);
401
+ const liveness = new Map(deps.agentRegistry.listNodeLiveness().map((n) => [n.id, n.isOnline]));
402
+ const nodeIds = new Set([...registry.list().map((r) => r.nodeId), ...liveness.keys()]);
403
+ for (const nodeId of [...nodeIds].toSorted()) {
404
+ const rows = registry.list(nodeId).map((r) => ({
405
+ serial: r.serial,
406
+ fingerprintSha256: r.fingerprint,
407
+ notBefore: r.notBefore,
408
+ notAfter: r.notAfter,
409
+ issuedAt: r.issuedAt,
410
+ issuedVia: r.issuedVia,
411
+ status: leafStatus(r, at),
412
+ revokedAt: r.revokedAt,
413
+ revokedReason: r.revokedReason,
414
+ }));
415
+ const current = rows.find((r) => r.status !== 'revoked') ?? null;
416
+ const online = nodeId === HUB_NODE_ID ? true : (liveness.get(nodeId) ?? false);
417
+ const presented = nodeId === HUB_NODE_ID ? undefined : peers.peerLeaf(nodeId);
418
+ nodes.push({
419
+ nodeId,
420
+ online,
421
+ current,
422
+ presentingCurrent: !online || nodeId === HUB_NODE_ID || current === null
423
+ ? null
424
+ : presented?.fingerprint === current.fingerprintSha256,
425
+ history: rows.filter((r) => r !== current),
426
+ });
427
+ }
428
+ }
429
+ return {
430
+ ca,
431
+ revokedSerials: [...crl().list()],
432
+ registryAvailable: registry !== null,
433
+ listener: listenerState(),
434
+ nodes,
435
+ };
436
+ }),
437
+ /**
438
+ * Replace a node's mesh leaf without ever leaving it without one: the node makes a new
439
+ * key and CSR, the hub signs and records it, the node swaps and reconnects, and only
440
+ * after the hub SEES it connected with the new leaf is the old serial revoked. A failed
441
+ * swap keeps the old leaf valid and says why.
442
+ */
443
+ renewMeshOnNode: trpc_middleware_js_1.adminProcedure
444
+ .input(zod_1.z.object({ nodeId: zod_1.z.string().min(1) }))
445
+ .output(MeshRenewResultSchema)
446
+ .mutation(async ({ input }) => {
447
+ const client = requireAgent(input.nodeId);
448
+ const ca = meshCa();
449
+ const registry = meshRegistry();
450
+ if (ca === null || registry === null) {
451
+ throw new server_1.TRPCError({
452
+ code: 'PRECONDITION_FAILED',
453
+ message: 'The mesh CA or the certificate registry is not available yet.',
454
+ });
455
+ }
456
+ const result = await exclusive(input.nodeId, 'mesh', () => (0, system_1.renewMeshCertificate)({
457
+ nodeId: input.nodeId,
458
+ client,
459
+ ca,
460
+ registry,
461
+ crl: crl(),
462
+ peers,
463
+ registrations,
464
+ log: deps.log,
465
+ ...(deps.reconnectTimeoutMs !== undefined
466
+ ? { reconnectTimeoutMs: deps.reconnectTimeoutMs }
467
+ : {}),
468
+ ...(deps.sleep !== undefined ? { sleep: deps.sleep } : {}),
469
+ ...(deps.setTimer !== undefined ? { setTimer: deps.setTimer } : {}),
470
+ }));
471
+ return { ...result, nodeId: input.nodeId };
472
+ }),
473
+ /**
474
+ * Put the node's mesh leaf on the CRL and disconnect it. The node can no longer
475
+ * connect: it must re-join with a new join token. It is NOT forgotten - that stays
476
+ * `clusterNodes.forgetNode`.
477
+ */
478
+ revokeMeshOnNode: trpc_middleware_js_1.adminProcedure
479
+ .input(zod_1.z.object({ nodeId: zod_1.z.string().min(1) }))
480
+ .output(MeshRevokeResultSchema)
481
+ .mutation(async ({ input }) => {
482
+ if (input.nodeId === HUB_NODE_ID) {
483
+ throw new server_1.TRPCError({
484
+ code: 'PRECONDITION_FAILED',
485
+ message: "The hub's own mesh leaf is re-issued at every boot; there is nothing to revoke.",
486
+ });
487
+ }
488
+ const registry = meshRegistry();
489
+ if (registry === null) {
490
+ throw new server_1.TRPCError({
491
+ code: 'PRECONDITION_FAILED',
492
+ message: 'The certificate registry is not available yet.',
493
+ });
494
+ }
495
+ const result = await (0, system_1.revokeMeshCertificate)({
496
+ nodeId: input.nodeId,
497
+ registry,
498
+ crl: crl(),
499
+ peers,
500
+ log: deps.log,
501
+ });
502
+ return { ...result, revokedSerials: [...result.revokedSerials] };
503
+ }),
504
+ });
505
+ }
@@ -12,7 +12,13 @@ function registerMeshEnrollRoute(fastify) {
12
12
  });
13
13
  }
14
14
  const body = request.body ?? {};
15
- const result = await (0, system_1.enrollMeshNode)({ token: body.token ?? '', csr: body.csr ?? '' }, { ca: caState.ca });
15
+ const result = await (0, system_1.enrollMeshNode)({ token: body.token ?? '', csr: body.csr ?? '' }, {
16
+ ca: caState.ca,
17
+ onRecordFailed: (nodeId, error) => fastify.log.warn({ nodeId, error: error instanceof Error ? error.message : String(error) }, 'mesh enroll: the issued leaf could not be recorded'),
18
+ onIssued: async (nodeId, certPem) => {
19
+ await (0, system_1.getProcessMeshCertificateRegistry)()?.recordIssued(nodeId, certPem, 'enroll');
20
+ },
21
+ });
16
22
  if (!result.ok) {
17
23
  return reply.status(result.status).send({ ok: false, error: result.error });
18
24
  }
@@ -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: 167 Skipped (legacy): 3
5
+ // Mounted: 169 Skipped (legacy): 3
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.LEGACY_SHAPE_SKIP = void 0;
8
8
  exports.mountAllCaps = mountAllCaps;
@@ -188,6 +188,7 @@ function mountAllCaps(services) {
188
188
  deviceAdminLink: (0, generated_cap_routers_js_1.createCapRouter_deviceAdminLink)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'device-admin-link'), remoteCapProxy),
189
189
  deviceAdoption: (0, generated_cap_routers_js_1.createCapRouter_deviceAdoption)((_ctx) => reg?.getSingleton('device-adoption') ??
190
190
  null, remoteCapProxy),
191
+ deviceCredentials: (0, generated_cap_routers_js_1.createCapRouter_deviceCredentials)((_ctx) => reg?.getSingleton('device-credentials') ?? null, remoteCapProxy),
191
192
  deviceDiscovery: (0, generated_cap_routers_js_1.createCapRouter_deviceDiscovery)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'device-discovery'), remoteCapProxy),
192
193
  deviceExport: (0, generated_cap_routers_js_1.createCapRouter_deviceExport)((_ctx, addonId) => {
193
194
  if (!reg)
@@ -402,6 +403,8 @@ function mountAllCaps(services) {
402
403
  networkLink: (0, generated_cap_routers_js_1.createCapRouter_networkLink)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'network-link'), remoteCapProxy),
403
404
  networkQuality: (0, generated_cap_routers_js_1.createCapRouter_networkQuality)((_ctx) => reg?.getSingleton('network-quality') ??
404
405
  null, remoteCapProxy),
406
+ nodeCertificates: (0, generated_cap_routers_js_1.createCapRouter_nodeCertificates)((_ctx) => reg?.getSingleton('node-certificates') ??
407
+ null, remoteCapProxy),
405
408
  nodes: (0, generated_cap_routers_js_1.createCapRouter_nodes)((_ctx) => reg?.getSingleton('nodes') ?? null, remoteCapProxy),
406
409
  notificationOutput: (0, generated_cap_routers_js_1.createCapRouter_notificationOutput)((_ctx, addonId) => {
407
410
  if (!reg)