@camstack/system 1.2.432 → 1.2.434

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.
Files changed (105) hide show
  1. package/dist/{tls-CpcGkeJV.mjs → agent-https-B8lKYVtU.mjs} +123 -617
  2. package/dist/{tls-C5IYg3V5.js → agent-https-BZIwS4xF.js} +157 -765
  3. package/dist/auth/device-ca.d.ts +0 -10
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/alerts/alerts.addon.js +1 -1
  9. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  10. package/dist/builtins/autotrack/index.js +1 -1
  11. package/dist/builtins/autotrack/index.mjs +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  13. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  14. package/dist/builtins/calendars/calendars.addon.js +1 -1
  15. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  16. package/dist/builtins/camera-grid/index.js +1 -1
  17. package/dist/builtins/camera-grid/index.mjs +1 -1
  18. package/dist/builtins/composer/composer.addon.js +2 -2
  19. package/dist/builtins/composer/composer.addon.mjs +2 -2
  20. package/dist/builtins/console-logging/index.js +1 -1
  21. package/dist/builtins/console-logging/index.mjs +1 -1
  22. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  23. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  24. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  25. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  26. package/dist/builtins/hub-forwarder/index.js +1 -1
  27. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  28. package/dist/builtins/keychain/index.js +3 -3
  29. package/dist/builtins/keychain/index.mjs +3 -3
  30. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  31. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  32. package/dist/builtins/local-auth/local-auth.addon.js +9 -4
  33. package/dist/builtins/local-auth/local-auth.addon.mjs +9 -4
  34. package/dist/builtins/local-network/address-book.d.ts +13 -0
  35. package/dist/builtins/local-network/agent-mdns.d.ts +51 -0
  36. package/dist/builtins/local-network/local-network.addon.js +74 -53
  37. package/dist/builtins/local-network/local-network.addon.mjs +56 -36
  38. package/dist/builtins/loki-logging/index.js +1 -1
  39. package/dist/builtins/loki-logging/index.mjs +1 -1
  40. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  41. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  42. package/dist/builtins/platform-probe/index.js +1 -1
  43. package/dist/builtins/platform-probe/index.mjs +1 -1
  44. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  45. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  46. package/dist/builtins/snapshot/index.js +1 -1
  47. package/dist/builtins/snapshot/index.mjs +1 -1
  48. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  49. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  50. package/dist/builtins/sqlite-storage/index.js +1 -1
  51. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  52. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +3 -3
  53. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +3 -3
  54. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  55. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  56. package/dist/builtins/system-config/system-config.addon.js +1388 -31
  57. package/dist/builtins/system-config/system-config.addon.mjs +1362 -28
  58. package/dist/builtins/winston-logging/index.js +1 -1
  59. package/dist/builtins/winston-logging/index.mjs +1 -1
  60. package/dist/{composition-sources-CFlUdpO7.js → composition-sources-B7R1oQnY.js} +1 -1
  61. package/dist/{composition-sources-DpbL38z3.mjs → composition-sources-CqrDyHM1.mjs} +1 -1
  62. package/dist/{constants-B89vjrSm.mjs → constants-CrMPWOug.mjs} +2 -2
  63. package/dist/{constants-DfP8V3eX.js → constants-CuOfMdYe.js} +2 -2
  64. package/dist/{device-ca-4IY98a_-.mjs → device-ca-CDjGQQPd.mjs} +17 -6
  65. package/dist/{device-ca-BCyAPmIj.js → device-ca-D21ZP00q.js} +17 -6
  66. package/dist/{dist-BCKOeWu_.js → dist-CCH3XPqO.js} +86 -6
  67. package/dist/{dist-Bt8GTRHB.mjs → dist-CYcBqnXk.mjs} +69 -7
  68. package/dist/file-host-worker.js +1 -1
  69. package/dist/file-host-worker.mjs +1 -1
  70. package/dist/hub-mdns-ciao-CBLvq2oj.js +821 -0
  71. package/dist/hub-mdns-ciao-DJRwtlA8.mjs +648 -0
  72. package/dist/index.d.ts +3 -0
  73. package/dist/index.js +176 -67
  74. package/dist/index.mjs +96 -13
  75. package/dist/kernel/index.d.ts +5 -4
  76. package/dist/kernel/moleculer/agent-mesh-authenticator.d.ts +12 -1
  77. package/dist/kernel/moleculer/agent-ui-unlock.d.ts +40 -0
  78. package/dist/kernel/moleculer/mesh-authenticator.d.ts +6 -0
  79. package/dist/kernel/moleculer/mesh-mtls/constants.d.ts +1 -1
  80. package/dist/kernel/moleculer/mesh-mtls/index.d.ts +3 -2
  81. package/dist/kernel/moleculer/mesh-mtls/mesh-ca.d.ts +4 -9
  82. package/dist/kernel/moleculer/mesh-mtls/mesh-certificate-registry.d.ts +2 -2
  83. package/dist/kernel/moleculer/mesh-mtls/mesh-join-token.d.ts +3 -2
  84. package/dist/kernel/moleculer/mesh-mtls/mesh-peer-transport.d.ts +7 -0
  85. package/dist/kernel/moleculer/mesh-mtls/pairing/agent-pairing.d.ts +204 -0
  86. package/dist/kernel/moleculer/mesh-mtls/pairing/hub-pairing.d.ts +115 -0
  87. package/dist/kernel/moleculer/mesh-mtls/pairing/index.d.ts +3 -0
  88. package/dist/kernel/moleculer/mesh-mtls/pairing/pairing-protocol.d.ts +107 -0
  89. package/dist/kernel/moleculer/mesh-mtls/tls-tcp-transporter.d.ts +18 -0
  90. package/dist/{lib-B_R6sPC3.mjs → lib-BeGzVQcd.mjs} +1 -1
  91. package/dist/{lib-DOJ5yyWA.js → lib-NNxHSOIE.js} +1 -1
  92. package/dist/{restore-archive-Bh34W8J8.mjs → restore-archive-5ml7uYMf.mjs} +1 -1
  93. package/dist/{restore-archive-DhHH7VQp.js → restore-archive-Do0QwAik.js} +1 -1
  94. package/dist/{retired-settings-keys-u3Wwkdrq.js → retired-settings-keys-CEiCaI_j.js} +1 -1
  95. package/dist/{retired-settings-keys-CmcEitC2.mjs → retired-settings-keys-CyDdyjLP.mjs} +1 -1
  96. package/dist/tls/agent-https-names.d.ts +2 -0
  97. package/dist/tls/cert-backdate.d.ts +10 -0
  98. package/dist/tls/cert-issue.d.ts +0 -20
  99. package/dist/{wal-maintenance-uQGuStFW.mjs → wal-maintenance-DgLLPK14.mjs} +0 -0
  100. package/dist/{wal-maintenance-ChzCPUi2.js → wal-maintenance-LZYd8iT-.js} +0 -0
  101. package/dist/{x509.es-CAjSsrkk.mjs → x509.es-CDvj67p1.mjs} +14 -1
  102. package/dist/{x509.es-Dzl0BbmG.js → x509.es-r7Ht1thA.js} +19 -0
  103. package/package.json +1 -1
  104. package/dist/agent-https-names-CpUSuP4w.js +0 -120
  105. package/dist/agent-https-names-uetr4OQl.mjs +0 -91
@@ -1,12 +1,12 @@
1
1
  import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM, r as __exportAll, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
2
- import { Br as hydrateSchema, Fr as defineCustomActions, Gr as isSecretMarker, Pr as customAction, Wr as isSecretClear, _r as BaseAddon, ci as transformSecretWrite, gr as errMsg, ii as secretTreeFromSchema, qr as makeSecretMarker } from "../../dist-Bt8GTRHB.mjs";
2
+ import { Jr as isSecretMarker, Lr as customAction, Rr as defineCustomActions, Ur as hydrateSchema, Xr as makeSecretMarker, br as BaseAddon, di as transformSecretWrite, qr as isSecretClear, si as secretTreeFromSchema, yr as errMsg } from "../../dist-CYcBqnXk.mjs";
3
3
  import { t as require_safe_buffer } from "../../safe-buffer-B1OjL1I9.mjs";
4
- import { a as KeyUsagesExtension, c as Pkcs10CertificateRequestGenerator, d as X509Certificate$1, f as X509CertificateGenerator, i as KeyUsageFlags, n as BasicConstraintsExtension, o as PemConverter, p as cryptoProvider, r as ExtendedKeyUsageExtension, s as Pkcs10CertificateRequest, t as AuthorityKeyIdentifierExtension, u as SubjectKeyIdentifierExtension } from "../../x509.es-CAjSsrkk.mjs";
5
- import { a as setProcessAgentHttpsNames, n as AGENT_HTTPS_NAMES_COLUMNS, r as AgentHttpsNames, t as AGENT_HTTPS_NAMES_COLLECTION } from "../../agent-https-names-uetr4OQl.mjs";
6
- import { a as MESH_CA_KEY_REF, b as setProcessClusterSecretGate, c as MESH_JOIN_TOKEN_MAX_TTL_SEC, d as CLUSTER_SECRET_REF, i as MESH_CA_FIELD, l as SERVER_AUTH_OID, n as DEFAULT_HUB_TLS_TCP_PORT, o as MESH_CA_SETTINGS_KEY, r as MESH_CA_COMMON_NAME, s as MESH_CA_VALIDITY_DAYS, t as CLIENT_AUTH_OID, v as hashClusterSecret } from "../../constants-B89vjrSm.mjs";
4
+ import { a as KeyUsagesExtension, c as Pkcs10CertificateRequestGenerator, d as X509Certificate$1, f as X509CertificateGenerator, g as backdatedNotBefore, i as KeyUsageFlags, n as BasicConstraintsExtension, o as PemConverter, p as cryptoProvider, r as ExtendedKeyUsageExtension, s as Pkcs10CertificateRequest, t as AuthorityKeyIdentifierExtension, u as SubjectKeyIdentifierExtension } from "../../x509.es-CDvj67p1.mjs";
5
+ import { A as AgentHttpsNames, M as setProcessAgentHttpsNames, O as AGENT_HTTPS_NAMES_COLLECTION, d as isPrivateAddress, j as getProcessAgentHttpsNames, k as AGENT_HTTPS_NAMES_COLUMNS, u as isLocalOnlyHostname } from "../../agent-https-B8lKYVtU.mjs";
6
+ import { a as MESH_CA_KEY_REF, b as setProcessClusterSecretGate, c as MESH_JOIN_TOKEN_MAX_TTL_SEC, d as CLUSTER_SECRET_REF, i as MESH_CA_FIELD, l as SERVER_AUTH_OID, n as DEFAULT_HUB_TLS_TCP_PORT, o as MESH_CA_SETTINGS_KEY, r as MESH_CA_COMMON_NAME, s as MESH_CA_VALIDITY_DAYS, t as CLIENT_AUTH_OID, v as hashClusterSecret } from "../../constants-CrMPWOug.mjs";
7
7
  import { t as require_src } from "../../src-hG4grMU4.mjs";
8
8
  import { z } from "zod";
9
- import { X509Certificate, createHash, createHmac, randomBytes, timingSafeEqual, webcrypto } from "node:crypto";
9
+ import { X509Certificate, createHash, createHmac, createPrivateKey, randomBytes, randomInt, sign, timingSafeEqual, verify, webcrypto } from "node:crypto";
10
10
  import { BlockList, isIP } from "node:net";
11
11
  import { lstat, readdir, readlink, realpath } from "node:fs/promises";
12
12
  import { posix, win32 } from "node:path";
@@ -158,6 +158,12 @@ function getProcessMeshAuthStatus() {
158
158
  */
159
159
  var AGENT_UI_UNLOCK_DOMAIN = "camstack-agent-ui-unlock/v1";
160
160
  var AGENT_UI_UNLOCK_VERSION = 1;
161
+ /**
162
+ * D857 (transition release): a ticket signed by the MESH CA key (ES256 JWS) is accepted by an agent
163
+ * IN ADDITION to the HMAC ticket. The removal release mints only this form.
164
+ */
165
+ var AGENT_UI_UNLOCK_TYP = "camstack-agent-ui-unlock+jwt";
166
+ var AGENT_UI_UNLOCK_USE = "agent-ui-unlock";
161
167
  /** Ticket lifetime — long enough for a redirect hop, short enough to steal poorly. */
162
168
  var AGENT_UI_UNLOCK_TTL_MS = 5 * 6e4;
163
169
  /**
@@ -193,11 +199,18 @@ function mintAgentUiUnlockTicket(input) {
193
199
  const payload = {
194
200
  v: 1,
195
201
  nodeId: input.nodeId,
202
+ iat: nowMs,
196
203
  exp: nowMs + ttl,
197
204
  jti: input.jti ?? randomBytes(16).toString("base64url")
198
205
  };
199
206
  const payloadB64 = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
200
- return `${payloadB64}.${sign(input.secretHash, payloadB64)}`;
207
+ return `${payloadB64}.${sign$1(input.secretHash, payloadB64)}`;
208
+ }
209
+ /** A ticket is issued now-ish, lives at most the TTL, and not before `notBeforeMs`. */
210
+ function issuedSanely(payload, nowMs, notBeforeMs) {
211
+ if (payload.iat > nowMs + 6e4) return false;
212
+ if (payload.exp - payload.iat > 3e5) return false;
213
+ return notBeforeMs === void 0 || payload.iat >= notBeforeMs;
201
214
  }
202
215
  /**
203
216
  * Verify a ticket under `secretHash`. Returns the payload, or `null` when the
@@ -209,7 +222,7 @@ function verifyAgentUiUnlockTicket(secretHash, ticket, options) {
209
222
  if (dot <= 0 || dot === ticket.length - 1) return null;
210
223
  const payloadB64 = ticket.slice(0, dot);
211
224
  const sig = ticket.slice(dot + 1);
212
- if (!sigsEqual(sign(secretHash, payloadB64), sig)) return null;
225
+ if (!sigsEqual(sign$1(secretHash, payloadB64), sig)) return null;
213
226
  let parsed;
214
227
  try {
215
228
  parsed = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
@@ -220,6 +233,7 @@ function verifyAgentUiUnlockTicket(secretHash, ticket, options) {
220
233
  if (payload === null) return null;
221
234
  if (payload.exp <= nowMs) return null;
222
235
  if (payload.nodeId !== options.expectedNodeId) return null;
236
+ if (!issuedSanely(payload, nowMs, options.notBeforeMs)) return null;
223
237
  return payload;
224
238
  }
225
239
  /**
@@ -237,11 +251,29 @@ function isAllowedAgentDashboardReturnTo(returnTo, extraOrigins = []) {
237
251
  if (url.protocol !== "http:" && url.protocol !== "https:") return false;
238
252
  if (url.username !== "" || url.password !== "") return false;
239
253
  if (url.pathname !== "/" && url.pathname !== "") return false;
240
- if (url.port === "4444") return true;
254
+ const host = url.hostname.replace(/^\[|\]$/g, "");
255
+ const local = host === "localhost" || isPrivateAddress(host) || isLocalOnlyHostname(host);
256
+ if (url.port === "4444") return local;
241
257
  const origin = url.origin;
242
258
  return extraOrigins.some((o) => o === origin);
243
259
  }
244
260
  /** Parse a stored comma/whitespace-separated origins list. */
261
+ /**
262
+ * True when `returnTo` points at an address the hub itself knows for the node (`knownNames`: its id
263
+ * and the address of its live mesh socket), or at an origin the operator listed. `knownNames` of
264
+ * `null` (nothing recorded for the node yet) refuses.
265
+ */
266
+ function isKnownAgentAddress(returnTo, knownNames, listedOrigins = []) {
267
+ let url;
268
+ try {
269
+ url = new URL(returnTo);
270
+ } catch {
271
+ return false;
272
+ }
273
+ if (listedOrigins.includes(url.origin)) return true;
274
+ if (knownNames === null) return false;
275
+ return knownNames.includes(url.hostname.replace(/^\[|\]$/g, "").toLowerCase());
276
+ }
245
277
  function parseAgentDashboardOrigins(stored) {
246
278
  if (typeof stored !== "string" || stored.trim().length === 0) return [];
247
279
  const out = [];
@@ -255,7 +287,7 @@ function parseAgentDashboardOrigins(stored) {
255
287
  }
256
288
  return out;
257
289
  }
258
- function sign(secretHash, payloadB64) {
290
+ function sign$1(secretHash, payloadB64) {
259
291
  return createHmac("sha256", Buffer.from(secretHash, "utf8")).update(`${AGENT_UI_UNLOCK_DOMAIN}|${payloadB64}`, "utf8").digest("base64url");
260
292
  }
261
293
  function sigsEqual(expected, presented) {
@@ -274,15 +306,108 @@ function parsePayload(value) {
274
306
  if (typeof nodeId !== "string" || nodeId.length === 0) return null;
275
307
  if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
276
308
  if (typeof jti !== "string" || jti.length === 0) return null;
309
+ const iat = rec["iat"];
277
310
  return {
278
311
  v: 1,
279
312
  nodeId,
313
+ iat: typeof iat === "number" && Number.isFinite(iat) ? iat : exp - AGENT_UI_UNLOCK_TTL_MS,
314
+ exp,
315
+ jti
316
+ };
317
+ }
318
+ function b64urlJsonEs256(value) {
319
+ return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
320
+ }
321
+ function mintAgentUiUnlockTicketEs256(input) {
322
+ const nowMs = input.nowMs ?? Date.now();
323
+ const ttl = Math.min(Math.max(1e3, input.ttlMs ?? 3e5), AGENT_UI_UNLOCK_TTL_MS);
324
+ const payload = {
325
+ v: 1,
326
+ use: AGENT_UI_UNLOCK_USE,
327
+ nodeId: input.nodeId,
328
+ iat: nowMs,
329
+ exp: nowMs + ttl,
330
+ jti: input.jti ?? randomBytes(16).toString("base64url")
331
+ };
332
+ const signingInput = `${b64urlJsonEs256({
333
+ alg: "ES256",
334
+ typ: AGENT_UI_UNLOCK_TYP
335
+ })}.${b64urlJsonEs256(payload)}`;
336
+ return `${signingInput}.${sign("sha256", Buffer.from(signingInput, "utf8"), {
337
+ key: createPrivateKey(input.caKeyPem),
338
+ dsaEncoding: "ieee-p1363"
339
+ }).toString("base64url")}`;
340
+ }
341
+ /**
342
+ * Verify a ticket against the mesh CA certificate the agent pinned. Returns the
343
+ * payload, or `null` when the signature, header, shape, expiry or nodeId fails
344
+ * (no reason string — refuse quietly).
345
+ */
346
+ function verifyAgentUiUnlockTicketEs256(caCertPem, ticket, options) {
347
+ const nowMs = options.nowMs ?? Date.now();
348
+ const parts = ticket.split(".");
349
+ if (parts.length !== 3) return null;
350
+ const [headerB64, payloadB64, signatureB64] = parts;
351
+ if (headerB64 === void 0 || payloadB64 === void 0 || signatureB64 === void 0) return null;
352
+ let header;
353
+ let parsed;
354
+ try {
355
+ header = JSON.parse(Buffer.from(headerB64, "base64url").toString("utf8"));
356
+ parsed = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
357
+ } catch {
358
+ return null;
359
+ }
360
+ if (typeof header !== "object" || header === null) return null;
361
+ if (Reflect.get(header, "alg") !== "ES256" || Reflect.get(header, "typ") !== "camstack-agent-ui-unlock+jwt") return null;
362
+ let valid = false;
363
+ try {
364
+ valid = verify("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), {
365
+ key: new X509Certificate(caCertPem).publicKey,
366
+ dsaEncoding: "ieee-p1363"
367
+ }, Buffer.from(signatureB64, "base64url"));
368
+ } catch {
369
+ return null;
370
+ }
371
+ if (!valid) return null;
372
+ const payload = parsePayloadEs256(parsed);
373
+ if (payload === null) return null;
374
+ if (payload.exp <= nowMs) return null;
375
+ if (payload.nodeId !== options.expectedNodeId) return null;
376
+ if (!issuedSanely(payload, nowMs, options.notBeforeMs)) return null;
377
+ return payload;
378
+ }
379
+ function parsePayloadEs256(value) {
380
+ if (typeof value !== "object" || value === null) return null;
381
+ const rec = value;
382
+ if (rec["v"] !== 1) return null;
383
+ if (rec["use"] !== "agent-ui-unlock") return null;
384
+ const nodeId = rec["nodeId"];
385
+ const exp = rec["exp"];
386
+ const jti = rec["jti"];
387
+ if (typeof nodeId !== "string" || nodeId.length === 0) return null;
388
+ if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
389
+ if (typeof jti !== "string" || jti.length === 0) return null;
390
+ const iat = rec["iat"];
391
+ if (typeof iat !== "number" || !Number.isFinite(iat)) return null;
392
+ return {
393
+ v: 1,
394
+ nodeId,
395
+ iat,
280
396
  exp,
281
397
  jti
282
398
  };
283
399
  }
284
400
  //#endregion
285
401
  //#region src/kernel/moleculer/mesh-mtls/mesh-ca.ts
402
+ /**
403
+ * Hub mesh CA — EC P-256, key in the keychain, cert in `cluster.meshCa`.
404
+ *
405
+ * Distinct from the HTTPS local CA (`tls/cert-issue.ts`, RSA): the mesh CA
406
+ * signs node leaves (CN = nodeID) and join-token JWS (ES256). Issuance uses
407
+ * `@peculiar/x509` over WebCrypto — same library the HTTPS path already uses
408
+ * (installed `1.14.3`; registry latest at adoption time was `2.1.0` — stay on
409
+ * the pinned range already shipping).
410
+ */
286
411
  cryptoProvider.set(webcrypto);
287
412
  var MS_PER_DAY = 864e5;
288
413
  /** Key generation — EC P-256 (plan). WebCrypto DOM names are not in @types/node. */
@@ -295,6 +420,14 @@ var MESH_SIGNING_ALGORITHM = {
295
420
  name: "ECDSA",
296
421
  hash: "SHA-256"
297
422
  };
423
+ /** A node id: letters, digits, `.`, `_`, `-`; at most 63 characters; starts with a letter or digit. */
424
+ var NODE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,62}$/i;
425
+ /** Ids no agent may take: the hub's own, whatever the case (D857). */
426
+ var RESERVED_NODE_IDS = new Set(["hub"]);
427
+ /** True for an id an agent may carry: the pattern, and not a reserved name. */
428
+ function isValidAgentNodeId(nodeId) {
429
+ return NODE_ID_PATTERN.test(nodeId) && !RESERVED_NODE_IDS.has(nodeId.toLowerCase());
430
+ }
298
431
  function freshSerial() {
299
432
  return `00${randomBytes(16).toString("hex")}`;
300
433
  }
@@ -325,7 +458,7 @@ async function generateMeshCa(now = /* @__PURE__ */ new Date()) {
325
458
  const cert = await X509CertificateGenerator.createSelfSigned({
326
459
  serialNumber: freshSerial(),
327
460
  name: `CN=${MESH_CA_COMMON_NAME}`,
328
- notBefore: now,
461
+ notBefore: backdatedNotBefore(now),
329
462
  notAfter: new Date(now.getTime() + MESH_CA_VALIDITY_DAYS * MS_PER_DAY),
330
463
  keys,
331
464
  signingAlgorithm: MESH_SIGNING_ALGORITHM,
@@ -358,6 +491,7 @@ async function issueMeshLeafFromCsr(ca, csrPem, opts = {}) {
358
491
  if (!await csr.verify(webcrypto)) throw new Error("CSR signature is invalid");
359
492
  const cn = subjectCn(csr.subject);
360
493
  if (cn === null || cn.length === 0) throw new Error("CSR subject has no CN");
494
+ if (opts.allowReserved !== true && !isValidAgentNodeId(cn)) throw new Error(`CSR CN '${cn}' is not an id an agent may carry`);
361
495
  if (opts.expectedNodeId !== void 0 && opts.expectedNodeId !== cn) throw new Error(`CSR CN '${cn}' does not match the token nodeId '${opts.expectedNodeId}'`);
362
496
  const now = opts.now ?? /* @__PURE__ */ new Date();
363
497
  const validDays = opts.validDays ?? 30;
@@ -375,7 +509,7 @@ async function issueMeshLeafFromCsr(ca, csrPem, opts = {}) {
375
509
  serialNumber,
376
510
  subject: `CN=${cn}`,
377
511
  issuer: ca.cert.subject,
378
- notBefore: now,
512
+ notBefore: backdatedNotBefore(now),
379
513
  notAfter,
380
514
  publicKey: csr.publicKey,
381
515
  signingKey: ca.privateKey,
@@ -394,7 +528,8 @@ async function issueMeshLeafForNode(ca, nodeId, opts = {}) {
394
528
  ...await issueMeshLeafFromCsr(ca, await createMeshCsrPem(keys, nodeId), {
395
529
  expectedNodeId: nodeId,
396
530
  now: opts.now,
397
- validDays: opts.validDays
531
+ validDays: opts.validDays,
532
+ allowReserved: true
398
533
  }),
399
534
  keyPem: await exportKeyPem(keys.privateKey)
400
535
  };
@@ -684,6 +819,7 @@ var RETENTION_MS = 90 * 864e5;
684
819
  var RENEWAL_ABANDONED = "renewal-abandoned";
685
820
  var VIAS = [
686
821
  "enroll",
822
+ "pairing",
687
823
  "renewal",
688
824
  "boot",
689
825
  "observed"
@@ -857,9 +993,10 @@ var MeshCertificateRegistry = class {
857
993
  * valid just because the hub once set it aside). Returns the serials put on the CRL.
858
994
  * A leaf revoked this way is never reinstated by `recordObserved`.
859
995
  */
860
- async revokeAllForNode(nodeId, reason, crl) {
996
+ async revokeAllForNode(nodeId, reason, crl, keepFingerprint) {
861
997
  const serials = [];
862
998
  for (const record of this.list(nodeId)) {
999
+ if (keepFingerprint !== void 0 && record.fingerprint === keepFingerprint) continue;
863
1000
  if (record.revokedAt !== null && record.revokedReason !== "renewal-abandoned") continue;
864
1001
  const revoked = {
865
1002
  ...record,
@@ -1309,6 +1446,10 @@ function getProcessNodeMeshTransport(nodeId) {
1309
1446
  function getProcessPeerLeaf(nodeId) {
1310
1447
  return getProcessMeshPeerTransportLookup()?.peerLeaf?.(nodeId);
1311
1448
  }
1449
+ /** True when the hub's transporter holds a certificate-verified mesh peer called `nodeId` (D857). */
1450
+ function isProcessVerifiedMeshPeer(nodeId) {
1451
+ return getProcessMeshPeerTransportLookup()?.isVerifiedPeer?.(nodeId) === true;
1452
+ }
1312
1453
  /** Disconnect `nodeId` (closes its mutual-TLS sockets). `0` when none were open. */
1313
1454
  function dropProcessPeer(nodeId) {
1314
1455
  return getProcessMeshPeerTransportLookup()?.dropPeer?.(nodeId) ?? 0;
@@ -2017,7 +2158,8 @@ var SignJWT = class extends SignJWT_base {
2017
2158
  /**
2018
2159
  * Mesh join tokens — single-use JWS ES256 signed with the hub mesh CA key.
2019
2160
  *
2020
- * Payload: `{ v, hubUrl, hubMeshAddress, caFingerprint, nodeId?, jti, exp }`.
2161
+ * Payload: `{ v, hubUrl, hubMeshAddress, caFingerprint, nodeId, jti, exp }`. The node id is MANDATORY
2162
+ * (D857): a token that does not name the node it is for would let its holder pick any id, `hub` included.
2021
2163
  * `exp` ≤ now + 1 h. `jti` is recorded until expiry so a token cannot be reused.
2022
2164
  */
2023
2165
  /** In-memory + optional persist of spent `jti` until `exp`. */
@@ -2048,6 +2190,7 @@ function processJoinTokenJtiStore() {
2048
2190
  return processJtiStore;
2049
2191
  }
2050
2192
  async function mintMeshJoinToken(input) {
2193
+ if (!isValidAgentNodeId(input.nodeId)) throw new Error(`a join token needs the node id it is for; "${input.nodeId}" is not one an agent may carry`);
2051
2194
  const now = input.now ?? /* @__PURE__ */ new Date();
2052
2195
  const nowSec = Math.floor(now.getTime() / 1e3);
2053
2196
  const exp = nowSec + Math.min(Math.max(1, input.ttlSec ?? 3600), MESH_JOIN_TOKEN_MAX_TTL_SEC);
@@ -2057,7 +2200,7 @@ async function mintMeshJoinToken(input) {
2057
2200
  hubUrl: input.hubUrl,
2058
2201
  hubMeshAddress: input.hubMeshAddress,
2059
2202
  caFingerprint: caFingerprintCompact(input.ca.certPem),
2060
- ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
2203
+ nodeId: input.nodeId,
2061
2204
  jti,
2062
2205
  exp
2063
2206
  }).setProtectedHeader({
@@ -2085,7 +2228,7 @@ async function verifyMeshJoinToken(input) {
2085
2228
  reason: `token signature invalid: ${msg}`
2086
2229
  };
2087
2230
  }
2088
- const claims = parseClaims(payload);
2231
+ const claims = parseClaims$1(payload);
2089
2232
  if (claims === null) return {
2090
2233
  ok: false,
2091
2234
  reason: "token claims malformed"
@@ -2121,7 +2264,7 @@ async function verifyMeshJoinToken(input) {
2121
2264
  function normalizeFp(fp) {
2122
2265
  return fp.replace(/:/g, "").toLowerCase();
2123
2266
  }
2124
- function parseClaims(payload) {
2267
+ function parseClaims$1(payload) {
2125
2268
  if (payload["v"] !== 1) return null;
2126
2269
  const hubUrl = payload["hubUrl"];
2127
2270
  const hubMeshAddress = payload["hubMeshAddress"];
@@ -2134,12 +2277,13 @@ function parseClaims(payload) {
2134
2277
  if (typeof jti !== "string" || jti.length === 0) return null;
2135
2278
  if (exp === null) return null;
2136
2279
  const nodeId = payload["nodeId"];
2280
+ if (typeof nodeId !== "string" || !isValidAgentNodeId(nodeId)) return null;
2137
2281
  return {
2138
2282
  v: 1,
2139
2283
  hubUrl,
2140
2284
  hubMeshAddress,
2141
2285
  caFingerprint: normalizeFp(caFingerprint),
2142
- ...typeof nodeId === "string" && nodeId.length > 0 ? { nodeId } : {},
2286
+ nodeId,
2143
2287
  jti,
2144
2288
  exp
2145
2289
  };
@@ -87715,6 +87859,26 @@ var TlsTcpTransporter = class extends TcpTransporterBase {
87715
87859
  return this.tlsPeers.has(nodeId) ? "tls" : "clear";
87716
87860
  }
87717
87861
  /**
87862
+ * The certificate identity of `nodeId` (D857): its mutual-TLS handshake chained to the
87863
+ * mesh CA this transporter was built with, its leaf CN is `nodeId`, and the leaf is not
87864
+ * revoked now. This is the ONLY proof of "who is calling" the mesh gates accept:
87865
+ * every inbound packet on a TLS socket is dropped unless its `sender` equals the leaf CN
87866
+ * (see `onIncomingMessage`), so a node id in a delivered packet that is verified here was
87867
+ * claimed by the holder of that node's private key.
87868
+ *
87869
+ * `undefined` for a node that reached us over clear text, never completed a handshake,
87870
+ * or whose serial is on the CRL.
87871
+ */
87872
+ verifiedPeer(nodeId) {
87873
+ const serial = this.tlsPeers.get(nodeId);
87874
+ if (serial === void 0) return void 0;
87875
+ if (serial.length > 0 && this.crl.isRevoked(serial)) return void 0;
87876
+ return {
87877
+ nodeId,
87878
+ serial
87879
+ };
87880
+ }
87881
+ /**
87718
87882
  * The node disconnected: its next connection decides its transport again —
87719
87883
  * unless it already re-handshook (the disconnect can be noticed after the
87720
87884
  * restarted node is back), in which case its live TLS socket keeps it `tls`.
@@ -87955,6 +88119,1174 @@ function isTlsSocket(socket) {
87955
88119
  return typeof socket.getPeerCertificate === "function" && typeof socket.encrypted === "boolean" && socket.encrypted === true;
87956
88120
  }
87957
88121
  //#endregion
88122
+ //#region src/kernel/moleculer/mesh-mtls/pairing/pairing-protocol.ts
88123
+ /**
88124
+ * Agent pairing — the cryptographic core (D857).
88125
+ *
88126
+ * Pairing makes an agent hold a certificate from the hub's mesh CA, with no
88127
+ * shared secret anywhere. The hub starts it (LAN discovery or a typed
88128
+ * address); the operator confirms the SAME six digits on the hub and in the
88129
+ * agent's own page. Everything the human compares is derived here.
88130
+ *
88131
+ * hub → agent offer { hub CA cert, commit = H(caFp ‖ hubNonce), hubUrl, hubMeshAddress }
88132
+ * agent → hub answer { pairingId, CSR (mesh key made on the agent), agentNonce }
88133
+ * hub → agent reveal { hubNonce } (agent checks it against the commit)
88134
+ * both SAS = 6 digits of H(caFp ‖ agentPublicKey ‖ hubNonce ‖ agentNonce)
88135
+ * operator reads the code on one screen and types it on the other, on both sides
88136
+ * hub → agent complete { leaf, proof } (only after both confirmations)
88137
+ *
88138
+ * Why a commitment: a six-digit code is 20 bits. An attacker between the two
88139
+ * who could still choose a value AFTER seeing the others could grind a CA key
88140
+ * whose code equals the real one in a million tries. The hub therefore commits
88141
+ * to its nonce before it sees the agent's answer and reveals it only after,
88142
+ * so nothing the attacker controls is chosen after everything the code is
88143
+ * derived from is fixed. A substituted CA changes the code with probability
88144
+ * 999 999 / 1 000 000 per attempt, and a wrong code ends the pairing after
88145
+ * three tries.
88146
+ *
88147
+ * `proof` is an ES256 JWS by the mesh CA key over the transcript (pairing id,
88148
+ * node id, both nonces, the leaf fingerprint, the CA fingerprint). The agent
88149
+ * verifies it with the CA certificate it was OFFERED and the operator
88150
+ * confirmed — never with one that arrives in the same message.
88151
+ */
88152
+ var PAIRING_PROTOCOL = 1;
88153
+ var PAIRING_DOMAIN = "camstack-pairing/v1";
88154
+ var PAIRING_PROOF_TYP = "camstack-pairing-complete+jwt";
88155
+ var PAIRING_PROOF_USE = "pairing-complete";
88156
+ var PAIRING_OFFER_TYP = "camstack-pairing-offer+jwt";
88157
+ var PAIRING_OFFER_USE = "pairing-offer";
88158
+ /** An offer signature older than this (or from further ahead than 60 s) is refused. */
88159
+ var PAIRING_OFFER_MAX_AGE_MS = 2 * 6e4;
88160
+ /** A pairing that is not confirmed on both sides within this window is cancelled on both. */
88161
+ var PAIRING_TTL_MS = 5 * 6e4;
88162
+ /** Wrong codes typed before the pairing is cancelled. */
88163
+ var PAIRING_MAX_CODE_ATTEMPTS = 3;
88164
+ var PAIRING_CODE_DIGITS = 6;
88165
+ /** Pending pairings an agent holds at once, one per hub CA (offer squatting, D857). */
88166
+ var PAIRING_MAX_SESSIONS = 4;
88167
+ /** The local PIN: single use, shown only on the agent's own machine. */
88168
+ var PAIRING_PIN_DIGITS = 8;
88169
+ /** A pairing proof is valid this long from `iat`, never longer. */
88170
+ var PAIRING_PROOF_MAX_LIFETIME_MS = 5 * 6e4;
88171
+ /** Agent endpoints, all under the agent API (`:4444`). */
88172
+ var PAIRING_PATHS = {
88173
+ hello: "/api/agent/pairing/hello",
88174
+ offer: "/api/agent/pairing/offer",
88175
+ reveal: "/api/agent/pairing/reveal",
88176
+ /** Public: who asks to pair - never a code, a pairing id or a PIN. */
88177
+ status: "/api/agent/pairing/status",
88178
+ /** The hub's poll, authorized by the pairing id: did the agent's operator confirm? */
88179
+ poll: "/api/agent/pairing/poll",
88180
+ complete: "/api/agent/pairing/complete",
88181
+ /** The agent's operator: the hub's code + the local PIN (or genuine loopback). */
88182
+ confirm: "/api/agent/pairing/confirm",
88183
+ reject: "/api/agent/pairing/reject",
88184
+ /** The hub withdraws its pairing, authorized by the pairing id. */
88185
+ cancel: "/api/agent/pairing/cancel",
88186
+ /** The agent's operator clears lockouts and pending offers (loopback, or the local reset code). */
88187
+ reset: "/api/agent/pairing/reset",
88188
+ forget: "/api/agent/pairing/forget"
88189
+ };
88190
+ /** A fresh single-use local PIN: digits only, uniformly random. */
88191
+ function randomPin() {
88192
+ return String(randomInt(0, 10 ** 8)).padStart(8, "0");
88193
+ }
88194
+ function randomHex(bytes) {
88195
+ return randomBytes(bytes).toString("hex");
88196
+ }
88197
+ function newPairingId() {
88198
+ return randomBytes(16).toString("base64url");
88199
+ }
88200
+ /** The hub's commitment to its nonce, bound to the CA certificate it offers. */
88201
+ function computePairingCommitment(caCertPem, hubNonce) {
88202
+ return createHash("sha256").update(`${PAIRING_DOMAIN}|commit|${caFingerprintCompact(caCertPem)}|${hubNonce}`, "utf8").digest("hex");
88203
+ }
88204
+ function commitmentsEqual(expected, presented) {
88205
+ return constantTimeEqual(expected, presented);
88206
+ }
88207
+ /** The six digits both screens show. */
88208
+ function computePairingSas(input) {
88209
+ const value = createHash("sha256").update([
88210
+ PAIRING_DOMAIN,
88211
+ "sas",
88212
+ caFingerprintCompact(input.caCertPem),
88213
+ input.agentPublicKeySpki,
88214
+ input.hubNonce,
88215
+ input.agentNonce
88216
+ ].join("|"), "utf8").digest().readUInt32BE(0) % 10 ** 6;
88217
+ return String(value).padStart(6, "0");
88218
+ }
88219
+ function constantTimeEqual(expected, presented) {
88220
+ if (typeof presented !== "string") return false;
88221
+ const a = Buffer.from(expected, "utf8");
88222
+ const b = Buffer.from(presented, "utf8");
88223
+ return a.length === b.length && timingSafeEqual(a, b);
88224
+ }
88225
+ async function describeCsr(csrPem) {
88226
+ const csr = new Pkcs10CertificateRequest(csrPem);
88227
+ if (!await csr.verify(webcrypto)) throw new Error("the agent CSR signature is invalid");
88228
+ const cn = subjectCn(csr.subject);
88229
+ if (cn === null || cn.length === 0) throw new Error("the agent CSR has no CN");
88230
+ return {
88231
+ commonName: cn,
88232
+ publicKeySpki: Buffer.from(csr.publicKey.rawData).toString("base64")
88233
+ };
88234
+ }
88235
+ function b64urlJson(value) {
88236
+ return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
88237
+ }
88238
+ function leafFingerprintCompact(certPem) {
88239
+ return createHash("sha256").update(new X509Certificate(certPem).raw).digest("hex");
88240
+ }
88241
+ function signPairingProof(caKeyPem, claims) {
88242
+ const signingInput = `${b64urlJson({
88243
+ alg: "ES256",
88244
+ typ: PAIRING_PROOF_TYP
88245
+ })}.${b64urlJson(claims)}`;
88246
+ return `${signingInput}.${sign("sha256", Buffer.from(signingInput, "utf8"), {
88247
+ key: createPrivateKey(caKeyPem),
88248
+ dsaEncoding: "ieee-p1363"
88249
+ }).toString("base64url")}`;
88250
+ }
88251
+ /**
88252
+ * The claims of `proof` when it was signed by `caCertPem`'s key, else `null`.
88253
+ * Shape, expiry and the binding to a session are the caller's to check.
88254
+ */
88255
+ function verifyPairingProof(caCertPem, proof) {
88256
+ const parts = proof.split(".");
88257
+ if (parts.length !== 3) return null;
88258
+ const [headerB64, payloadB64, signatureB64] = parts;
88259
+ if (headerB64 === void 0 || payloadB64 === void 0 || signatureB64 === void 0) return null;
88260
+ let header;
88261
+ let payload;
88262
+ try {
88263
+ header = JSON.parse(Buffer.from(headerB64, "base64url").toString("utf8"));
88264
+ payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
88265
+ } catch {
88266
+ return null;
88267
+ }
88268
+ if (header === null || typeof header !== "object") return null;
88269
+ if (Reflect.get(header, "alg") !== "ES256" || Reflect.get(header, "typ") !== "camstack-pairing-complete+jwt") return null;
88270
+ let valid = false;
88271
+ try {
88272
+ valid = verify("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), {
88273
+ key: new X509Certificate(caCertPem).publicKey,
88274
+ dsaEncoding: "ieee-p1363"
88275
+ }, Buffer.from(signatureB64, "base64url"));
88276
+ } catch {
88277
+ return null;
88278
+ }
88279
+ return valid ? parseClaims(payload) : null;
88280
+ }
88281
+ function parseClaims(value) {
88282
+ if (value === null || typeof value !== "object") return null;
88283
+ const str = (key) => {
88284
+ const v = Reflect.get(value, key);
88285
+ return typeof v === "string" && v.length > 0 ? v : null;
88286
+ };
88287
+ const pairingId = str("pairingId");
88288
+ const nodeId = str("nodeId");
88289
+ const hubNonce = str("hubNonce");
88290
+ const agentNonce = str("agentNonce");
88291
+ const leafFingerprint = str("leafFingerprint");
88292
+ const caFingerprint = str("caFingerprint");
88293
+ const hubMeshAddress = str("hubMeshAddress");
88294
+ const hubUrl = str("hubUrl");
88295
+ const exp = Reflect.get(value, "exp");
88296
+ const iat = Reflect.get(value, "iat");
88297
+ if (Reflect.get(value, "v") !== 1) return null;
88298
+ if (Reflect.get(value, "use") !== "pairing-complete") return null;
88299
+ if (pairingId === null || nodeId === null || hubNonce === null || agentNonce === null || leafFingerprint === null || caFingerprint === null || hubMeshAddress === null || hubUrl === null || typeof exp !== "number" || !Number.isFinite(exp) || typeof iat !== "number" || !Number.isFinite(iat)) return null;
88300
+ return {
88301
+ v: 1,
88302
+ use: PAIRING_PROOF_USE,
88303
+ pairingId,
88304
+ nodeId,
88305
+ hubNonce,
88306
+ agentNonce,
88307
+ leafFingerprint,
88308
+ caFingerprint,
88309
+ hubMeshAddress,
88310
+ hubUrl,
88311
+ iat,
88312
+ exp
88313
+ };
88314
+ }
88315
+ /** A short public fingerprint of a public key, for the mDNS TXT record. */
88316
+ function keyFingerprintShort(publicKeySpkiB64) {
88317
+ return createHash("sha256").update(Buffer.from(publicKeySpkiB64, "base64")).digest("hex").slice(0, 16);
88318
+ }
88319
+ function signPairingOffer(caKeyPem, claims) {
88320
+ const signingInput = `${b64urlJson({
88321
+ alg: "ES256",
88322
+ typ: PAIRING_OFFER_TYP
88323
+ })}.${b64urlJson(claims)}`;
88324
+ return `${signingInput}.${sign("sha256", Buffer.from(signingInput, "utf8"), {
88325
+ key: createPrivateKey(caKeyPem),
88326
+ dsaEncoding: "ieee-p1363"
88327
+ }).toString("base64url")}`;
88328
+ }
88329
+ /** The claims of `token` when `caCertPem`'s key signed it as a pairing offer, else `null`. */
88330
+ function verifyPairingOffer(caCertPem, token) {
88331
+ const parts = token.split(".");
88332
+ if (parts.length !== 3) return null;
88333
+ const [headerB64, payloadB64, signatureB64] = parts;
88334
+ if (headerB64 === void 0 || payloadB64 === void 0 || signatureB64 === void 0) return null;
88335
+ let header;
88336
+ let payload;
88337
+ try {
88338
+ header = JSON.parse(Buffer.from(headerB64, "base64url").toString("utf8"));
88339
+ payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
88340
+ } catch {
88341
+ return null;
88342
+ }
88343
+ if (header === null || typeof header !== "object") return null;
88344
+ if (Reflect.get(header, "alg") !== "ES256" || Reflect.get(header, "typ") !== "camstack-pairing-offer+jwt") return null;
88345
+ let valid = false;
88346
+ try {
88347
+ valid = verify("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), {
88348
+ key: new X509Certificate(caCertPem).publicKey,
88349
+ dsaEncoding: "ieee-p1363"
88350
+ }, Buffer.from(signatureB64, "base64url"));
88351
+ } catch {
88352
+ return null;
88353
+ }
88354
+ if (!valid || payload === null || typeof payload !== "object") return null;
88355
+ const str = (key) => {
88356
+ const v = Reflect.get(payload, key);
88357
+ return typeof v === "string" && v.length > 0 ? v : null;
88358
+ };
88359
+ const iat = Reflect.get(payload, "iat");
88360
+ const agentNodeId = str("agentNodeId");
88361
+ const hubNodeId = str("hubNodeId");
88362
+ const caFingerprint = str("caFingerprint");
88363
+ const commitment = str("commitment");
88364
+ const hubUrl = str("hubUrl");
88365
+ const hubMeshAddress = str("hubMeshAddress");
88366
+ if (Reflect.get(payload, "use") !== "pairing-offer") return null;
88367
+ if (typeof iat !== "number" || !Number.isFinite(iat)) return null;
88368
+ if (agentNodeId === null || hubNodeId === null || caFingerprint === null || commitment === null || hubUrl === null || hubMeshAddress === null) return null;
88369
+ return {
88370
+ use: PAIRING_OFFER_USE,
88371
+ agentNodeId,
88372
+ hubNodeId,
88373
+ caFingerprint,
88374
+ commitment,
88375
+ hubUrl,
88376
+ hubMeshAddress,
88377
+ iat
88378
+ };
88379
+ }
88380
+ //#endregion
88381
+ //#region src/kernel/moleculer/mesh-mtls/pairing/agent-pairing.ts
88382
+ /**
88383
+ * The agent's side of pairing (D857): up to four pending pairings (one per hub CA), their keys
88384
+ * in memory only, installed ONLY after the agent's own operator confirmed the same six digits
88385
+ * the hub shows AND proved presence at this agent with a one-time local PIN, and the hub's
88386
+ * transcript proof verified under the CA the agent was offered.
88387
+ *
88388
+ * Why a PIN: the agent's page is opened from another PC and its API is reachable by the whole
88389
+ * LAN. Anything the network can read, the network can use to "confirm" - so the code, the pairing
88390
+ * id and the PIN are shown ONLY on the agent's own machine (its console, a 0600 file, the local
88391
+ * window). A request that is not genuinely local must present the PIN of the pairing it confirms.
88392
+ *
88393
+ * unpaired ──offer──▶ pending ──reveal──▶ (code known) ──confirm(code + PIN) + complete──▶ paired
88394
+ * │ │
88395
+ * └── reject / expiry / 3 wrong codes / 3 wrong PINs ──▶ unpaired
88396
+ *
88397
+ * Offers are rationed: pairings that ended without the local operator confirming them count
88398
+ * against a budget (3 per 10 minutes, then exponential lockout) and against the hub CA that made
88399
+ * them (so a loop of "offer, read the code, abort" cannot grind the six digits). A paired agent
88400
+ * refuses every offer.
88401
+ */
88402
+ /** Abandoned offers allowed in {@link BUDGET_WINDOW_MS} before the lockout starts. */
88403
+ var BUDGET_OFFERS = 3;
88404
+ var BUDGET_WINDOW_MS = 10 * 6e4;
88405
+ var LOCKOUT_BASE_MS = 5 * 6e4;
88406
+ var LOCKOUT_MAX_MS = 60 * 6e4;
88407
+ /** Per hub CA: nothing for the first abandonment, then this x 2^(n-2), capped. */
88408
+ var CA_BACKOFF_BASE_MS = 3e4;
88409
+ var CA_BACKOFF_MAX_MS = 10 * 6e4;
88410
+ /** Wrong PINs (any session) before confirming is locked for a while. */
88411
+ var PIN_FAILURE_LIMIT = 5;
88412
+ var PIN_LOCK_MS = 2 * 6e4;
88413
+ var PIN_LOCK_MAX_MS = 60 * 6e4;
88414
+ /** Wrong PINs across ALL sources within one pairing lifetime before every pending pairing is cancelled. */
88415
+ var PIN_GLOBAL_CEILING = 200;
88416
+ var MAX_TRACKED_SOURCES = 256;
88417
+ /** Outcomes kept by pairing id so the hub can learn how its pairing ended. */
88418
+ var OUTCOME_LIMIT = 16;
88419
+ var OUTCOME_TTL_MS = 15 * 6e4;
88420
+ var MAX_FIELD = 2048;
88421
+ function fail(status, error, message, retryAfterMs) {
88422
+ return {
88423
+ ok: false,
88424
+ status,
88425
+ error,
88426
+ message,
88427
+ ...retryAfterMs !== void 0 ? { retryAfterMs } : {}
88428
+ };
88429
+ }
88430
+ function isText(value, max = MAX_FIELD) {
88431
+ return typeof value === "string" && value.length > 0 && value.length <= max;
88432
+ }
88433
+ function isHttpUrl(value) {
88434
+ try {
88435
+ const u = new URL(value);
88436
+ return u.protocol === "https:" || u.protocol === "http:";
88437
+ } catch {
88438
+ return false;
88439
+ }
88440
+ }
88441
+ /**
88442
+ * Clock skew tolerated between the hub that minted a CA and this agent. X.509 dates have
88443
+ * one-second resolution and a fresh CA's notBefore is the hub's "now", so an agent whose clock
88444
+ * is even a moment behind would otherwise refuse a brand-new CA. Same bound as the offer `iat`.
88445
+ */
88446
+ var CA_CLOCK_SKEW_MS = 6e4;
88447
+ /** The hub CA must be a self-signed CA certificate, valid now (give or take the skew). */
88448
+ function readCaCertificate(pem, nowMs) {
88449
+ try {
88450
+ const cert = new X509Certificate(pem);
88451
+ if (!cert.ca || !cert.verify(cert.publicKey)) return null;
88452
+ if (Date.parse(cert.validTo) <= nowMs || Date.parse(cert.validFrom) > nowMs + CA_CLOCK_SKEW_MS) return null;
88453
+ return cert;
88454
+ } catch {
88455
+ return null;
88456
+ }
88457
+ }
88458
+ /** `host:6443` / `[::1]:6443` / `host` → the host, port dropped. */
88459
+ function hubHostFromMeshAddress(address) {
88460
+ const trimmed = address.trim();
88461
+ const bracketed = /^(\[[^\]]+\])(?::\d+)?$/.exec(trimmed);
88462
+ if (bracketed?.[1] !== void 0) return bracketed[1];
88463
+ return /^([^:]+):\d+$/.exec(trimmed)?.[1] ?? trimmed;
88464
+ }
88465
+ var AgentPairing = class {
88466
+ deps;
88467
+ sessions = /* @__PURE__ */ new Map();
88468
+ /** Outcome of each recent pairing BY id (bounded): ending others never overwrites an answer. */
88469
+ outcomes = /* @__PURE__ */ new Map();
88470
+ /** When pairings that never reached local confirmation ended. */
88471
+ abandoned = [];
88472
+ lockedUntil = 0;
88473
+ perCa = /* @__PURE__ */ new Map();
88474
+ pinBySource = /* @__PURE__ */ new Map();
88475
+ globalPinFailures = [];
88476
+ resetCode;
88477
+ /** Commitments already offered (signed offers are single use) until their signature is stale. */
88478
+ seenCommitments = /* @__PURE__ */ new Map();
88479
+ now;
88480
+ constructor(deps) {
88481
+ this.deps = deps;
88482
+ this.now = deps.now ?? Date.now;
88483
+ this.resetCode = randomPin();
88484
+ deps.onResetCode?.(this.resetCode);
88485
+ }
88486
+ hello() {
88487
+ this.expireIfDue();
88488
+ const expiries = [...this.sessions.values()].map((s) => s.expiresAt);
88489
+ return {
88490
+ protocol: 1,
88491
+ nodeId: this.deps.nodeId,
88492
+ version: this.deps.version,
88493
+ state: this.state(),
88494
+ pendingExpiresAt: expiries.length === 0 ? null : Math.min(...expiries)
88495
+ };
88496
+ }
88497
+ state() {
88498
+ this.expireIfDue();
88499
+ if (this.deps.isPaired()) return "paired";
88500
+ return this.sessions.size > 0 ? "pending" : "unpaired";
88501
+ }
88502
+ /** Who is asking to pair, and until when. No code, no pairing id, no PIN. */
88503
+ publicView() {
88504
+ this.expireIfDue();
88505
+ return {
88506
+ state: this.state(),
88507
+ pending: [...this.sessions.values()].map((s) => ({
88508
+ hub: {
88509
+ nodeId: s.hub.nodeId,
88510
+ url: s.hub.url,
88511
+ caFingerprint: s.hub.caFingerprint
88512
+ },
88513
+ expiresAt: s.expiresAt
88514
+ }))
88515
+ };
88516
+ }
88517
+ /** The view for the agent's own machine: also the pairing ids, the codes and the PINs. */
88518
+ localView() {
88519
+ this.expireIfDue();
88520
+ return {
88521
+ state: this.state(),
88522
+ pending: [...this.sessions.values()].map((s) => ({
88523
+ pairingId: s.pairingId,
88524
+ hub: {
88525
+ nodeId: s.hub.nodeId,
88526
+ url: s.hub.url,
88527
+ caFingerprint: s.hub.caFingerprint
88528
+ },
88529
+ expiresAt: s.expiresAt,
88530
+ pin: s.pin,
88531
+ code: s.code,
88532
+ localConfirmed: s.localConfirmed
88533
+ }))
88534
+ };
88535
+ }
88536
+ async offer(body) {
88537
+ this.expireIfDue();
88538
+ if (this.deps.isPaired()) return fail(409, "already-paired", "this agent is paired — unpair it first (from the hub or here)");
88539
+ if (!isValidAgentNodeId(this.deps.nodeId)) return fail(409, "invalid-node-id", `this agent's node id "${this.deps.nodeId}" cannot be paired - rename the agent`);
88540
+ const request = parseOffer(body);
88541
+ if (request === null) return fail(400, "bad-offer", "the offer is malformed");
88542
+ if (request.protocol !== 1) return fail(400, "unsupported-protocol", `pairing protocol ${String(request.protocol)} is not supported`);
88543
+ if (readCaCertificate(request.hubCaCertPem, this.now()) === null) return fail(400, "bad-ca", "the offered CA is not a valid self-signed CA certificate");
88544
+ if (!/^[0-9a-f]{64}$/.test(request.commitment)) return fail(400, "bad-offer", "the commitment is malformed");
88545
+ if (!isHttpUrl(request.hubUrl)) return fail(400, "bad-offer", "hubUrl is not an http(s) URL");
88546
+ const caFingerprint = caFingerprintCompact(request.hubCaCertPem);
88547
+ const refused = this.checkOfferSignature(request, caFingerprint);
88548
+ if (refused !== null) return refused;
88549
+ const rationed = this.rationOffer(caFingerprint);
88550
+ if (rationed !== null) return rationed;
88551
+ for (const existing of [...this.sessions.values()]) if (existing.hub.caFingerprint === caFingerprint) this.end(existing, "failed");
88552
+ if (this.sessions.size >= 4 && !this.makeRoom()) return fail(409, "too-many-pending", "too many pairings are pending on this agent");
88553
+ const keys = await generateMeshKeyPair();
88554
+ const csrPem = await createMeshCsrPem(keys, this.deps.nodeId);
88555
+ const keyPem = await exportMeshPrivateKeyPem(keys.privateKey);
88556
+ const publicKeySpki = Buffer.from(await webcrypto.subtle.exportKey("spki", keys.publicKey)).toString("base64");
88557
+ if (this.sessions.size >= 4 && !this.makeRoom()) return fail(409, "too-many-pending", "too many pairings are pending on this agent");
88558
+ const session = {
88559
+ pairingId: newPairingId(),
88560
+ hub: {
88561
+ nodeId: request.hubNodeId,
88562
+ caCertPem: request.hubCaCertPem,
88563
+ caFingerprint,
88564
+ commitment: request.commitment,
88565
+ url: request.hubUrl,
88566
+ meshAddress: request.hubMeshAddress
88567
+ },
88568
+ keyPem,
88569
+ csrPem,
88570
+ publicKeySpki,
88571
+ agentNonce: randomHex(32),
88572
+ pin: randomPin(),
88573
+ expiresAt: this.now() + PAIRING_TTL_MS,
88574
+ hubNonce: null,
88575
+ code: null,
88576
+ localConfirmed: false,
88577
+ attempts: 0,
88578
+ touched: false,
88579
+ createdAt: this.now()
88580
+ };
88581
+ this.sessions.set(session.pairingId, session);
88582
+ this.deps.onStateChange?.();
88583
+ this.announceLocally(session);
88584
+ this.deps.log?.("pairing requested by a hub", {
88585
+ hubUrl: session.hub.url,
88586
+ caFingerprint: session.hub.caFingerprint
88587
+ });
88588
+ return {
88589
+ ok: true,
88590
+ answer: {
88591
+ pairingId: session.pairingId,
88592
+ csrPem,
88593
+ agentNonce: session.agentNonce,
88594
+ expiresAt: session.expiresAt
88595
+ }
88596
+ };
88597
+ }
88598
+ /** The hub reveals its nonce; the agent checks it against the commitment and derives the code. */
88599
+ reveal(body) {
88600
+ const session = this.pending(body);
88601
+ if ("ok" in session) return session;
88602
+ const hubNonce = field(body, "hubNonce");
88603
+ if (!isText(hubNonce, 256)) return fail(400, "bad-reveal", "hubNonce is missing");
88604
+ if (session.hubNonce !== null) return fail(409, "already-revealed", "the nonce was already revealed");
88605
+ if (!commitmentsEqual(computePairingCommitment(session.hub.caCertPem, hubNonce), session.hub.commitment)) {
88606
+ this.end(session, "failed");
88607
+ return fail(400, "commitment-mismatch", "the revealed nonce does not match the hub commitment");
88608
+ }
88609
+ session.touched = true;
88610
+ session.hubNonce = hubNonce;
88611
+ session.code = computePairingSas({
88612
+ caCertPem: session.hub.caCertPem,
88613
+ agentPublicKeySpki: session.publicKeySpki,
88614
+ hubNonce,
88615
+ agentNonce: session.agentNonce
88616
+ });
88617
+ this.announceLocally(session);
88618
+ return { ok: true };
88619
+ }
88620
+ /** The hub polls with the pairing id: did the agent's operator confirm, did it end? */
88621
+ poll(body) {
88622
+ this.expireIfDue();
88623
+ const pairingId = field(body, "pairingId");
88624
+ const session = typeof pairingId === "string" ? this.sessions.get(pairingId) : void 0;
88625
+ if (session !== void 0) {
88626
+ session.touched = true;
88627
+ return {
88628
+ ok: true,
88629
+ poll: {
88630
+ state: "pending",
88631
+ localConfirmed: session.localConfirmed,
88632
+ last: null
88633
+ }
88634
+ };
88635
+ }
88636
+ const known = typeof pairingId === "string" ? this.outcomeOf(pairingId) : null;
88637
+ if (typeof pairingId === "string" && known !== null) return {
88638
+ ok: true,
88639
+ poll: {
88640
+ state: "idle",
88641
+ localConfirmed: false,
88642
+ last: {
88643
+ pairingId,
88644
+ outcome: known
88645
+ }
88646
+ }
88647
+ };
88648
+ return fail(404, "no-pairing", "no such pairing is pending");
88649
+ }
88650
+ /**
88651
+ * The operator typed the code the HUB shows. From the agent's own machine that is enough; from
88652
+ * anywhere else the one-time PIN of THIS pairing (read on the agent's console) must come with
88653
+ * it. A wrong code counts against the pairing (the third cancels it); a wrong PIN counts against
88654
+ * the agent (five lock confirming for five minutes).
88655
+ */
88656
+ confirm(body, ctx) {
88657
+ const found = this.localTarget(body, ctx);
88658
+ if ("ok" in found) return found;
88659
+ const session = found;
88660
+ if (session.code === null) return fail(409, "not-ready", "the hub has not revealed its nonce yet");
88661
+ const code = field(body, "code");
88662
+ if (!constantTimeEqual(session.code, typeof code === "string" ? code.trim() : code)) {
88663
+ session.attempts += 1;
88664
+ if (session.attempts >= 3) {
88665
+ this.end(session, "code-mismatch");
88666
+ return fail(403, "code-mismatch", "wrong code three times — the pairing was cancelled");
88667
+ }
88668
+ return fail(403, "code-mismatch", "that is not the code this agent computed");
88669
+ }
88670
+ session.localConfirmed = true;
88671
+ return { ok: true };
88672
+ }
88673
+ reject(body, ctx) {
88674
+ const found = this.localTarget(body, ctx);
88675
+ if ("ok" in found) return found;
88676
+ this.end(found, "rejected");
88677
+ return { ok: true };
88678
+ }
88679
+ /** The hub withdraws its own pairing (authorized by the pairing id it was given). */
88680
+ cancel(body) {
88681
+ const session = this.pending(body);
88682
+ if ("ok" in session) return session;
88683
+ this.end(session, "rejected");
88684
+ return { ok: true };
88685
+ }
88686
+ async complete(body) {
88687
+ const session = this.pending(body);
88688
+ if ("ok" in session) return session;
88689
+ if (session.hubNonce === null || session.code === null) return fail(409, "not-ready", "the hub has not revealed its nonce yet");
88690
+ if (!session.localConfirmed) return fail(409, "awaiting-local-confirmation", "the operator has not confirmed on this agent");
88691
+ const leafPem = field(body, "leafPem");
88692
+ const caCertPem = field(body, "caCertPem");
88693
+ const proof = field(body, "proof");
88694
+ if (!isText(leafPem, 16384) || !isText(caCertPem, 16384) || !isText(proof, 16384)) return fail(400, "bad-complete", "the completion is malformed");
88695
+ const refusal = this.checkCompletion(session, leafPem, caCertPem, proof);
88696
+ if (refusal !== null) {
88697
+ this.end(session, "failed");
88698
+ return refusal;
88699
+ }
88700
+ try {
88701
+ await this.deps.install({
88702
+ meshTls: {
88703
+ certPem: leafPem,
88704
+ keyPem: session.keyPem,
88705
+ caCertPem: session.hub.caCertPem
88706
+ },
88707
+ hubAddress: hubHostFromMeshAddress(session.hub.meshAddress),
88708
+ hubUrl: session.hub.url
88709
+ });
88710
+ } catch (err) {
88711
+ this.end(session, "failed");
88712
+ return fail(500, "install-failed", err instanceof Error ? err.message : String(err));
88713
+ }
88714
+ this.end(session, "completed");
88715
+ for (const other of [...this.sessions.values()]) this.end(other, "failed", false);
88716
+ this.deps.log?.("paired with the hub", { hubUrl: session.hub.url });
88717
+ return { ok: true };
88718
+ }
88719
+ /** The agent forgot its hub (local action): the pairing slate is clean. */
88720
+ noteUnpaired() {
88721
+ for (const s of [...this.sessions.values()]) this.end(s, "failed", false);
88722
+ this.deps.onStateChange?.();
88723
+ }
88724
+ /** Remaining lock for a source, or `null`. Loopback never calls this. */
88725
+ sourceLock(source, at) {
88726
+ const entry = this.pinBySource.get(source);
88727
+ return entry !== void 0 && at < entry.lockedUntil ? entry.lockedUntil - at : null;
88728
+ }
88729
+ /** A wrong PIN or reset code from `source`: five in five minutes lock THAT source, doubling. */
88730
+ noteWrongSecret(source, at) {
88731
+ if (this.pinBySource.size >= MAX_TRACKED_SOURCES && !this.pinBySource.has(source)) this.pinBySource.clear();
88732
+ const prev = this.pinBySource.get(source) ?? {
88733
+ failures: [],
88734
+ strikes: 0,
88735
+ lockedUntil: 0
88736
+ };
88737
+ const failures = [...prev.failures.filter((t) => at - t < 5 * 6e4), at];
88738
+ if (failures.length >= PIN_FAILURE_LIMIT) {
88739
+ const lockedUntil = at + Math.min(PIN_LOCK_MAX_MS, PIN_LOCK_MS * 2 ** prev.strikes);
88740
+ this.deps.log?.("confirming is locked for a source: too many wrong PINs", { source });
88741
+ this.pinBySource.set(source, {
88742
+ failures: [],
88743
+ strikes: prev.strikes + 1,
88744
+ lockedUntil
88745
+ });
88746
+ } else this.pinBySource.set(source, {
88747
+ ...prev,
88748
+ failures
88749
+ });
88750
+ this.globalPinFailures = [...this.globalPinFailures.filter((t) => at - t < PAIRING_TTL_MS), at];
88751
+ if (this.globalPinFailures.length >= PIN_GLOBAL_CEILING) {
88752
+ this.globalPinFailures = [];
88753
+ this.deps.log?.("too many wrong PINs overall: every pending pairing is cancelled", {});
88754
+ for (const session of [...this.sessions.values()]) this.end(session, "failed", false);
88755
+ }
88756
+ }
88757
+ /**
88758
+ * The operator clears the lockouts, the budget and every pending offer. From this machine
88759
+ * (genuine loopback) or with the LOCAL RESET CODE printed at boot; the code is renewed on use.
88760
+ */
88761
+ reset(body, ctx) {
88762
+ const at = this.now();
88763
+ if (!ctx.local) {
88764
+ const key = `reset:${ctx.source ?? "unknown"}`;
88765
+ const locked = this.sourceLock(key, at);
88766
+ if (locked !== null) return fail(429, "reset-locked", "too many wrong codes", locked);
88767
+ const code = field(body, "code");
88768
+ if (typeof code !== "string" || !constantTimeEqual(this.resetCode, code.trim())) {
88769
+ this.noteWrongSecret(key, at);
88770
+ return fail(401, "reset-code-required", "the reset code printed on the agent is required");
88771
+ }
88772
+ }
88773
+ for (const session of [...this.sessions.values()]) this.end(session, "failed", false);
88774
+ this.abandoned = [];
88775
+ this.lockedUntil = 0;
88776
+ this.perCa.clear();
88777
+ this.pinBySource.clear();
88778
+ this.globalPinFailures = [];
88779
+ this.resetCode = randomPin();
88780
+ this.deps.onResetCode?.(this.resetCode);
88781
+ this.deps.log?.("pairing was reset by the local operator", {});
88782
+ return { ok: true };
88783
+ }
88784
+ /** Verify the CA-key signature of an offer and spend its commitment. */
88785
+ checkOfferSignature(request, caFingerprint) {
88786
+ const claims = verifyPairingOffer(request.hubCaCertPem, request.signature);
88787
+ const at = this.now();
88788
+ if (claims === null || claims.use !== "pairing-offer" || claims.agentNodeId !== this.deps.nodeId || claims.hubNodeId !== request.hubNodeId || claims.caFingerprint !== caFingerprint || claims.commitment !== request.commitment || claims.hubUrl !== request.hubUrl || claims.hubMeshAddress !== request.hubMeshAddress || claims.iat > at + 6e4 || at - claims.iat > 12e4) {
88789
+ this.deps.log?.("an offer without a valid CA signature was refused", {});
88790
+ return fail(403, "bad-offer-signature", "the offer is not signed by the CA it presents");
88791
+ }
88792
+ for (const [commitment, until] of this.seenCommitments) if (until <= at) this.seenCommitments.delete(commitment);
88793
+ if (this.seenCommitments.has(request.commitment)) return fail(409, "offer-replayed", "this offer was already used");
88794
+ this.seenCommitments.set(request.commitment, at + PAIRING_OFFER_MAX_AGE_MS + 6e4);
88795
+ return null;
88796
+ }
88797
+ /**
88798
+ * All slots taken: a signed offer makes room by dropping the oldest pairing whose hub never showed
88799
+ * activity (no reveal, no poll) and that no operator confirmed. `false` when there is none.
88800
+ */
88801
+ makeRoom() {
88802
+ const oldest = [...this.sessions.values()].filter((s) => !s.touched && !s.localConfirmed).toSorted((a, b) => a.createdAt - b.createdAt)[0];
88803
+ if (oldest === void 0) return false;
88804
+ this.deps.log?.("a pending pairing made room for a newer signed offer", { caFingerprint: oldest.hub.caFingerprint });
88805
+ this.end(oldest, "failed", false);
88806
+ return true;
88807
+ }
88808
+ /** Offers that never reached local confirmation are rationed - globally and per hub CA. */
88809
+ rationOffer(caFingerprint) {
88810
+ const at = this.now();
88811
+ if (at < this.lockedUntil) {
88812
+ this.deps.log?.("offer refused: the budget of abandoned pairings is spent", { retryAfterMs: this.lockedUntil - at });
88813
+ return fail(429, "offer-budget", "too many pairings were abandoned recently - try again later", this.lockedUntil - at);
88814
+ }
88815
+ const ca = this.perCa.get(caFingerprint);
88816
+ if (ca !== void 0 && at < ca.until) {
88817
+ this.deps.log?.("offer refused: this hub abandoned a pairing a moment ago", {
88818
+ caFingerprint,
88819
+ retryAfterMs: ca.until - at
88820
+ });
88821
+ return fail(429, "offer-backoff", "this hub abandoned a pairing a moment ago - wait before offering again", ca.until - at);
88822
+ }
88823
+ return null;
88824
+ }
88825
+ /** A pairing ended without the local operator confirming it: it counts against the budget. */
88826
+ noteAbandoned(session) {
88827
+ const at = this.now();
88828
+ this.abandoned = [...this.abandoned.filter((t) => at - t < BUDGET_WINDOW_MS), at];
88829
+ if (this.abandoned.length >= BUDGET_OFFERS) {
88830
+ const over = this.abandoned.length - BUDGET_OFFERS;
88831
+ const lock = Math.min(LOCKOUT_MAX_MS, LOCKOUT_BASE_MS * 2 ** over);
88832
+ this.lockedUntil = at + lock;
88833
+ this.deps.log?.("pairing offers are locked: too many were abandoned", {
88834
+ abandoned: this.abandoned.length,
88835
+ lockMs: lock
88836
+ });
88837
+ }
88838
+ const count = (this.perCa.get(session.hub.caFingerprint)?.count ?? 0) + 1;
88839
+ const backoff = count < 2 ? 0 : Math.min(CA_BACKOFF_MAX_MS, CA_BACKOFF_BASE_MS * 2 ** (count - 2));
88840
+ this.perCa.set(session.hub.caFingerprint, {
88841
+ count,
88842
+ until: at + backoff
88843
+ });
88844
+ }
88845
+ /** The pairing this local request means, else the refusal. */
88846
+ localTarget(body, ctx) {
88847
+ this.expireIfDue();
88848
+ if (this.sessions.size === 0) return fail(404, "no-pairing", "no pairing is pending");
88849
+ const pin = field(body, "pin");
88850
+ if (!ctx.local) {
88851
+ const at = this.now();
88852
+ const source = ctx.source ?? "unknown";
88853
+ const locked = this.sourceLock(source, at);
88854
+ if (locked !== null) return fail(429, "pin-locked", "too many wrong PINs - try again later", locked);
88855
+ const match = typeof pin === "string" ? [...this.sessions.values()].find((s) => constantTimeEqual(s.pin, pin.trim())) : void 0;
88856
+ if (match === void 0) {
88857
+ this.noteWrongSecret(source, at);
88858
+ return fail(401, "pin-required", "the PIN shown on this agent's own machine (console, file or local window) is required");
88859
+ }
88860
+ return match;
88861
+ }
88862
+ if (typeof pin === "string" && pin.trim().length > 0) {
88863
+ const match = [...this.sessions.values()].find((s) => constantTimeEqual(s.pin, pin.trim()));
88864
+ if (match !== void 0) return match;
88865
+ }
88866
+ const only = [...this.sessions.values()];
88867
+ if (only.length === 1 && only[0] !== void 0) return only[0];
88868
+ return fail(409, "ambiguous", "several pairings are pending - give the PIN of the one you mean");
88869
+ }
88870
+ announceLocally(session) {
88871
+ this.deps.onLocalSecret?.({
88872
+ pairingId: session.pairingId,
88873
+ pin: session.pin,
88874
+ code: session.code,
88875
+ hub: {
88876
+ nodeId: session.hub.nodeId,
88877
+ url: session.hub.url,
88878
+ caFingerprint: session.hub.caFingerprint
88879
+ },
88880
+ expiresAt: session.expiresAt
88881
+ });
88882
+ }
88883
+ checkCompletion(session, leafPem, caCertPem, proof) {
88884
+ if (caFingerprintOf(caCertPem) !== session.hub.caFingerprint) return fail(400, "ca-mismatch", "the completion carries a CA other than the one offered");
88885
+ const claims = verifyPairingProof(session.hub.caCertPem, proof);
88886
+ if (claims === null) return fail(400, "bad-proof", "the pairing proof does not verify");
88887
+ if (claims.pairingId !== session.pairingId || claims.nodeId !== this.deps.nodeId || claims.hubNonce !== session.hubNonce || claims.agentNonce !== session.agentNonce || claims.caFingerprint !== session.hub.caFingerprint || claims.hubMeshAddress !== session.hub.meshAddress || claims.hubUrl !== session.hub.url) return fail(400, "bad-proof", "the pairing proof is for another pairing");
88888
+ const at = this.now();
88889
+ if (claims.exp <= at || claims.iat > at + 6e4 || claims.exp - claims.iat > 3e5) return fail(400, "bad-proof", "the pairing proof is expired or has an impossible lifetime");
88890
+ try {
88891
+ if (claims.leafFingerprint !== leafFingerprintCompact(leafPem)) return fail(400, "bad-leaf", "the certificate is not the one the proof names");
88892
+ const leaf = new X509Certificate(leafPem);
88893
+ const ca = new X509Certificate(session.hub.caCertPem);
88894
+ if (!leaf.verify(ca.publicKey)) return fail(400, "bad-leaf", "the certificate is not signed by the offered CA");
88895
+ if (subjectCn(leaf.subject.replaceAll("\n", ",")) !== this.deps.nodeId) return fail(400, "bad-leaf", `the certificate is not issued for "${this.deps.nodeId}"`);
88896
+ if (!leaf.checkPrivateKey(createPrivateKey(session.keyPem))) return fail(400, "bad-leaf", "the certificate does not match this agent's private key");
88897
+ if (Date.parse(leaf.validTo) <= this.now()) return fail(400, "bad-leaf", "the certificate is already expired");
88898
+ } catch {
88899
+ return fail(400, "bad-leaf", "the certificate could not be read");
88900
+ }
88901
+ return null;
88902
+ }
88903
+ /** The live session `body.pairingId` names, else the refusal. */
88904
+ pending(body) {
88905
+ this.expireIfDue();
88906
+ const pairingId = field(body, "pairingId");
88907
+ if (this.sessions.size === 0) return fail(404, "no-pairing", "no pairing is pending");
88908
+ const session = typeof pairingId === "string" ? this.sessions.get(pairingId) : void 0;
88909
+ if (session === void 0) return fail(403, "wrong-pairing", "that is not a pending pairing");
88910
+ return session;
88911
+ }
88912
+ expireIfDue() {
88913
+ for (const s of [...this.sessions.values()]) if (s.expiresAt <= this.now()) this.end(s, "expired");
88914
+ }
88915
+ outcomeOf(pairingId) {
88916
+ const entry = this.outcomes.get(pairingId);
88917
+ if (entry === void 0) return null;
88918
+ if (this.now() - entry.at > OUTCOME_TTL_MS) {
88919
+ this.outcomes.delete(pairingId);
88920
+ return null;
88921
+ }
88922
+ return entry.outcome;
88923
+ }
88924
+ rememberOutcome(pairingId, outcome) {
88925
+ this.outcomes.set(pairingId, {
88926
+ outcome,
88927
+ at: this.now()
88928
+ });
88929
+ while (this.outcomes.size > OUTCOME_LIMIT) {
88930
+ const oldest = this.outcomes.keys().next().value;
88931
+ if (oldest === void 0) break;
88932
+ this.outcomes.delete(oldest);
88933
+ }
88934
+ }
88935
+ end(session, outcome, count = true) {
88936
+ if (!this.sessions.delete(session.pairingId)) return;
88937
+ this.rememberOutcome(session.pairingId, outcome);
88938
+ if (outcome === "completed") this.perCa.delete(session.hub.caFingerprint);
88939
+ else if (count && !session.localConfirmed) this.noteAbandoned(session);
88940
+ this.deps.onLocalSecretCleared?.(session.pairingId);
88941
+ this.deps.onStateChange?.();
88942
+ if (outcome !== "completed") this.deps.log?.("pairing cancelled", {
88943
+ outcome,
88944
+ hubUrl: session.hub.url
88945
+ });
88946
+ }
88947
+ };
88948
+ function field(body, key) {
88949
+ return body !== null && typeof body === "object" ? Reflect.get(body, key) : void 0;
88950
+ }
88951
+ function caFingerprintOf(pem) {
88952
+ try {
88953
+ return caFingerprintCompact(pem);
88954
+ } catch {
88955
+ return null;
88956
+ }
88957
+ }
88958
+ function parseOffer(body) {
88959
+ if (body === null || typeof body !== "object") return null;
88960
+ const protocol = Reflect.get(body, "protocol");
88961
+ const hubNodeId = Reflect.get(body, "hubNodeId");
88962
+ const hubCaCertPem = Reflect.get(body, "hubCaCertPem");
88963
+ const commitment = Reflect.get(body, "commitment");
88964
+ const hubUrl = Reflect.get(body, "hubUrl");
88965
+ const hubMeshAddress = Reflect.get(body, "hubMeshAddress");
88966
+ const signature = Reflect.get(body, "signature");
88967
+ if (typeof protocol !== "number") return null;
88968
+ if (!isText(hubNodeId, 128) || !isText(hubCaCertPem, 16384) || !isText(commitment, 128)) return null;
88969
+ if (!isText(hubUrl) || !isText(hubMeshAddress, 255) || !isText(signature, 4096)) return null;
88970
+ return {
88971
+ protocol,
88972
+ hubNodeId,
88973
+ hubCaCertPem,
88974
+ commitment,
88975
+ hubUrl,
88976
+ hubMeshAddress,
88977
+ signature
88978
+ };
88979
+ }
88980
+ //#endregion
88981
+ //#region src/kernel/moleculer/mesh-mtls/pairing/hub-pairing.ts
88982
+ /**
88983
+ * The hub's side of pairing (D857): starts a pairing with an agent it found
88984
+ * (mDNS) or was given an address for, computes the same six digits the agent
88985
+ * shows, and signs the agent's certificate ONLY after the operator confirmed
88986
+ * them on the hub AND the agent's own operator confirmed them there.
88987
+ *
88988
+ * Either confirmation may come first. The hub learns the agent's by polling the
88989
+ * agent's pairing status (`advance`), never by being told: nothing the agent
88990
+ * says is trusted beyond "my operator confirmed", and even that only matters
88991
+ * together with the hub's own confirmation.
88992
+ *
88993
+ * Transport-agnostic: `AgentPairingTransport` is the HTTP client in production
88994
+ * and an in-memory shim (or a man in the middle) in specs.
88995
+ */
88996
+ /** A refusal the agent sent, kept with its machine-readable code. */
88997
+ var PairingHttpError = class extends Error {
88998
+ status;
88999
+ code;
89000
+ constructor(status, code, message) {
89001
+ super(message);
89002
+ this.status = status;
89003
+ this.code = code;
89004
+ this.name = "PairingHttpError";
89005
+ }
89006
+ };
89007
+ /** Pairings kept (active and just ended) so the page can show how one ended. */
89008
+ var MAX_SESSIONS = 16;
89009
+ var PROOF_TTL_MS = Math.min(2 * 6e4, PAIRING_PROOF_MAX_LIFETIME_MS);
89010
+ var TERMINAL = new Set([
89011
+ "paired",
89012
+ "rejected",
89013
+ "expired",
89014
+ "failed"
89015
+ ]);
89016
+ var HubPairing = class {
89017
+ deps;
89018
+ sessions = /* @__PURE__ */ new Map();
89019
+ now;
89020
+ constructor(deps) {
89021
+ this.deps = deps;
89022
+ this.now = deps.now ?? Date.now;
89023
+ }
89024
+ /** Offer the hub's CA to the agent at `endpoint` and derive the code. Throws an `Error` with the reason. */
89025
+ async begin(endpoint) {
89026
+ const ca = this.deps.ca();
89027
+ if (ca === null) throw new Error("the mesh CA is not ready — try again in a moment");
89028
+ const hello = await this.deps.transport.hello(endpoint);
89029
+ if (hello.protocol !== 1) throw new Error(`the agent speaks pairing protocol ${String(hello.protocol)}, not 1`);
89030
+ if (hello.state === "paired") throw new Error(`"${hello.nodeId}" is already paired — unpair it first`);
89031
+ this.assertNodeIdUsable(hello.nodeId);
89032
+ for (const s of this.sessions.values()) if (s.nodeId === hello.nodeId && !TERMINAL.has(s.state)) throw new Error(`a pairing with "${hello.nodeId}" is already in progress`);
89033
+ const hubNonce = randomHex(32);
89034
+ const hubUrl = await this.deps.hubUrl();
89035
+ const hubMeshAddress = await this.deps.hubMeshAddress();
89036
+ const commitment = computePairingCommitment(ca.certPem, hubNonce);
89037
+ const answer = await this.deps.transport.offer(endpoint, {
89038
+ protocol: 1,
89039
+ hubNodeId: this.deps.hubNodeId,
89040
+ hubCaCertPem: ca.certPem,
89041
+ commitment,
89042
+ hubUrl,
89043
+ hubMeshAddress,
89044
+ signature: signPairingOffer(ca.keyPem, {
89045
+ use: PAIRING_OFFER_USE,
89046
+ agentNodeId: hello.nodeId,
89047
+ hubNodeId: this.deps.hubNodeId,
89048
+ caFingerprint: caFingerprintCompact(ca.certPem),
89049
+ commitment,
89050
+ hubUrl,
89051
+ hubMeshAddress,
89052
+ iat: this.now()
89053
+ })
89054
+ });
89055
+ try {
89056
+ const csr = await describeCsr(answer.csrPem);
89057
+ if (csr.commonName !== hello.nodeId) throw new Error(`the agent's CSR is for "${csr.commonName}", not "${hello.nodeId}"`);
89058
+ await this.deps.transport.reveal(endpoint, {
89059
+ pairingId: answer.pairingId,
89060
+ hubNonce
89061
+ });
89062
+ const code = computePairingSas({
89063
+ caCertPem: ca.certPem,
89064
+ agentPublicKeySpki: csr.publicKeySpki,
89065
+ hubNonce,
89066
+ agentNonce: answer.agentNonce
89067
+ });
89068
+ const session = {
89069
+ pairingId: answer.pairingId,
89070
+ nodeId: hello.nodeId,
89071
+ endpoint,
89072
+ code,
89073
+ csrPem: answer.csrPem,
89074
+ hubNonce,
89075
+ agentNonce: answer.agentNonce,
89076
+ caCertPem: ca.certPem,
89077
+ hubUrl,
89078
+ hubMeshAddress,
89079
+ expiresAt: Math.min(answer.expiresAt, this.now() + PAIRING_TTL_MS),
89080
+ state: "awaiting-confirmation",
89081
+ hubConfirmed: false,
89082
+ agentConfirmed: false,
89083
+ attempts: 0,
89084
+ failure: null,
89085
+ issued: false
89086
+ };
89087
+ this.remember(session);
89088
+ this.deps.log?.("pairing started", {
89089
+ nodeId: session.nodeId,
89090
+ pairingId: session.pairingId
89091
+ });
89092
+ return this.view(session);
89093
+ } catch (err) {
89094
+ await this.deps.transport.reject(endpoint, answer.pairingId).catch(() => void 0);
89095
+ throw err;
89096
+ }
89097
+ }
89098
+ /** The operator typed the code the AGENT shows. A wrong one counts; the third cancels the pairing. */
89099
+ async confirm(pairingId, code) {
89100
+ const session = this.require(pairingId);
89101
+ this.expireIfDue(session);
89102
+ if (TERMINAL.has(session.state)) return this.view(session);
89103
+ if (!constantTimeEqual(session.code, typeof code === "string" ? code.trim() : code)) {
89104
+ session.attempts += 1;
89105
+ if (session.attempts >= 3) await this.cancel(session, "rejected", "wrong code three times");
89106
+ throw new Error(session.attempts >= 3 ? "wrong code three times — the pairing was cancelled" : "that is not the code the hub computed — if the agent shows a different one, someone is between them; cancel it");
89107
+ }
89108
+ session.hubConfirmed = true;
89109
+ return this.advance(pairingId);
89110
+ }
89111
+ async reject(pairingId) {
89112
+ const session = this.require(pairingId);
89113
+ if (!TERMINAL.has(session.state)) await this.cancel(session, "rejected", "rejected on the hub");
89114
+ return this.view(session);
89115
+ }
89116
+ get(pairingId) {
89117
+ const session = this.sessions.get(pairingId);
89118
+ return session === void 0 ? void 0 : this.view(session);
89119
+ }
89120
+ list() {
89121
+ return [...this.sessions.values()].map((s) => this.view(s));
89122
+ }
89123
+ /** Active pairings, for the poller. */
89124
+ activeIds() {
89125
+ return [...this.sessions.values()].filter((s) => !TERMINAL.has(s.state)).map((s) => s.pairingId);
89126
+ }
89127
+ /**
89128
+ * Move a pairing on: notice an expiry, a cancellation or the agent's
89129
+ * confirmation, and complete when both sides confirmed. Never throws.
89130
+ */
89131
+ async advance(pairingId) {
89132
+ const session = this.require(pairingId);
89133
+ if (TERMINAL.has(session.state) || session.state === "completing") return this.view(session);
89134
+ if (this.expireIfDue(session)) {
89135
+ await this.deps.transport.reject(session.endpoint, session.pairingId).catch(() => void 0);
89136
+ return this.view(session);
89137
+ }
89138
+ try {
89139
+ const poll = await this.deps.transport.poll(session.endpoint, session.pairingId);
89140
+ if (poll.state === "pending") session.agentConfirmed = poll.localConfirmed;
89141
+ else if (poll.last?.pairingId === session.pairingId && poll.last.outcome === "completed") {
89142
+ this.finish(session, "paired", null);
89143
+ return this.view(session);
89144
+ } else {
89145
+ const why = poll.last?.pairingId === session.pairingId ? poll.last.outcome : "gone";
89146
+ this.finish(session, "failed", `the agent ended the pairing (${why})`);
89147
+ return this.view(session);
89148
+ }
89149
+ } catch (err) {
89150
+ if (err instanceof PairingHttpError && err.code === "no-pairing") {
89151
+ this.finish(session, "failed", "the agent no longer has this pairing");
89152
+ return this.view(session);
89153
+ }
89154
+ this.deps.log?.("pairing status poll failed", {
89155
+ nodeId: session.nodeId,
89156
+ error: err instanceof Error ? err.message : String(err)
89157
+ });
89158
+ return this.view(session);
89159
+ }
89160
+ if (session.hubConfirmed && session.agentConfirmed) await this.complete(session);
89161
+ return this.view(session);
89162
+ }
89163
+ async complete(session) {
89164
+ const ca = this.deps.ca();
89165
+ if (ca === null || caFingerprintCompact(ca.certPem) !== caFingerprintCompact(session.caCertPem)) {
89166
+ await this.cancel(session, "failed", "the mesh CA changed during the pairing");
89167
+ return;
89168
+ }
89169
+ session.state = "completing";
89170
+ let issuedPem = null;
89171
+ try {
89172
+ const leaf = await issueMeshLeafFromCsr(ca, session.csrPem, { expectedNodeId: session.nodeId });
89173
+ issuedPem = leaf.certPem;
89174
+ await this.deps.recordIssued(session.nodeId, leaf.certPem);
89175
+ session.issued = true;
89176
+ const proof = signPairingProof(ca.keyPem, {
89177
+ v: 1,
89178
+ use: PAIRING_PROOF_USE,
89179
+ pairingId: session.pairingId,
89180
+ nodeId: session.nodeId,
89181
+ hubNonce: session.hubNonce,
89182
+ agentNonce: session.agentNonce,
89183
+ leafFingerprint: leafFingerprintCompact(leaf.certPem),
89184
+ caFingerprint: caFingerprintCompact(ca.certPem),
89185
+ hubMeshAddress: session.hubMeshAddress,
89186
+ hubUrl: session.hubUrl,
89187
+ iat: this.now(),
89188
+ exp: this.now() + PROOF_TTL_MS
89189
+ });
89190
+ await this.deps.transport.complete(session.endpoint, {
89191
+ pairingId: session.pairingId,
89192
+ leafPem: leaf.certPem,
89193
+ caCertPem: ca.certPem,
89194
+ proof
89195
+ });
89196
+ await this.installed(session, leaf.certPem);
89197
+ } catch (err) {
89198
+ const reason = err instanceof Error ? err.message : String(err);
89199
+ if (err instanceof PairingHttpError && err.code === "awaiting-local-confirmation") {
89200
+ session.state = "awaiting-confirmation";
89201
+ session.agentConfirmed = false;
89202
+ return;
89203
+ }
89204
+ let outcome = issuedPem === null ? "absent" : await this.installOutcome(session);
89205
+ if (outcome !== "installed" && issuedPem !== null && this.deps.isLeafPresented?.(session.nodeId, issuedPem) === true) outcome = "installed";
89206
+ if (outcome === "installed" && issuedPem !== null) {
89207
+ await this.installed(session, issuedPem);
89208
+ return;
89209
+ }
89210
+ if (outcome === "absent" && issuedPem !== null) await this.deps.revokeLeaf?.(session.nodeId, issuedPem, "pairing did not complete").catch(() => void 0);
89211
+ const note = outcome === "unknown" ? `unconfirmed: ${reason} - the agent could not be reached to learn whether it installed the certificate; unpair it if it shows as paired` : reason;
89212
+ await this.cancel(session, "failed", note);
89213
+ }
89214
+ }
89215
+ /** The new leaf is installed: older leaves of the node go, the new one stays. */
89216
+ async installed(session, leafPem) {
89217
+ await this.deps.revokeNode(session.nodeId, "superseded by pairing", leafPem).catch((err) => this.deps.log?.("older leaves of the node could not be revoked", {
89218
+ nodeId: session.nodeId,
89219
+ error: err instanceof Error ? err.message : String(err)
89220
+ }));
89221
+ this.finish(session, "paired", null);
89222
+ this.deps.log?.("agent paired", {
89223
+ nodeId: session.nodeId,
89224
+ pairingId: session.pairingId
89225
+ });
89226
+ }
89227
+ /** After a failed completion call: did the agent install the certificate after all? */
89228
+ async installOutcome(session) {
89229
+ try {
89230
+ const poll = await this.deps.transport.poll(session.endpoint, session.pairingId);
89231
+ if (poll.last?.pairingId === session.pairingId && poll.last.outcome === "completed") return "installed";
89232
+ return "absent";
89233
+ } catch (err) {
89234
+ return "unknown";
89235
+ }
89236
+ }
89237
+ assertNodeIdUsable(nodeId) {
89238
+ if (!isValidAgentNodeId(nodeId)) throw new Error(`"${nodeId}" cannot be an agent node id (letters, digits, . _ - ; not "hub"; at most 63 characters)`);
89239
+ if (this.deps.isNodeConnected(nodeId)) throw new Error(`a node called "${nodeId}" is already connected to this hub — unpair it first`);
89240
+ const lower = nodeId.toLowerCase();
89241
+ const clash = (this.deps.knownNodeIds?.() ?? []).find((known) => known !== nodeId && known.toLowerCase() === lower);
89242
+ if (clash !== void 0) throw new Error(`"${nodeId}" differs only in case from the existing node "${clash}"`);
89243
+ }
89244
+ expireIfDue(session) {
89245
+ if (TERMINAL.has(session.state) || session.expiresAt > this.now()) return false;
89246
+ this.finish(session, "expired", "the pairing was not confirmed on both sides in time");
89247
+ return true;
89248
+ }
89249
+ finish(session, state, failure) {
89250
+ session.state = state;
89251
+ session.failure = failure;
89252
+ }
89253
+ async cancel(session, state, reason) {
89254
+ this.finish(session, state, reason);
89255
+ await this.deps.transport.reject(session.endpoint, session.pairingId).catch(() => void 0);
89256
+ this.deps.log?.("pairing cancelled", {
89257
+ nodeId: session.nodeId,
89258
+ state,
89259
+ reason
89260
+ });
89261
+ }
89262
+ require(pairingId) {
89263
+ const session = this.sessions.get(pairingId);
89264
+ if (session === void 0) throw new Error("no such pairing");
89265
+ return session;
89266
+ }
89267
+ remember(session) {
89268
+ this.sessions.set(session.pairingId, session);
89269
+ if (this.sessions.size <= MAX_SESSIONS) return;
89270
+ for (const [id, s] of this.sessions) {
89271
+ if (this.sessions.size <= MAX_SESSIONS) break;
89272
+ if (TERMINAL.has(s.state)) this.sessions.delete(id);
89273
+ }
89274
+ }
89275
+ view(s) {
89276
+ return {
89277
+ pairingId: s.pairingId,
89278
+ nodeId: s.nodeId,
89279
+ endpoint: s.endpoint,
89280
+ code: s.code,
89281
+ state: s.state,
89282
+ hubConfirmed: s.hubConfirmed,
89283
+ agentConfirmed: s.agentConfirmed,
89284
+ expiresAt: s.expiresAt,
89285
+ failure: s.failure
89286
+ };
89287
+ }
89288
+ };
89289
+ //#endregion
87958
89290
  //#region src/builtins/system-config/cluster-secret-keeper.ts
87959
89291
  /**
87960
89292
  * Keeps the hub's cluster-secret gate in step with System → Cluster (D797,
@@ -88102,22 +89434,22 @@ function errText(err) {
88102
89434
  * agent-ui unlock tickets (D821).
88103
89435
  */
88104
89436
  var mintJoinTokenInputSchema = z.object({
88105
- /** Optional pinned node id for the CSR CN. */
88106
- nodeId: z.string().min(1).optional(),
89437
+ /** The node id the token is for - mandatory: the CSR CN must be exactly this. */
89438
+ nodeId: z.string().min(1),
88107
89439
  /** Override hub HTTPS base (default: local-network publicEndpoint). */
88108
89440
  hubUrl: z.string().url().optional(),
88109
89441
  /** Override `host:6443` TLS mesh address carried in the token. */
88110
89442
  hubMeshAddress: z.string().min(3).optional(),
88111
89443
  /** Seconds until expiry; capped at 1 h. */
88112
89444
  ttlSec: z.number().int().positive().max(3600).optional()
88113
- }).optional();
89445
+ });
88114
89446
  var mintJoinTokenOutputSchema = z.object({
88115
89447
  token: z.string().min(1),
88116
89448
  exp: z.number().int(),
88117
89449
  hubUrl: z.string(),
88118
89450
  hubMeshAddress: z.string(),
88119
89451
  caFingerprint: z.string(),
88120
- nodeId: z.string().optional()
89452
+ nodeId: z.string()
88121
89453
  });
88122
89454
  var mintAgentUiUnlockInputSchema = z.object({
88123
89455
  nodeId: z.string().min(1),
@@ -88378,7 +89710,7 @@ var SystemConfigAddon = class extends BaseAddon {
88378
89710
  ca,
88379
89711
  hubUrl,
88380
89712
  hubMeshAddress,
88381
- ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
89713
+ nodeId: input.nodeId,
88382
89714
  ...input.ttlSec !== void 0 ? { ttlSec: input.ttlSec } : {}
88383
89715
  });
88384
89716
  return {
@@ -88387,7 +89719,7 @@ var SystemConfigAddon = class extends BaseAddon {
88387
89719
  hubUrl,
88388
89720
  hubMeshAddress,
88389
89721
  caFingerprint: caFingerprintCompact(ca.certPem),
88390
- ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {}
89722
+ nodeId: input.nodeId
88391
89723
  };
88392
89724
  }
88393
89725
  /** D821 — short-lived ticket for the agent's `:4444` unlock session. */
@@ -88399,6 +89731,8 @@ var SystemConfigAddon = class extends BaseAddon {
88399
89731
  if (input.dashboardBaseUrl !== void 0) {
88400
89732
  const origins = await this.readAgentDashboardOrigins();
88401
89733
  if (!isAllowedAgentDashboardReturnTo(input.dashboardBaseUrl, origins)) throw new Error("dashboardBaseUrl is not an allowed agent dashboard origin (:4444 or agentDashboardOrigins)");
89734
+ const known = getProcessAgentHttpsNames()?.namesOf(input.nodeId) ?? null;
89735
+ if (!isKnownAgentAddress(input.dashboardBaseUrl, known, origins)) throw new Error(`dashboardBaseUrl is not an address this hub knows for "${input.nodeId}" - open the agent at its own address`);
88402
89736
  }
88403
89737
  const ticket = mintAgentUiUnlockTicket({
88404
89738
  secretHash: hashClusterSecret(entry.value),
@@ -88677,13 +90011,13 @@ var SystemConfigAddon = class extends BaseAddon {
88677
90011
  }
88678
90012
  };
88679
90013
  function parseMintJoinTokenInput(input) {
88680
- if (input === null || input === void 0 || typeof input !== "object") return {};
88681
- const out = {};
90014
+ if (input === null || input === void 0 || typeof input !== "object") throw new Error("system-config: mintJoinToken requires { nodeId }");
88682
90015
  const nodeId = Reflect.get(input, "nodeId");
90016
+ if (typeof nodeId !== "string" || nodeId.length === 0) throw new Error("system-config: mintJoinToken requires { nodeId } - the node the token is for");
90017
+ const out = { nodeId };
88683
90018
  const hubUrl = Reflect.get(input, "hubUrl");
88684
90019
  const hubMeshAddress = Reflect.get(input, "hubMeshAddress");
88685
90020
  const ttlSec = Reflect.get(input, "ttlSec");
88686
- if (typeof nodeId === "string") out.nodeId = nodeId;
88687
90021
  if (typeof hubUrl === "string") out.hubUrl = hubUrl;
88688
90022
  if (typeof hubMeshAddress === "string") out.hubMeshAddress = hubMeshAddress;
88689
90023
  if (typeof ttlSec === "number") out.ttlSec = ttlSec;
@@ -88710,4 +90044,4 @@ function readJwtExp(token) {
88710
90044
  return typeof exp === "number" ? exp : 0;
88711
90045
  }
88712
90046
  //#endregion
88713
- export { verifyAgentUiUnlockTicket as $, setProcessMeshCertificateRegistry as A, generateMeshCa as B, setProcessMeshTlsListenerState as C, RENEWAL_ABANDONED as D, MeshCertificateRegistry as E, requireProcessMeshCa as F, AGENT_DASHBOARD_ORIGINS_FIELD as G, issueMeshLeafForNode as H, caFingerprint as I, AGENT_UI_UNLOCK_VERSION as J, AGENT_UI_UNLOCK_DOMAIN as K, caFingerprintCompact as L, processMeshCrl as M, MeshCaKeeper as N, describeMeshLeaf as O, getProcessMeshCaState as P, parseAgentDashboardOrigins as Q, createMeshCsrPem as R, openMeshTlsWhenRevocationsLoaded as S, SYSTEM_CONFIG_CLUSTER_SECTION_ID, SystemConfigAddon, SystemConfigAddon as default, MESH_NODE_CERTIFICATES_COLUMNS as T, issueMeshLeafFromCsr as U, generateMeshKeyPair as V, loadMeshCa as W, isAllowedAgentDashboardReturnTo as X, UnlockTicketJtiStore as Y, mintAgentUiUnlockTicket as Z, onProcessNodeRegistered as _, ServiceBroker as a, requestProcessMeshEnforcement as at, revokeMeshCertificate as b, enrollMeshNode as c, setProcessMeshEnforcer as ct, processJoinTokenJtiStore as d, parseCidrList as dt, MESH_AUTH_MODE_FIELD as et, verifyMeshJoinToken as f, readCidrSetting as ft, getProcessPeerLeaf as g, getProcessNodeMeshTransport as h, Errors as i, parseMeshAuthMode as it, MeshCrl as j, getProcessMeshCertificateRegistry as k, JoinTokenJtiStore as l, createCidrMatcher as lt, emitProcessNodeRegistered as m, MESH_TLS_PORT_HELLO_FIELD as n, getProcessMeshAuthStatus as nt, Transporters as o, setProcessMeshAuthMode as ot, dropProcessPeer as p, AGENT_UI_UNLOCK_TTL_MS as q, TlsTcpTransporter as r, isMeshAuthMode as rt, setProcessMeshTlsEnabler as s, setProcessMeshAuthStatusSource as st, systemConfigMeshActions as t, getProcessMeshAuthMode as tt, mintMeshJoinToken as u, normalizeIp as ut, setProcessMeshPeerTransportLookup as v, MESH_NODE_CERTIFICATES_COLLECTION as w, getProcessMeshTlsListenerState as x, renewMeshCertificate as y, exportMeshPrivateKeyPem as z };
90047
+ export { requireProcessMeshCa as $, dropProcessPeer as A, setProcessMeshAuthStatusSource as At, openMeshTlsWhenRevocationsLoaded as B, Transporters as C, MESH_AUTH_MODE_FIELD as Ct, mintMeshJoinToken as D, parseMeshAuthMode as Dt, JoinTokenJtiStore as E, isMeshAuthMode as Et, onProcessNodeRegistered as F, readCidrSetting as Ft, RENEWAL_ABANDONED as G, MESH_NODE_CERTIFICATES_COLLECTION as H, setProcessMeshPeerTransportLookup as I, setProcessMeshCertificateRegistry as J, describeMeshLeaf as K, renewMeshCertificate as L, getProcessNodeMeshTransport as M, createCidrMatcher as Mt, getProcessPeerLeaf as N, normalizeIp as Nt, processJoinTokenJtiStore as O, requestProcessMeshEnforcement as Ot, isProcessVerifiedMeshPeer as P, parseCidrList as Pt, getProcessMeshCaState as Q, revokeMeshCertificate as R, ServiceBroker as S, SYSTEM_CONFIG_CLUSTER_SECTION_ID, verifyAgentUiUnlockTicketEs256 as St, SystemConfigAddon, SystemConfigAddon as default, enrollMeshNode as T, getProcessMeshAuthStatus as Tt, MESH_NODE_CERTIFICATES_COLUMNS as U, setProcessMeshTlsListenerState as V, MeshCertificateRegistry as W, processMeshCrl as X, MeshCrl as Y, MeshCaKeeper as Z, signPairingProof as _, isAllowedAgentDashboardReturnTo as _t, hubHostFromMeshAddress as a, generateMeshKeyPair as at, TlsTcpTransporter as b, parseAgentDashboardOrigins as bt, PAIRING_MAX_SESSIONS as c, issueMeshLeafFromCsr as ct, PAIRING_PROTOCOL as d, AGENT_UI_UNLOCK_DOMAIN as dt, caFingerprint as et, PAIRING_TTL_MS as f, AGENT_UI_UNLOCK_TTL_MS as ft, keyFingerprintShort as g, UnlockTicketJtiStore as gt, describeCsr as h, AGENT_UI_UNLOCK_VERSION as ht, AgentPairing as i, generateMeshCa as it, emitProcessNodeRegistered as j, setProcessMeshEnforcer as jt, verifyMeshJoinToken as k, setProcessMeshAuthMode as kt, PAIRING_PATHS as l, loadMeshCa as lt, computePairingSas as m, AGENT_UI_UNLOCK_USE as mt, HubPairing as n, createMeshCsrPem as nt, PAIRING_CODE_DIGITS as o, isValidAgentNodeId as ot, computePairingCommitment as p, AGENT_UI_UNLOCK_TYP as pt, getProcessMeshCertificateRegistry as q, PairingHttpError as r, exportMeshPrivateKeyPem as rt, PAIRING_MAX_CODE_ATTEMPTS as s, issueMeshLeafForNode as st, systemConfigMeshActions as t, caFingerprintCompact as tt, PAIRING_PIN_DIGITS as u, AGENT_DASHBOARD_ORIGINS_FIELD as ut, verifyPairingProof as v, mintAgentUiUnlockTicket as vt, setProcessMeshTlsEnabler as w, getProcessMeshAuthMode as wt, Errors as x, verifyAgentUiUnlockTicket as xt, MESH_TLS_PORT_HELLO_FIELD as y, mintAgentUiUnlockTicketEs256 as yt, getProcessMeshTlsListenerState as z };