@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
@@ -3,11 +3,11 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BCKOeWu_.js");
6
+ const require_dist = require("../../dist-CCH3XPqO.js");
7
7
  const require_safe_buffer$1 = require("../../safe-buffer-D3aoa-7X.js");
8
- const require_x509_es = require("../../x509.es-Dzl0BbmG.js");
9
- const require_agent_https_names = require("../../agent-https-names-CpUSuP4w.js");
10
- const require_constants$8 = require("../../constants-DfP8V3eX.js");
8
+ const require_x509_es = require("../../x509.es-r7Ht1thA.js");
9
+ const require_agent_https = require("../../agent-https-BZIwS4xF.js");
10
+ const require_constants$8 = require("../../constants-CuOfMdYe.js");
11
11
  const require_src$1 = require("../../src-CnAqG6EF.js");
12
12
  let zod = require("zod");
13
13
  let node_crypto = require("node:crypto");
@@ -167,6 +167,12 @@ function getProcessMeshAuthStatus() {
167
167
  */
168
168
  var AGENT_UI_UNLOCK_DOMAIN = "camstack-agent-ui-unlock/v1";
169
169
  var AGENT_UI_UNLOCK_VERSION = 1;
170
+ /**
171
+ * D857 (transition release): a ticket signed by the MESH CA key (ES256 JWS) is accepted by an agent
172
+ * IN ADDITION to the HMAC ticket. The removal release mints only this form.
173
+ */
174
+ var AGENT_UI_UNLOCK_TYP = "camstack-agent-ui-unlock+jwt";
175
+ var AGENT_UI_UNLOCK_USE = "agent-ui-unlock";
170
176
  /** Ticket lifetime — long enough for a redirect hop, short enough to steal poorly. */
171
177
  var AGENT_UI_UNLOCK_TTL_MS = 5 * 6e4;
172
178
  /**
@@ -202,11 +208,18 @@ function mintAgentUiUnlockTicket(input) {
202
208
  const payload = {
203
209
  v: 1,
204
210
  nodeId: input.nodeId,
211
+ iat: nowMs,
205
212
  exp: nowMs + ttl,
206
213
  jti: input.jti ?? (0, node_crypto.randomBytes)(16).toString("base64url")
207
214
  };
208
215
  const payloadB64 = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
209
- return `${payloadB64}.${sign(input.secretHash, payloadB64)}`;
216
+ return `${payloadB64}.${sign$1(input.secretHash, payloadB64)}`;
217
+ }
218
+ /** A ticket is issued now-ish, lives at most the TTL, and not before `notBeforeMs`. */
219
+ function issuedSanely(payload, nowMs, notBeforeMs) {
220
+ if (payload.iat > nowMs + 6e4) return false;
221
+ if (payload.exp - payload.iat > 3e5) return false;
222
+ return notBeforeMs === void 0 || payload.iat >= notBeforeMs;
210
223
  }
211
224
  /**
212
225
  * Verify a ticket under `secretHash`. Returns the payload, or `null` when the
@@ -218,7 +231,7 @@ function verifyAgentUiUnlockTicket(secretHash, ticket, options) {
218
231
  if (dot <= 0 || dot === ticket.length - 1) return null;
219
232
  const payloadB64 = ticket.slice(0, dot);
220
233
  const sig = ticket.slice(dot + 1);
221
- if (!sigsEqual(sign(secretHash, payloadB64), sig)) return null;
234
+ if (!sigsEqual(sign$1(secretHash, payloadB64), sig)) return null;
222
235
  let parsed;
223
236
  try {
224
237
  parsed = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
@@ -229,6 +242,7 @@ function verifyAgentUiUnlockTicket(secretHash, ticket, options) {
229
242
  if (payload === null) return null;
230
243
  if (payload.exp <= nowMs) return null;
231
244
  if (payload.nodeId !== options.expectedNodeId) return null;
245
+ if (!issuedSanely(payload, nowMs, options.notBeforeMs)) return null;
232
246
  return payload;
233
247
  }
234
248
  /**
@@ -246,11 +260,29 @@ function isAllowedAgentDashboardReturnTo(returnTo, extraOrigins = []) {
246
260
  if (url.protocol !== "http:" && url.protocol !== "https:") return false;
247
261
  if (url.username !== "" || url.password !== "") return false;
248
262
  if (url.pathname !== "/" && url.pathname !== "") return false;
249
- if (url.port === "4444") return true;
263
+ const host = url.hostname.replace(/^\[|\]$/g, "");
264
+ const local = host === "localhost" || require_agent_https.isPrivateAddress(host) || require_agent_https.isLocalOnlyHostname(host);
265
+ if (url.port === "4444") return local;
250
266
  const origin = url.origin;
251
267
  return extraOrigins.some((o) => o === origin);
252
268
  }
253
269
  /** Parse a stored comma/whitespace-separated origins list. */
270
+ /**
271
+ * True when `returnTo` points at an address the hub itself knows for the node (`knownNames`: its id
272
+ * and the address of its live mesh socket), or at an origin the operator listed. `knownNames` of
273
+ * `null` (nothing recorded for the node yet) refuses.
274
+ */
275
+ function isKnownAgentAddress(returnTo, knownNames, listedOrigins = []) {
276
+ let url;
277
+ try {
278
+ url = new URL(returnTo);
279
+ } catch {
280
+ return false;
281
+ }
282
+ if (listedOrigins.includes(url.origin)) return true;
283
+ if (knownNames === null) return false;
284
+ return knownNames.includes(url.hostname.replace(/^\[|\]$/g, "").toLowerCase());
285
+ }
254
286
  function parseAgentDashboardOrigins(stored) {
255
287
  if (typeof stored !== "string" || stored.trim().length === 0) return [];
256
288
  const out = [];
@@ -264,7 +296,7 @@ function parseAgentDashboardOrigins(stored) {
264
296
  }
265
297
  return out;
266
298
  }
267
- function sign(secretHash, payloadB64) {
299
+ function sign$1(secretHash, payloadB64) {
268
300
  return (0, node_crypto.createHmac)("sha256", Buffer.from(secretHash, "utf8")).update(`${AGENT_UI_UNLOCK_DOMAIN}|${payloadB64}`, "utf8").digest("base64url");
269
301
  }
270
302
  function sigsEqual(expected, presented) {
@@ -283,15 +315,108 @@ function parsePayload(value) {
283
315
  if (typeof nodeId !== "string" || nodeId.length === 0) return null;
284
316
  if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
285
317
  if (typeof jti !== "string" || jti.length === 0) return null;
318
+ const iat = rec["iat"];
286
319
  return {
287
320
  v: 1,
288
321
  nodeId,
322
+ iat: typeof iat === "number" && Number.isFinite(iat) ? iat : exp - AGENT_UI_UNLOCK_TTL_MS,
323
+ exp,
324
+ jti
325
+ };
326
+ }
327
+ function b64urlJsonEs256(value) {
328
+ return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
329
+ }
330
+ function mintAgentUiUnlockTicketEs256(input) {
331
+ const nowMs = input.nowMs ?? Date.now();
332
+ const ttl = Math.min(Math.max(1e3, input.ttlMs ?? 3e5), AGENT_UI_UNLOCK_TTL_MS);
333
+ const payload = {
334
+ v: 1,
335
+ use: AGENT_UI_UNLOCK_USE,
336
+ nodeId: input.nodeId,
337
+ iat: nowMs,
338
+ exp: nowMs + ttl,
339
+ jti: input.jti ?? (0, node_crypto.randomBytes)(16).toString("base64url")
340
+ };
341
+ const signingInput = `${b64urlJsonEs256({
342
+ alg: "ES256",
343
+ typ: AGENT_UI_UNLOCK_TYP
344
+ })}.${b64urlJsonEs256(payload)}`;
345
+ return `${signingInput}.${(0, node_crypto.sign)("sha256", Buffer.from(signingInput, "utf8"), {
346
+ key: (0, node_crypto.createPrivateKey)(input.caKeyPem),
347
+ dsaEncoding: "ieee-p1363"
348
+ }).toString("base64url")}`;
349
+ }
350
+ /**
351
+ * Verify a ticket against the mesh CA certificate the agent pinned. Returns the
352
+ * payload, or `null` when the signature, header, shape, expiry or nodeId fails
353
+ * (no reason string — refuse quietly).
354
+ */
355
+ function verifyAgentUiUnlockTicketEs256(caCertPem, ticket, options) {
356
+ const nowMs = options.nowMs ?? Date.now();
357
+ const parts = ticket.split(".");
358
+ if (parts.length !== 3) return null;
359
+ const [headerB64, payloadB64, signatureB64] = parts;
360
+ if (headerB64 === void 0 || payloadB64 === void 0 || signatureB64 === void 0) return null;
361
+ let header;
362
+ let parsed;
363
+ try {
364
+ header = JSON.parse(Buffer.from(headerB64, "base64url").toString("utf8"));
365
+ parsed = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
366
+ } catch {
367
+ return null;
368
+ }
369
+ if (typeof header !== "object" || header === null) return null;
370
+ if (Reflect.get(header, "alg") !== "ES256" || Reflect.get(header, "typ") !== "camstack-agent-ui-unlock+jwt") return null;
371
+ let valid = false;
372
+ try {
373
+ valid = (0, node_crypto.verify)("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), {
374
+ key: new node_crypto.X509Certificate(caCertPem).publicKey,
375
+ dsaEncoding: "ieee-p1363"
376
+ }, Buffer.from(signatureB64, "base64url"));
377
+ } catch {
378
+ return null;
379
+ }
380
+ if (!valid) return null;
381
+ const payload = parsePayloadEs256(parsed);
382
+ if (payload === null) return null;
383
+ if (payload.exp <= nowMs) return null;
384
+ if (payload.nodeId !== options.expectedNodeId) return null;
385
+ if (!issuedSanely(payload, nowMs, options.notBeforeMs)) return null;
386
+ return payload;
387
+ }
388
+ function parsePayloadEs256(value) {
389
+ if (typeof value !== "object" || value === null) return null;
390
+ const rec = value;
391
+ if (rec["v"] !== 1) return null;
392
+ if (rec["use"] !== "agent-ui-unlock") return null;
393
+ const nodeId = rec["nodeId"];
394
+ const exp = rec["exp"];
395
+ const jti = rec["jti"];
396
+ if (typeof nodeId !== "string" || nodeId.length === 0) return null;
397
+ if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
398
+ if (typeof jti !== "string" || jti.length === 0) return null;
399
+ const iat = rec["iat"];
400
+ if (typeof iat !== "number" || !Number.isFinite(iat)) return null;
401
+ return {
402
+ v: 1,
403
+ nodeId,
404
+ iat,
289
405
  exp,
290
406
  jti
291
407
  };
292
408
  }
293
409
  //#endregion
294
410
  //#region src/kernel/moleculer/mesh-mtls/mesh-ca.ts
411
+ /**
412
+ * Hub mesh CA — EC P-256, key in the keychain, cert in `cluster.meshCa`.
413
+ *
414
+ * Distinct from the HTTPS local CA (`tls/cert-issue.ts`, RSA): the mesh CA
415
+ * signs node leaves (CN = nodeID) and join-token JWS (ES256). Issuance uses
416
+ * `@peculiar/x509` over WebCrypto — same library the HTTPS path already uses
417
+ * (installed `1.14.3`; registry latest at adoption time was `2.1.0` — stay on
418
+ * the pinned range already shipping).
419
+ */
295
420
  require_x509_es.cryptoProvider.set(node_crypto.webcrypto);
296
421
  var MS_PER_DAY = 864e5;
297
422
  /** Key generation — EC P-256 (plan). WebCrypto DOM names are not in @types/node. */
@@ -304,6 +429,14 @@ var MESH_SIGNING_ALGORITHM = {
304
429
  name: "ECDSA",
305
430
  hash: "SHA-256"
306
431
  };
432
+ /** A node id: letters, digits, `.`, `_`, `-`; at most 63 characters; starts with a letter or digit. */
433
+ var NODE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,62}$/i;
434
+ /** Ids no agent may take: the hub's own, whatever the case (D857). */
435
+ var RESERVED_NODE_IDS = new Set(["hub"]);
436
+ /** True for an id an agent may carry: the pattern, and not a reserved name. */
437
+ function isValidAgentNodeId(nodeId) {
438
+ return NODE_ID_PATTERN.test(nodeId) && !RESERVED_NODE_IDS.has(nodeId.toLowerCase());
439
+ }
307
440
  function freshSerial() {
308
441
  return `00${(0, node_crypto.randomBytes)(16).toString("hex")}`;
309
442
  }
@@ -334,7 +467,7 @@ async function generateMeshCa(now = /* @__PURE__ */ new Date()) {
334
467
  const cert = await require_x509_es.X509CertificateGenerator.createSelfSigned({
335
468
  serialNumber: freshSerial(),
336
469
  name: `CN=${require_constants$8.MESH_CA_COMMON_NAME}`,
337
- notBefore: now,
470
+ notBefore: require_x509_es.backdatedNotBefore(now),
338
471
  notAfter: new Date(now.getTime() + require_constants$8.MESH_CA_VALIDITY_DAYS * MS_PER_DAY),
339
472
  keys,
340
473
  signingAlgorithm: MESH_SIGNING_ALGORITHM,
@@ -367,6 +500,7 @@ async function issueMeshLeafFromCsr(ca, csrPem, opts = {}) {
367
500
  if (!await csr.verify(node_crypto.webcrypto)) throw new Error("CSR signature is invalid");
368
501
  const cn = subjectCn(csr.subject);
369
502
  if (cn === null || cn.length === 0) throw new Error("CSR subject has no CN");
503
+ if (opts.allowReserved !== true && !isValidAgentNodeId(cn)) throw new Error(`CSR CN '${cn}' is not an id an agent may carry`);
370
504
  if (opts.expectedNodeId !== void 0 && opts.expectedNodeId !== cn) throw new Error(`CSR CN '${cn}' does not match the token nodeId '${opts.expectedNodeId}'`);
371
505
  const now = opts.now ?? /* @__PURE__ */ new Date();
372
506
  const validDays = opts.validDays ?? 30;
@@ -384,7 +518,7 @@ async function issueMeshLeafFromCsr(ca, csrPem, opts = {}) {
384
518
  serialNumber,
385
519
  subject: `CN=${cn}`,
386
520
  issuer: ca.cert.subject,
387
- notBefore: now,
521
+ notBefore: require_x509_es.backdatedNotBefore(now),
388
522
  notAfter,
389
523
  publicKey: csr.publicKey,
390
524
  signingKey: ca.privateKey,
@@ -403,7 +537,8 @@ async function issueMeshLeafForNode(ca, nodeId, opts = {}) {
403
537
  ...await issueMeshLeafFromCsr(ca, await createMeshCsrPem(keys, nodeId), {
404
538
  expectedNodeId: nodeId,
405
539
  now: opts.now,
406
- validDays: opts.validDays
540
+ validDays: opts.validDays,
541
+ allowReserved: true
407
542
  }),
408
543
  keyPem: await exportKeyPem(keys.privateKey)
409
544
  };
@@ -693,6 +828,7 @@ var RETENTION_MS = 90 * 864e5;
693
828
  var RENEWAL_ABANDONED = "renewal-abandoned";
694
829
  var VIAS = [
695
830
  "enroll",
831
+ "pairing",
696
832
  "renewal",
697
833
  "boot",
698
834
  "observed"
@@ -866,9 +1002,10 @@ var MeshCertificateRegistry = class {
866
1002
  * valid just because the hub once set it aside). Returns the serials put on the CRL.
867
1003
  * A leaf revoked this way is never reinstated by `recordObserved`.
868
1004
  */
869
- async revokeAllForNode(nodeId, reason, crl) {
1005
+ async revokeAllForNode(nodeId, reason, crl, keepFingerprint) {
870
1006
  const serials = [];
871
1007
  for (const record of this.list(nodeId)) {
1008
+ if (keepFingerprint !== void 0 && record.fingerprint === keepFingerprint) continue;
872
1009
  if (record.revokedAt !== null && record.revokedReason !== "renewal-abandoned") continue;
873
1010
  const revoked = {
874
1011
  ...record,
@@ -1318,6 +1455,10 @@ function getProcessNodeMeshTransport(nodeId) {
1318
1455
  function getProcessPeerLeaf(nodeId) {
1319
1456
  return getProcessMeshPeerTransportLookup()?.peerLeaf?.(nodeId);
1320
1457
  }
1458
+ /** True when the hub's transporter holds a certificate-verified mesh peer called `nodeId` (D857). */
1459
+ function isProcessVerifiedMeshPeer(nodeId) {
1460
+ return getProcessMeshPeerTransportLookup()?.isVerifiedPeer?.(nodeId) === true;
1461
+ }
1321
1462
  /** Disconnect `nodeId` (closes its mutual-TLS sockets). `0` when none were open. */
1322
1463
  function dropProcessPeer(nodeId) {
1323
1464
  return getProcessMeshPeerTransportLookup()?.dropPeer?.(nodeId) ?? 0;
@@ -2026,7 +2167,8 @@ var SignJWT = class extends SignJWT_base {
2026
2167
  /**
2027
2168
  * Mesh join tokens — single-use JWS ES256 signed with the hub mesh CA key.
2028
2169
  *
2029
- * Payload: `{ v, hubUrl, hubMeshAddress, caFingerprint, nodeId?, jti, exp }`.
2170
+ * Payload: `{ v, hubUrl, hubMeshAddress, caFingerprint, nodeId, jti, exp }`. The node id is MANDATORY
2171
+ * (D857): a token that does not name the node it is for would let its holder pick any id, `hub` included.
2030
2172
  * `exp` ≤ now + 1 h. `jti` is recorded until expiry so a token cannot be reused.
2031
2173
  */
2032
2174
  /** In-memory + optional persist of spent `jti` until `exp`. */
@@ -2057,6 +2199,7 @@ function processJoinTokenJtiStore() {
2057
2199
  return processJtiStore;
2058
2200
  }
2059
2201
  async function mintMeshJoinToken(input) {
2202
+ 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`);
2060
2203
  const now = input.now ?? /* @__PURE__ */ new Date();
2061
2204
  const nowSec = Math.floor(now.getTime() / 1e3);
2062
2205
  const exp = nowSec + Math.min(Math.max(1, input.ttlSec ?? 3600), require_constants$8.MESH_JOIN_TOKEN_MAX_TTL_SEC);
@@ -2066,7 +2209,7 @@ async function mintMeshJoinToken(input) {
2066
2209
  hubUrl: input.hubUrl,
2067
2210
  hubMeshAddress: input.hubMeshAddress,
2068
2211
  caFingerprint: caFingerprintCompact(input.ca.certPem),
2069
- ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
2212
+ nodeId: input.nodeId,
2070
2213
  jti,
2071
2214
  exp
2072
2215
  }).setProtectedHeader({
@@ -2094,7 +2237,7 @@ async function verifyMeshJoinToken(input) {
2094
2237
  reason: `token signature invalid: ${msg}`
2095
2238
  };
2096
2239
  }
2097
- const claims = parseClaims(payload);
2240
+ const claims = parseClaims$1(payload);
2098
2241
  if (claims === null) return {
2099
2242
  ok: false,
2100
2243
  reason: "token claims malformed"
@@ -2130,7 +2273,7 @@ async function verifyMeshJoinToken(input) {
2130
2273
  function normalizeFp(fp) {
2131
2274
  return fp.replace(/:/g, "").toLowerCase();
2132
2275
  }
2133
- function parseClaims(payload) {
2276
+ function parseClaims$1(payload) {
2134
2277
  if (payload["v"] !== 1) return null;
2135
2278
  const hubUrl = payload["hubUrl"];
2136
2279
  const hubMeshAddress = payload["hubMeshAddress"];
@@ -2143,12 +2286,13 @@ function parseClaims(payload) {
2143
2286
  if (typeof jti !== "string" || jti.length === 0) return null;
2144
2287
  if (exp === null) return null;
2145
2288
  const nodeId = payload["nodeId"];
2289
+ if (typeof nodeId !== "string" || !isValidAgentNodeId(nodeId)) return null;
2146
2290
  return {
2147
2291
  v: 1,
2148
2292
  hubUrl,
2149
2293
  hubMeshAddress,
2150
2294
  caFingerprint: normalizeFp(caFingerprint),
2151
- ...typeof nodeId === "string" && nodeId.length > 0 ? { nodeId } : {},
2295
+ nodeId,
2152
2296
  jti,
2153
2297
  exp
2154
2298
  };
@@ -87724,6 +87868,26 @@ var TlsTcpTransporter = class extends TcpTransporterBase {
87724
87868
  return this.tlsPeers.has(nodeId) ? "tls" : "clear";
87725
87869
  }
87726
87870
  /**
87871
+ * The certificate identity of `nodeId` (D857): its mutual-TLS handshake chained to the
87872
+ * mesh CA this transporter was built with, its leaf CN is `nodeId`, and the leaf is not
87873
+ * revoked now. This is the ONLY proof of "who is calling" the mesh gates accept:
87874
+ * every inbound packet on a TLS socket is dropped unless its `sender` equals the leaf CN
87875
+ * (see `onIncomingMessage`), so a node id in a delivered packet that is verified here was
87876
+ * claimed by the holder of that node's private key.
87877
+ *
87878
+ * `undefined` for a node that reached us over clear text, never completed a handshake,
87879
+ * or whose serial is on the CRL.
87880
+ */
87881
+ verifiedPeer(nodeId) {
87882
+ const serial = this.tlsPeers.get(nodeId);
87883
+ if (serial === void 0) return void 0;
87884
+ if (serial.length > 0 && this.crl.isRevoked(serial)) return void 0;
87885
+ return {
87886
+ nodeId,
87887
+ serial
87888
+ };
87889
+ }
87890
+ /**
87727
87891
  * The node disconnected: its next connection decides its transport again —
87728
87892
  * unless it already re-handshook (the disconnect can be noticed after the
87729
87893
  * restarted node is back), in which case its live TLS socket keeps it `tls`.
@@ -87965,6 +88129,1174 @@ function isTlsSocket(socket) {
87965
88129
  return typeof socket.getPeerCertificate === "function" && typeof socket.encrypted === "boolean" && socket.encrypted === true;
87966
88130
  }
87967
88131
  //#endregion
88132
+ //#region src/kernel/moleculer/mesh-mtls/pairing/pairing-protocol.ts
88133
+ /**
88134
+ * Agent pairing — the cryptographic core (D857).
88135
+ *
88136
+ * Pairing makes an agent hold a certificate from the hub's mesh CA, with no
88137
+ * shared secret anywhere. The hub starts it (LAN discovery or a typed
88138
+ * address); the operator confirms the SAME six digits on the hub and in the
88139
+ * agent's own page. Everything the human compares is derived here.
88140
+ *
88141
+ * hub → agent offer { hub CA cert, commit = H(caFp ‖ hubNonce), hubUrl, hubMeshAddress }
88142
+ * agent → hub answer { pairingId, CSR (mesh key made on the agent), agentNonce }
88143
+ * hub → agent reveal { hubNonce } (agent checks it against the commit)
88144
+ * both SAS = 6 digits of H(caFp ‖ agentPublicKey ‖ hubNonce ‖ agentNonce)
88145
+ * operator reads the code on one screen and types it on the other, on both sides
88146
+ * hub → agent complete { leaf, proof } (only after both confirmations)
88147
+ *
88148
+ * Why a commitment: a six-digit code is 20 bits. An attacker between the two
88149
+ * who could still choose a value AFTER seeing the others could grind a CA key
88150
+ * whose code equals the real one in a million tries. The hub therefore commits
88151
+ * to its nonce before it sees the agent's answer and reveals it only after,
88152
+ * so nothing the attacker controls is chosen after everything the code is
88153
+ * derived from is fixed. A substituted CA changes the code with probability
88154
+ * 999 999 / 1 000 000 per attempt, and a wrong code ends the pairing after
88155
+ * three tries.
88156
+ *
88157
+ * `proof` is an ES256 JWS by the mesh CA key over the transcript (pairing id,
88158
+ * node id, both nonces, the leaf fingerprint, the CA fingerprint). The agent
88159
+ * verifies it with the CA certificate it was OFFERED and the operator
88160
+ * confirmed — never with one that arrives in the same message.
88161
+ */
88162
+ var PAIRING_PROTOCOL = 1;
88163
+ var PAIRING_DOMAIN = "camstack-pairing/v1";
88164
+ var PAIRING_PROOF_TYP = "camstack-pairing-complete+jwt";
88165
+ var PAIRING_PROOF_USE = "pairing-complete";
88166
+ var PAIRING_OFFER_TYP = "camstack-pairing-offer+jwt";
88167
+ var PAIRING_OFFER_USE = "pairing-offer";
88168
+ /** An offer signature older than this (or from further ahead than 60 s) is refused. */
88169
+ var PAIRING_OFFER_MAX_AGE_MS = 2 * 6e4;
88170
+ /** A pairing that is not confirmed on both sides within this window is cancelled on both. */
88171
+ var PAIRING_TTL_MS = 5 * 6e4;
88172
+ /** Wrong codes typed before the pairing is cancelled. */
88173
+ var PAIRING_MAX_CODE_ATTEMPTS = 3;
88174
+ var PAIRING_CODE_DIGITS = 6;
88175
+ /** Pending pairings an agent holds at once, one per hub CA (offer squatting, D857). */
88176
+ var PAIRING_MAX_SESSIONS = 4;
88177
+ /** The local PIN: single use, shown only on the agent's own machine. */
88178
+ var PAIRING_PIN_DIGITS = 8;
88179
+ /** A pairing proof is valid this long from `iat`, never longer. */
88180
+ var PAIRING_PROOF_MAX_LIFETIME_MS = 5 * 6e4;
88181
+ /** Agent endpoints, all under the agent API (`:4444`). */
88182
+ var PAIRING_PATHS = {
88183
+ hello: "/api/agent/pairing/hello",
88184
+ offer: "/api/agent/pairing/offer",
88185
+ reveal: "/api/agent/pairing/reveal",
88186
+ /** Public: who asks to pair - never a code, a pairing id or a PIN. */
88187
+ status: "/api/agent/pairing/status",
88188
+ /** The hub's poll, authorized by the pairing id: did the agent's operator confirm? */
88189
+ poll: "/api/agent/pairing/poll",
88190
+ complete: "/api/agent/pairing/complete",
88191
+ /** The agent's operator: the hub's code + the local PIN (or genuine loopback). */
88192
+ confirm: "/api/agent/pairing/confirm",
88193
+ reject: "/api/agent/pairing/reject",
88194
+ /** The hub withdraws its pairing, authorized by the pairing id. */
88195
+ cancel: "/api/agent/pairing/cancel",
88196
+ /** The agent's operator clears lockouts and pending offers (loopback, or the local reset code). */
88197
+ reset: "/api/agent/pairing/reset",
88198
+ forget: "/api/agent/pairing/forget"
88199
+ };
88200
+ /** A fresh single-use local PIN: digits only, uniformly random. */
88201
+ function randomPin() {
88202
+ return String((0, node_crypto.randomInt)(0, 10 ** 8)).padStart(8, "0");
88203
+ }
88204
+ function randomHex(bytes) {
88205
+ return (0, node_crypto.randomBytes)(bytes).toString("hex");
88206
+ }
88207
+ function newPairingId() {
88208
+ return (0, node_crypto.randomBytes)(16).toString("base64url");
88209
+ }
88210
+ /** The hub's commitment to its nonce, bound to the CA certificate it offers. */
88211
+ function computePairingCommitment(caCertPem, hubNonce) {
88212
+ return (0, node_crypto.createHash)("sha256").update(`${PAIRING_DOMAIN}|commit|${caFingerprintCompact(caCertPem)}|${hubNonce}`, "utf8").digest("hex");
88213
+ }
88214
+ function commitmentsEqual(expected, presented) {
88215
+ return constantTimeEqual(expected, presented);
88216
+ }
88217
+ /** The six digits both screens show. */
88218
+ function computePairingSas(input) {
88219
+ const value = (0, node_crypto.createHash)("sha256").update([
88220
+ PAIRING_DOMAIN,
88221
+ "sas",
88222
+ caFingerprintCompact(input.caCertPem),
88223
+ input.agentPublicKeySpki,
88224
+ input.hubNonce,
88225
+ input.agentNonce
88226
+ ].join("|"), "utf8").digest().readUInt32BE(0) % 10 ** 6;
88227
+ return String(value).padStart(6, "0");
88228
+ }
88229
+ function constantTimeEqual(expected, presented) {
88230
+ if (typeof presented !== "string") return false;
88231
+ const a = Buffer.from(expected, "utf8");
88232
+ const b = Buffer.from(presented, "utf8");
88233
+ return a.length === b.length && (0, node_crypto.timingSafeEqual)(a, b);
88234
+ }
88235
+ async function describeCsr(csrPem) {
88236
+ const csr = new require_x509_es.Pkcs10CertificateRequest(csrPem);
88237
+ if (!await csr.verify(node_crypto.webcrypto)) throw new Error("the agent CSR signature is invalid");
88238
+ const cn = subjectCn(csr.subject);
88239
+ if (cn === null || cn.length === 0) throw new Error("the agent CSR has no CN");
88240
+ return {
88241
+ commonName: cn,
88242
+ publicKeySpki: Buffer.from(csr.publicKey.rawData).toString("base64")
88243
+ };
88244
+ }
88245
+ function b64urlJson(value) {
88246
+ return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
88247
+ }
88248
+ function leafFingerprintCompact(certPem) {
88249
+ return (0, node_crypto.createHash)("sha256").update(new node_crypto.X509Certificate(certPem).raw).digest("hex");
88250
+ }
88251
+ function signPairingProof(caKeyPem, claims) {
88252
+ const signingInput = `${b64urlJson({
88253
+ alg: "ES256",
88254
+ typ: PAIRING_PROOF_TYP
88255
+ })}.${b64urlJson(claims)}`;
88256
+ return `${signingInput}.${(0, node_crypto.sign)("sha256", Buffer.from(signingInput, "utf8"), {
88257
+ key: (0, node_crypto.createPrivateKey)(caKeyPem),
88258
+ dsaEncoding: "ieee-p1363"
88259
+ }).toString("base64url")}`;
88260
+ }
88261
+ /**
88262
+ * The claims of `proof` when it was signed by `caCertPem`'s key, else `null`.
88263
+ * Shape, expiry and the binding to a session are the caller's to check.
88264
+ */
88265
+ function verifyPairingProof(caCertPem, proof) {
88266
+ const parts = proof.split(".");
88267
+ if (parts.length !== 3) return null;
88268
+ const [headerB64, payloadB64, signatureB64] = parts;
88269
+ if (headerB64 === void 0 || payloadB64 === void 0 || signatureB64 === void 0) return null;
88270
+ let header;
88271
+ let payload;
88272
+ try {
88273
+ header = JSON.parse(Buffer.from(headerB64, "base64url").toString("utf8"));
88274
+ payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
88275
+ } catch {
88276
+ return null;
88277
+ }
88278
+ if (header === null || typeof header !== "object") return null;
88279
+ if (Reflect.get(header, "alg") !== "ES256" || Reflect.get(header, "typ") !== "camstack-pairing-complete+jwt") return null;
88280
+ let valid = false;
88281
+ try {
88282
+ valid = (0, node_crypto.verify)("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), {
88283
+ key: new node_crypto.X509Certificate(caCertPem).publicKey,
88284
+ dsaEncoding: "ieee-p1363"
88285
+ }, Buffer.from(signatureB64, "base64url"));
88286
+ } catch {
88287
+ return null;
88288
+ }
88289
+ return valid ? parseClaims(payload) : null;
88290
+ }
88291
+ function parseClaims(value) {
88292
+ if (value === null || typeof value !== "object") return null;
88293
+ const str = (key) => {
88294
+ const v = Reflect.get(value, key);
88295
+ return typeof v === "string" && v.length > 0 ? v : null;
88296
+ };
88297
+ const pairingId = str("pairingId");
88298
+ const nodeId = str("nodeId");
88299
+ const hubNonce = str("hubNonce");
88300
+ const agentNonce = str("agentNonce");
88301
+ const leafFingerprint = str("leafFingerprint");
88302
+ const caFingerprint = str("caFingerprint");
88303
+ const hubMeshAddress = str("hubMeshAddress");
88304
+ const hubUrl = str("hubUrl");
88305
+ const exp = Reflect.get(value, "exp");
88306
+ const iat = Reflect.get(value, "iat");
88307
+ if (Reflect.get(value, "v") !== 1) return null;
88308
+ if (Reflect.get(value, "use") !== "pairing-complete") return null;
88309
+ 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;
88310
+ return {
88311
+ v: 1,
88312
+ use: PAIRING_PROOF_USE,
88313
+ pairingId,
88314
+ nodeId,
88315
+ hubNonce,
88316
+ agentNonce,
88317
+ leafFingerprint,
88318
+ caFingerprint,
88319
+ hubMeshAddress,
88320
+ hubUrl,
88321
+ iat,
88322
+ exp
88323
+ };
88324
+ }
88325
+ /** A short public fingerprint of a public key, for the mDNS TXT record. */
88326
+ function keyFingerprintShort(publicKeySpkiB64) {
88327
+ return (0, node_crypto.createHash)("sha256").update(Buffer.from(publicKeySpkiB64, "base64")).digest("hex").slice(0, 16);
88328
+ }
88329
+ function signPairingOffer(caKeyPem, claims) {
88330
+ const signingInput = `${b64urlJson({
88331
+ alg: "ES256",
88332
+ typ: PAIRING_OFFER_TYP
88333
+ })}.${b64urlJson(claims)}`;
88334
+ return `${signingInput}.${(0, node_crypto.sign)("sha256", Buffer.from(signingInput, "utf8"), {
88335
+ key: (0, node_crypto.createPrivateKey)(caKeyPem),
88336
+ dsaEncoding: "ieee-p1363"
88337
+ }).toString("base64url")}`;
88338
+ }
88339
+ /** The claims of `token` when `caCertPem`'s key signed it as a pairing offer, else `null`. */
88340
+ function verifyPairingOffer(caCertPem, token) {
88341
+ const parts = token.split(".");
88342
+ if (parts.length !== 3) return null;
88343
+ const [headerB64, payloadB64, signatureB64] = parts;
88344
+ if (headerB64 === void 0 || payloadB64 === void 0 || signatureB64 === void 0) return null;
88345
+ let header;
88346
+ let payload;
88347
+ try {
88348
+ header = JSON.parse(Buffer.from(headerB64, "base64url").toString("utf8"));
88349
+ payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
88350
+ } catch {
88351
+ return null;
88352
+ }
88353
+ if (header === null || typeof header !== "object") return null;
88354
+ if (Reflect.get(header, "alg") !== "ES256" || Reflect.get(header, "typ") !== "camstack-pairing-offer+jwt") return null;
88355
+ let valid = false;
88356
+ try {
88357
+ valid = (0, node_crypto.verify)("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), {
88358
+ key: new node_crypto.X509Certificate(caCertPem).publicKey,
88359
+ dsaEncoding: "ieee-p1363"
88360
+ }, Buffer.from(signatureB64, "base64url"));
88361
+ } catch {
88362
+ return null;
88363
+ }
88364
+ if (!valid || payload === null || typeof payload !== "object") return null;
88365
+ const str = (key) => {
88366
+ const v = Reflect.get(payload, key);
88367
+ return typeof v === "string" && v.length > 0 ? v : null;
88368
+ };
88369
+ const iat = Reflect.get(payload, "iat");
88370
+ const agentNodeId = str("agentNodeId");
88371
+ const hubNodeId = str("hubNodeId");
88372
+ const caFingerprint = str("caFingerprint");
88373
+ const commitment = str("commitment");
88374
+ const hubUrl = str("hubUrl");
88375
+ const hubMeshAddress = str("hubMeshAddress");
88376
+ if (Reflect.get(payload, "use") !== "pairing-offer") return null;
88377
+ if (typeof iat !== "number" || !Number.isFinite(iat)) return null;
88378
+ if (agentNodeId === null || hubNodeId === null || caFingerprint === null || commitment === null || hubUrl === null || hubMeshAddress === null) return null;
88379
+ return {
88380
+ use: PAIRING_OFFER_USE,
88381
+ agentNodeId,
88382
+ hubNodeId,
88383
+ caFingerprint,
88384
+ commitment,
88385
+ hubUrl,
88386
+ hubMeshAddress,
88387
+ iat
88388
+ };
88389
+ }
88390
+ //#endregion
88391
+ //#region src/kernel/moleculer/mesh-mtls/pairing/agent-pairing.ts
88392
+ /**
88393
+ * The agent's side of pairing (D857): up to four pending pairings (one per hub CA), their keys
88394
+ * in memory only, installed ONLY after the agent's own operator confirmed the same six digits
88395
+ * the hub shows AND proved presence at this agent with a one-time local PIN, and the hub's
88396
+ * transcript proof verified under the CA the agent was offered.
88397
+ *
88398
+ * Why a PIN: the agent's page is opened from another PC and its API is reachable by the whole
88399
+ * LAN. Anything the network can read, the network can use to "confirm" - so the code, the pairing
88400
+ * id and the PIN are shown ONLY on the agent's own machine (its console, a 0600 file, the local
88401
+ * window). A request that is not genuinely local must present the PIN of the pairing it confirms.
88402
+ *
88403
+ * unpaired ──offer──▶ pending ──reveal──▶ (code known) ──confirm(code + PIN) + complete──▶ paired
88404
+ * │ │
88405
+ * └── reject / expiry / 3 wrong codes / 3 wrong PINs ──▶ unpaired
88406
+ *
88407
+ * Offers are rationed: pairings that ended without the local operator confirming them count
88408
+ * against a budget (3 per 10 minutes, then exponential lockout) and against the hub CA that made
88409
+ * them (so a loop of "offer, read the code, abort" cannot grind the six digits). A paired agent
88410
+ * refuses every offer.
88411
+ */
88412
+ /** Abandoned offers allowed in {@link BUDGET_WINDOW_MS} before the lockout starts. */
88413
+ var BUDGET_OFFERS = 3;
88414
+ var BUDGET_WINDOW_MS = 10 * 6e4;
88415
+ var LOCKOUT_BASE_MS = 5 * 6e4;
88416
+ var LOCKOUT_MAX_MS = 60 * 6e4;
88417
+ /** Per hub CA: nothing for the first abandonment, then this x 2^(n-2), capped. */
88418
+ var CA_BACKOFF_BASE_MS = 3e4;
88419
+ var CA_BACKOFF_MAX_MS = 10 * 6e4;
88420
+ /** Wrong PINs (any session) before confirming is locked for a while. */
88421
+ var PIN_FAILURE_LIMIT = 5;
88422
+ var PIN_LOCK_MS = 2 * 6e4;
88423
+ var PIN_LOCK_MAX_MS = 60 * 6e4;
88424
+ /** Wrong PINs across ALL sources within one pairing lifetime before every pending pairing is cancelled. */
88425
+ var PIN_GLOBAL_CEILING = 200;
88426
+ var MAX_TRACKED_SOURCES = 256;
88427
+ /** Outcomes kept by pairing id so the hub can learn how its pairing ended. */
88428
+ var OUTCOME_LIMIT = 16;
88429
+ var OUTCOME_TTL_MS = 15 * 6e4;
88430
+ var MAX_FIELD = 2048;
88431
+ function fail(status, error, message, retryAfterMs) {
88432
+ return {
88433
+ ok: false,
88434
+ status,
88435
+ error,
88436
+ message,
88437
+ ...retryAfterMs !== void 0 ? { retryAfterMs } : {}
88438
+ };
88439
+ }
88440
+ function isText(value, max = MAX_FIELD) {
88441
+ return typeof value === "string" && value.length > 0 && value.length <= max;
88442
+ }
88443
+ function isHttpUrl(value) {
88444
+ try {
88445
+ const u = new URL(value);
88446
+ return u.protocol === "https:" || u.protocol === "http:";
88447
+ } catch {
88448
+ return false;
88449
+ }
88450
+ }
88451
+ /**
88452
+ * Clock skew tolerated between the hub that minted a CA and this agent. X.509 dates have
88453
+ * one-second resolution and a fresh CA's notBefore is the hub's "now", so an agent whose clock
88454
+ * is even a moment behind would otherwise refuse a brand-new CA. Same bound as the offer `iat`.
88455
+ */
88456
+ var CA_CLOCK_SKEW_MS = 6e4;
88457
+ /** The hub CA must be a self-signed CA certificate, valid now (give or take the skew). */
88458
+ function readCaCertificate(pem, nowMs) {
88459
+ try {
88460
+ const cert = new node_crypto.X509Certificate(pem);
88461
+ if (!cert.ca || !cert.verify(cert.publicKey)) return null;
88462
+ if (Date.parse(cert.validTo) <= nowMs || Date.parse(cert.validFrom) > nowMs + CA_CLOCK_SKEW_MS) return null;
88463
+ return cert;
88464
+ } catch {
88465
+ return null;
88466
+ }
88467
+ }
88468
+ /** `host:6443` / `[::1]:6443` / `host` → the host, port dropped. */
88469
+ function hubHostFromMeshAddress(address) {
88470
+ const trimmed = address.trim();
88471
+ const bracketed = /^(\[[^\]]+\])(?::\d+)?$/.exec(trimmed);
88472
+ if (bracketed?.[1] !== void 0) return bracketed[1];
88473
+ return /^([^:]+):\d+$/.exec(trimmed)?.[1] ?? trimmed;
88474
+ }
88475
+ var AgentPairing = class {
88476
+ deps;
88477
+ sessions = /* @__PURE__ */ new Map();
88478
+ /** Outcome of each recent pairing BY id (bounded): ending others never overwrites an answer. */
88479
+ outcomes = /* @__PURE__ */ new Map();
88480
+ /** When pairings that never reached local confirmation ended. */
88481
+ abandoned = [];
88482
+ lockedUntil = 0;
88483
+ perCa = /* @__PURE__ */ new Map();
88484
+ pinBySource = /* @__PURE__ */ new Map();
88485
+ globalPinFailures = [];
88486
+ resetCode;
88487
+ /** Commitments already offered (signed offers are single use) until their signature is stale. */
88488
+ seenCommitments = /* @__PURE__ */ new Map();
88489
+ now;
88490
+ constructor(deps) {
88491
+ this.deps = deps;
88492
+ this.now = deps.now ?? Date.now;
88493
+ this.resetCode = randomPin();
88494
+ deps.onResetCode?.(this.resetCode);
88495
+ }
88496
+ hello() {
88497
+ this.expireIfDue();
88498
+ const expiries = [...this.sessions.values()].map((s) => s.expiresAt);
88499
+ return {
88500
+ protocol: 1,
88501
+ nodeId: this.deps.nodeId,
88502
+ version: this.deps.version,
88503
+ state: this.state(),
88504
+ pendingExpiresAt: expiries.length === 0 ? null : Math.min(...expiries)
88505
+ };
88506
+ }
88507
+ state() {
88508
+ this.expireIfDue();
88509
+ if (this.deps.isPaired()) return "paired";
88510
+ return this.sessions.size > 0 ? "pending" : "unpaired";
88511
+ }
88512
+ /** Who is asking to pair, and until when. No code, no pairing id, no PIN. */
88513
+ publicView() {
88514
+ this.expireIfDue();
88515
+ return {
88516
+ state: this.state(),
88517
+ pending: [...this.sessions.values()].map((s) => ({
88518
+ hub: {
88519
+ nodeId: s.hub.nodeId,
88520
+ url: s.hub.url,
88521
+ caFingerprint: s.hub.caFingerprint
88522
+ },
88523
+ expiresAt: s.expiresAt
88524
+ }))
88525
+ };
88526
+ }
88527
+ /** The view for the agent's own machine: also the pairing ids, the codes and the PINs. */
88528
+ localView() {
88529
+ this.expireIfDue();
88530
+ return {
88531
+ state: this.state(),
88532
+ pending: [...this.sessions.values()].map((s) => ({
88533
+ pairingId: s.pairingId,
88534
+ hub: {
88535
+ nodeId: s.hub.nodeId,
88536
+ url: s.hub.url,
88537
+ caFingerprint: s.hub.caFingerprint
88538
+ },
88539
+ expiresAt: s.expiresAt,
88540
+ pin: s.pin,
88541
+ code: s.code,
88542
+ localConfirmed: s.localConfirmed
88543
+ }))
88544
+ };
88545
+ }
88546
+ async offer(body) {
88547
+ this.expireIfDue();
88548
+ if (this.deps.isPaired()) return fail(409, "already-paired", "this agent is paired — unpair it first (from the hub or here)");
88549
+ 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`);
88550
+ const request = parseOffer(body);
88551
+ if (request === null) return fail(400, "bad-offer", "the offer is malformed");
88552
+ if (request.protocol !== 1) return fail(400, "unsupported-protocol", `pairing protocol ${String(request.protocol)} is not supported`);
88553
+ if (readCaCertificate(request.hubCaCertPem, this.now()) === null) return fail(400, "bad-ca", "the offered CA is not a valid self-signed CA certificate");
88554
+ if (!/^[0-9a-f]{64}$/.test(request.commitment)) return fail(400, "bad-offer", "the commitment is malformed");
88555
+ if (!isHttpUrl(request.hubUrl)) return fail(400, "bad-offer", "hubUrl is not an http(s) URL");
88556
+ const caFingerprint = caFingerprintCompact(request.hubCaCertPem);
88557
+ const refused = this.checkOfferSignature(request, caFingerprint);
88558
+ if (refused !== null) return refused;
88559
+ const rationed = this.rationOffer(caFingerprint);
88560
+ if (rationed !== null) return rationed;
88561
+ for (const existing of [...this.sessions.values()]) if (existing.hub.caFingerprint === caFingerprint) this.end(existing, "failed");
88562
+ if (this.sessions.size >= 4 && !this.makeRoom()) return fail(409, "too-many-pending", "too many pairings are pending on this agent");
88563
+ const keys = await generateMeshKeyPair();
88564
+ const csrPem = await createMeshCsrPem(keys, this.deps.nodeId);
88565
+ const keyPem = await exportMeshPrivateKeyPem(keys.privateKey);
88566
+ const publicKeySpki = Buffer.from(await node_crypto.webcrypto.subtle.exportKey("spki", keys.publicKey)).toString("base64");
88567
+ if (this.sessions.size >= 4 && !this.makeRoom()) return fail(409, "too-many-pending", "too many pairings are pending on this agent");
88568
+ const session = {
88569
+ pairingId: newPairingId(),
88570
+ hub: {
88571
+ nodeId: request.hubNodeId,
88572
+ caCertPem: request.hubCaCertPem,
88573
+ caFingerprint,
88574
+ commitment: request.commitment,
88575
+ url: request.hubUrl,
88576
+ meshAddress: request.hubMeshAddress
88577
+ },
88578
+ keyPem,
88579
+ csrPem,
88580
+ publicKeySpki,
88581
+ agentNonce: randomHex(32),
88582
+ pin: randomPin(),
88583
+ expiresAt: this.now() + PAIRING_TTL_MS,
88584
+ hubNonce: null,
88585
+ code: null,
88586
+ localConfirmed: false,
88587
+ attempts: 0,
88588
+ touched: false,
88589
+ createdAt: this.now()
88590
+ };
88591
+ this.sessions.set(session.pairingId, session);
88592
+ this.deps.onStateChange?.();
88593
+ this.announceLocally(session);
88594
+ this.deps.log?.("pairing requested by a hub", {
88595
+ hubUrl: session.hub.url,
88596
+ caFingerprint: session.hub.caFingerprint
88597
+ });
88598
+ return {
88599
+ ok: true,
88600
+ answer: {
88601
+ pairingId: session.pairingId,
88602
+ csrPem,
88603
+ agentNonce: session.agentNonce,
88604
+ expiresAt: session.expiresAt
88605
+ }
88606
+ };
88607
+ }
88608
+ /** The hub reveals its nonce; the agent checks it against the commitment and derives the code. */
88609
+ reveal(body) {
88610
+ const session = this.pending(body);
88611
+ if ("ok" in session) return session;
88612
+ const hubNonce = field(body, "hubNonce");
88613
+ if (!isText(hubNonce, 256)) return fail(400, "bad-reveal", "hubNonce is missing");
88614
+ if (session.hubNonce !== null) return fail(409, "already-revealed", "the nonce was already revealed");
88615
+ if (!commitmentsEqual(computePairingCommitment(session.hub.caCertPem, hubNonce), session.hub.commitment)) {
88616
+ this.end(session, "failed");
88617
+ return fail(400, "commitment-mismatch", "the revealed nonce does not match the hub commitment");
88618
+ }
88619
+ session.touched = true;
88620
+ session.hubNonce = hubNonce;
88621
+ session.code = computePairingSas({
88622
+ caCertPem: session.hub.caCertPem,
88623
+ agentPublicKeySpki: session.publicKeySpki,
88624
+ hubNonce,
88625
+ agentNonce: session.agentNonce
88626
+ });
88627
+ this.announceLocally(session);
88628
+ return { ok: true };
88629
+ }
88630
+ /** The hub polls with the pairing id: did the agent's operator confirm, did it end? */
88631
+ poll(body) {
88632
+ this.expireIfDue();
88633
+ const pairingId = field(body, "pairingId");
88634
+ const session = typeof pairingId === "string" ? this.sessions.get(pairingId) : void 0;
88635
+ if (session !== void 0) {
88636
+ session.touched = true;
88637
+ return {
88638
+ ok: true,
88639
+ poll: {
88640
+ state: "pending",
88641
+ localConfirmed: session.localConfirmed,
88642
+ last: null
88643
+ }
88644
+ };
88645
+ }
88646
+ const known = typeof pairingId === "string" ? this.outcomeOf(pairingId) : null;
88647
+ if (typeof pairingId === "string" && known !== null) return {
88648
+ ok: true,
88649
+ poll: {
88650
+ state: "idle",
88651
+ localConfirmed: false,
88652
+ last: {
88653
+ pairingId,
88654
+ outcome: known
88655
+ }
88656
+ }
88657
+ };
88658
+ return fail(404, "no-pairing", "no such pairing is pending");
88659
+ }
88660
+ /**
88661
+ * The operator typed the code the HUB shows. From the agent's own machine that is enough; from
88662
+ * anywhere else the one-time PIN of THIS pairing (read on the agent's console) must come with
88663
+ * it. A wrong code counts against the pairing (the third cancels it); a wrong PIN counts against
88664
+ * the agent (five lock confirming for five minutes).
88665
+ */
88666
+ confirm(body, ctx) {
88667
+ const found = this.localTarget(body, ctx);
88668
+ if ("ok" in found) return found;
88669
+ const session = found;
88670
+ if (session.code === null) return fail(409, "not-ready", "the hub has not revealed its nonce yet");
88671
+ const code = field(body, "code");
88672
+ if (!constantTimeEqual(session.code, typeof code === "string" ? code.trim() : code)) {
88673
+ session.attempts += 1;
88674
+ if (session.attempts >= 3) {
88675
+ this.end(session, "code-mismatch");
88676
+ return fail(403, "code-mismatch", "wrong code three times — the pairing was cancelled");
88677
+ }
88678
+ return fail(403, "code-mismatch", "that is not the code this agent computed");
88679
+ }
88680
+ session.localConfirmed = true;
88681
+ return { ok: true };
88682
+ }
88683
+ reject(body, ctx) {
88684
+ const found = this.localTarget(body, ctx);
88685
+ if ("ok" in found) return found;
88686
+ this.end(found, "rejected");
88687
+ return { ok: true };
88688
+ }
88689
+ /** The hub withdraws its own pairing (authorized by the pairing id it was given). */
88690
+ cancel(body) {
88691
+ const session = this.pending(body);
88692
+ if ("ok" in session) return session;
88693
+ this.end(session, "rejected");
88694
+ return { ok: true };
88695
+ }
88696
+ async complete(body) {
88697
+ const session = this.pending(body);
88698
+ if ("ok" in session) return session;
88699
+ if (session.hubNonce === null || session.code === null) return fail(409, "not-ready", "the hub has not revealed its nonce yet");
88700
+ if (!session.localConfirmed) return fail(409, "awaiting-local-confirmation", "the operator has not confirmed on this agent");
88701
+ const leafPem = field(body, "leafPem");
88702
+ const caCertPem = field(body, "caCertPem");
88703
+ const proof = field(body, "proof");
88704
+ if (!isText(leafPem, 16384) || !isText(caCertPem, 16384) || !isText(proof, 16384)) return fail(400, "bad-complete", "the completion is malformed");
88705
+ const refusal = this.checkCompletion(session, leafPem, caCertPem, proof);
88706
+ if (refusal !== null) {
88707
+ this.end(session, "failed");
88708
+ return refusal;
88709
+ }
88710
+ try {
88711
+ await this.deps.install({
88712
+ meshTls: {
88713
+ certPem: leafPem,
88714
+ keyPem: session.keyPem,
88715
+ caCertPem: session.hub.caCertPem
88716
+ },
88717
+ hubAddress: hubHostFromMeshAddress(session.hub.meshAddress),
88718
+ hubUrl: session.hub.url
88719
+ });
88720
+ } catch (err) {
88721
+ this.end(session, "failed");
88722
+ return fail(500, "install-failed", err instanceof Error ? err.message : String(err));
88723
+ }
88724
+ this.end(session, "completed");
88725
+ for (const other of [...this.sessions.values()]) this.end(other, "failed", false);
88726
+ this.deps.log?.("paired with the hub", { hubUrl: session.hub.url });
88727
+ return { ok: true };
88728
+ }
88729
+ /** The agent forgot its hub (local action): the pairing slate is clean. */
88730
+ noteUnpaired() {
88731
+ for (const s of [...this.sessions.values()]) this.end(s, "failed", false);
88732
+ this.deps.onStateChange?.();
88733
+ }
88734
+ /** Remaining lock for a source, or `null`. Loopback never calls this. */
88735
+ sourceLock(source, at) {
88736
+ const entry = this.pinBySource.get(source);
88737
+ return entry !== void 0 && at < entry.lockedUntil ? entry.lockedUntil - at : null;
88738
+ }
88739
+ /** A wrong PIN or reset code from `source`: five in five minutes lock THAT source, doubling. */
88740
+ noteWrongSecret(source, at) {
88741
+ if (this.pinBySource.size >= MAX_TRACKED_SOURCES && !this.pinBySource.has(source)) this.pinBySource.clear();
88742
+ const prev = this.pinBySource.get(source) ?? {
88743
+ failures: [],
88744
+ strikes: 0,
88745
+ lockedUntil: 0
88746
+ };
88747
+ const failures = [...prev.failures.filter((t) => at - t < 5 * 6e4), at];
88748
+ if (failures.length >= PIN_FAILURE_LIMIT) {
88749
+ const lockedUntil = at + Math.min(PIN_LOCK_MAX_MS, PIN_LOCK_MS * 2 ** prev.strikes);
88750
+ this.deps.log?.("confirming is locked for a source: too many wrong PINs", { source });
88751
+ this.pinBySource.set(source, {
88752
+ failures: [],
88753
+ strikes: prev.strikes + 1,
88754
+ lockedUntil
88755
+ });
88756
+ } else this.pinBySource.set(source, {
88757
+ ...prev,
88758
+ failures
88759
+ });
88760
+ this.globalPinFailures = [...this.globalPinFailures.filter((t) => at - t < PAIRING_TTL_MS), at];
88761
+ if (this.globalPinFailures.length >= PIN_GLOBAL_CEILING) {
88762
+ this.globalPinFailures = [];
88763
+ this.deps.log?.("too many wrong PINs overall: every pending pairing is cancelled", {});
88764
+ for (const session of [...this.sessions.values()]) this.end(session, "failed", false);
88765
+ }
88766
+ }
88767
+ /**
88768
+ * The operator clears the lockouts, the budget and every pending offer. From this machine
88769
+ * (genuine loopback) or with the LOCAL RESET CODE printed at boot; the code is renewed on use.
88770
+ */
88771
+ reset(body, ctx) {
88772
+ const at = this.now();
88773
+ if (!ctx.local) {
88774
+ const key = `reset:${ctx.source ?? "unknown"}`;
88775
+ const locked = this.sourceLock(key, at);
88776
+ if (locked !== null) return fail(429, "reset-locked", "too many wrong codes", locked);
88777
+ const code = field(body, "code");
88778
+ if (typeof code !== "string" || !constantTimeEqual(this.resetCode, code.trim())) {
88779
+ this.noteWrongSecret(key, at);
88780
+ return fail(401, "reset-code-required", "the reset code printed on the agent is required");
88781
+ }
88782
+ }
88783
+ for (const session of [...this.sessions.values()]) this.end(session, "failed", false);
88784
+ this.abandoned = [];
88785
+ this.lockedUntil = 0;
88786
+ this.perCa.clear();
88787
+ this.pinBySource.clear();
88788
+ this.globalPinFailures = [];
88789
+ this.resetCode = randomPin();
88790
+ this.deps.onResetCode?.(this.resetCode);
88791
+ this.deps.log?.("pairing was reset by the local operator", {});
88792
+ return { ok: true };
88793
+ }
88794
+ /** Verify the CA-key signature of an offer and spend its commitment. */
88795
+ checkOfferSignature(request, caFingerprint) {
88796
+ const claims = verifyPairingOffer(request.hubCaCertPem, request.signature);
88797
+ const at = this.now();
88798
+ 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) {
88799
+ this.deps.log?.("an offer without a valid CA signature was refused", {});
88800
+ return fail(403, "bad-offer-signature", "the offer is not signed by the CA it presents");
88801
+ }
88802
+ for (const [commitment, until] of this.seenCommitments) if (until <= at) this.seenCommitments.delete(commitment);
88803
+ if (this.seenCommitments.has(request.commitment)) return fail(409, "offer-replayed", "this offer was already used");
88804
+ this.seenCommitments.set(request.commitment, at + PAIRING_OFFER_MAX_AGE_MS + 6e4);
88805
+ return null;
88806
+ }
88807
+ /**
88808
+ * All slots taken: a signed offer makes room by dropping the oldest pairing whose hub never showed
88809
+ * activity (no reveal, no poll) and that no operator confirmed. `false` when there is none.
88810
+ */
88811
+ makeRoom() {
88812
+ const oldest = [...this.sessions.values()].filter((s) => !s.touched && !s.localConfirmed).toSorted((a, b) => a.createdAt - b.createdAt)[0];
88813
+ if (oldest === void 0) return false;
88814
+ this.deps.log?.("a pending pairing made room for a newer signed offer", { caFingerprint: oldest.hub.caFingerprint });
88815
+ this.end(oldest, "failed", false);
88816
+ return true;
88817
+ }
88818
+ /** Offers that never reached local confirmation are rationed - globally and per hub CA. */
88819
+ rationOffer(caFingerprint) {
88820
+ const at = this.now();
88821
+ if (at < this.lockedUntil) {
88822
+ this.deps.log?.("offer refused: the budget of abandoned pairings is spent", { retryAfterMs: this.lockedUntil - at });
88823
+ return fail(429, "offer-budget", "too many pairings were abandoned recently - try again later", this.lockedUntil - at);
88824
+ }
88825
+ const ca = this.perCa.get(caFingerprint);
88826
+ if (ca !== void 0 && at < ca.until) {
88827
+ this.deps.log?.("offer refused: this hub abandoned a pairing a moment ago", {
88828
+ caFingerprint,
88829
+ retryAfterMs: ca.until - at
88830
+ });
88831
+ return fail(429, "offer-backoff", "this hub abandoned a pairing a moment ago - wait before offering again", ca.until - at);
88832
+ }
88833
+ return null;
88834
+ }
88835
+ /** A pairing ended without the local operator confirming it: it counts against the budget. */
88836
+ noteAbandoned(session) {
88837
+ const at = this.now();
88838
+ this.abandoned = [...this.abandoned.filter((t) => at - t < BUDGET_WINDOW_MS), at];
88839
+ if (this.abandoned.length >= BUDGET_OFFERS) {
88840
+ const over = this.abandoned.length - BUDGET_OFFERS;
88841
+ const lock = Math.min(LOCKOUT_MAX_MS, LOCKOUT_BASE_MS * 2 ** over);
88842
+ this.lockedUntil = at + lock;
88843
+ this.deps.log?.("pairing offers are locked: too many were abandoned", {
88844
+ abandoned: this.abandoned.length,
88845
+ lockMs: lock
88846
+ });
88847
+ }
88848
+ const count = (this.perCa.get(session.hub.caFingerprint)?.count ?? 0) + 1;
88849
+ const backoff = count < 2 ? 0 : Math.min(CA_BACKOFF_MAX_MS, CA_BACKOFF_BASE_MS * 2 ** (count - 2));
88850
+ this.perCa.set(session.hub.caFingerprint, {
88851
+ count,
88852
+ until: at + backoff
88853
+ });
88854
+ }
88855
+ /** The pairing this local request means, else the refusal. */
88856
+ localTarget(body, ctx) {
88857
+ this.expireIfDue();
88858
+ if (this.sessions.size === 0) return fail(404, "no-pairing", "no pairing is pending");
88859
+ const pin = field(body, "pin");
88860
+ if (!ctx.local) {
88861
+ const at = this.now();
88862
+ const source = ctx.source ?? "unknown";
88863
+ const locked = this.sourceLock(source, at);
88864
+ if (locked !== null) return fail(429, "pin-locked", "too many wrong PINs - try again later", locked);
88865
+ const match = typeof pin === "string" ? [...this.sessions.values()].find((s) => constantTimeEqual(s.pin, pin.trim())) : void 0;
88866
+ if (match === void 0) {
88867
+ this.noteWrongSecret(source, at);
88868
+ return fail(401, "pin-required", "the PIN shown on this agent's own machine (console, file or local window) is required");
88869
+ }
88870
+ return match;
88871
+ }
88872
+ if (typeof pin === "string" && pin.trim().length > 0) {
88873
+ const match = [...this.sessions.values()].find((s) => constantTimeEqual(s.pin, pin.trim()));
88874
+ if (match !== void 0) return match;
88875
+ }
88876
+ const only = [...this.sessions.values()];
88877
+ if (only.length === 1 && only[0] !== void 0) return only[0];
88878
+ return fail(409, "ambiguous", "several pairings are pending - give the PIN of the one you mean");
88879
+ }
88880
+ announceLocally(session) {
88881
+ this.deps.onLocalSecret?.({
88882
+ pairingId: session.pairingId,
88883
+ pin: session.pin,
88884
+ code: session.code,
88885
+ hub: {
88886
+ nodeId: session.hub.nodeId,
88887
+ url: session.hub.url,
88888
+ caFingerprint: session.hub.caFingerprint
88889
+ },
88890
+ expiresAt: session.expiresAt
88891
+ });
88892
+ }
88893
+ checkCompletion(session, leafPem, caCertPem, proof) {
88894
+ if (caFingerprintOf(caCertPem) !== session.hub.caFingerprint) return fail(400, "ca-mismatch", "the completion carries a CA other than the one offered");
88895
+ const claims = verifyPairingProof(session.hub.caCertPem, proof);
88896
+ if (claims === null) return fail(400, "bad-proof", "the pairing proof does not verify");
88897
+ 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");
88898
+ const at = this.now();
88899
+ 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");
88900
+ try {
88901
+ if (claims.leafFingerprint !== leafFingerprintCompact(leafPem)) return fail(400, "bad-leaf", "the certificate is not the one the proof names");
88902
+ const leaf = new node_crypto.X509Certificate(leafPem);
88903
+ const ca = new node_crypto.X509Certificate(session.hub.caCertPem);
88904
+ if (!leaf.verify(ca.publicKey)) return fail(400, "bad-leaf", "the certificate is not signed by the offered CA");
88905
+ if (subjectCn(leaf.subject.replaceAll("\n", ",")) !== this.deps.nodeId) return fail(400, "bad-leaf", `the certificate is not issued for "${this.deps.nodeId}"`);
88906
+ if (!leaf.checkPrivateKey((0, node_crypto.createPrivateKey)(session.keyPem))) return fail(400, "bad-leaf", "the certificate does not match this agent's private key");
88907
+ if (Date.parse(leaf.validTo) <= this.now()) return fail(400, "bad-leaf", "the certificate is already expired");
88908
+ } catch {
88909
+ return fail(400, "bad-leaf", "the certificate could not be read");
88910
+ }
88911
+ return null;
88912
+ }
88913
+ /** The live session `body.pairingId` names, else the refusal. */
88914
+ pending(body) {
88915
+ this.expireIfDue();
88916
+ const pairingId = field(body, "pairingId");
88917
+ if (this.sessions.size === 0) return fail(404, "no-pairing", "no pairing is pending");
88918
+ const session = typeof pairingId === "string" ? this.sessions.get(pairingId) : void 0;
88919
+ if (session === void 0) return fail(403, "wrong-pairing", "that is not a pending pairing");
88920
+ return session;
88921
+ }
88922
+ expireIfDue() {
88923
+ for (const s of [...this.sessions.values()]) if (s.expiresAt <= this.now()) this.end(s, "expired");
88924
+ }
88925
+ outcomeOf(pairingId) {
88926
+ const entry = this.outcomes.get(pairingId);
88927
+ if (entry === void 0) return null;
88928
+ if (this.now() - entry.at > OUTCOME_TTL_MS) {
88929
+ this.outcomes.delete(pairingId);
88930
+ return null;
88931
+ }
88932
+ return entry.outcome;
88933
+ }
88934
+ rememberOutcome(pairingId, outcome) {
88935
+ this.outcomes.set(pairingId, {
88936
+ outcome,
88937
+ at: this.now()
88938
+ });
88939
+ while (this.outcomes.size > OUTCOME_LIMIT) {
88940
+ const oldest = this.outcomes.keys().next().value;
88941
+ if (oldest === void 0) break;
88942
+ this.outcomes.delete(oldest);
88943
+ }
88944
+ }
88945
+ end(session, outcome, count = true) {
88946
+ if (!this.sessions.delete(session.pairingId)) return;
88947
+ this.rememberOutcome(session.pairingId, outcome);
88948
+ if (outcome === "completed") this.perCa.delete(session.hub.caFingerprint);
88949
+ else if (count && !session.localConfirmed) this.noteAbandoned(session);
88950
+ this.deps.onLocalSecretCleared?.(session.pairingId);
88951
+ this.deps.onStateChange?.();
88952
+ if (outcome !== "completed") this.deps.log?.("pairing cancelled", {
88953
+ outcome,
88954
+ hubUrl: session.hub.url
88955
+ });
88956
+ }
88957
+ };
88958
+ function field(body, key) {
88959
+ return body !== null && typeof body === "object" ? Reflect.get(body, key) : void 0;
88960
+ }
88961
+ function caFingerprintOf(pem) {
88962
+ try {
88963
+ return caFingerprintCompact(pem);
88964
+ } catch {
88965
+ return null;
88966
+ }
88967
+ }
88968
+ function parseOffer(body) {
88969
+ if (body === null || typeof body !== "object") return null;
88970
+ const protocol = Reflect.get(body, "protocol");
88971
+ const hubNodeId = Reflect.get(body, "hubNodeId");
88972
+ const hubCaCertPem = Reflect.get(body, "hubCaCertPem");
88973
+ const commitment = Reflect.get(body, "commitment");
88974
+ const hubUrl = Reflect.get(body, "hubUrl");
88975
+ const hubMeshAddress = Reflect.get(body, "hubMeshAddress");
88976
+ const signature = Reflect.get(body, "signature");
88977
+ if (typeof protocol !== "number") return null;
88978
+ if (!isText(hubNodeId, 128) || !isText(hubCaCertPem, 16384) || !isText(commitment, 128)) return null;
88979
+ if (!isText(hubUrl) || !isText(hubMeshAddress, 255) || !isText(signature, 4096)) return null;
88980
+ return {
88981
+ protocol,
88982
+ hubNodeId,
88983
+ hubCaCertPem,
88984
+ commitment,
88985
+ hubUrl,
88986
+ hubMeshAddress,
88987
+ signature
88988
+ };
88989
+ }
88990
+ //#endregion
88991
+ //#region src/kernel/moleculer/mesh-mtls/pairing/hub-pairing.ts
88992
+ /**
88993
+ * The hub's side of pairing (D857): starts a pairing with an agent it found
88994
+ * (mDNS) or was given an address for, computes the same six digits the agent
88995
+ * shows, and signs the agent's certificate ONLY after the operator confirmed
88996
+ * them on the hub AND the agent's own operator confirmed them there.
88997
+ *
88998
+ * Either confirmation may come first. The hub learns the agent's by polling the
88999
+ * agent's pairing status (`advance`), never by being told: nothing the agent
89000
+ * says is trusted beyond "my operator confirmed", and even that only matters
89001
+ * together with the hub's own confirmation.
89002
+ *
89003
+ * Transport-agnostic: `AgentPairingTransport` is the HTTP client in production
89004
+ * and an in-memory shim (or a man in the middle) in specs.
89005
+ */
89006
+ /** A refusal the agent sent, kept with its machine-readable code. */
89007
+ var PairingHttpError = class extends Error {
89008
+ status;
89009
+ code;
89010
+ constructor(status, code, message) {
89011
+ super(message);
89012
+ this.status = status;
89013
+ this.code = code;
89014
+ this.name = "PairingHttpError";
89015
+ }
89016
+ };
89017
+ /** Pairings kept (active and just ended) so the page can show how one ended. */
89018
+ var MAX_SESSIONS = 16;
89019
+ var PROOF_TTL_MS = Math.min(2 * 6e4, PAIRING_PROOF_MAX_LIFETIME_MS);
89020
+ var TERMINAL = new Set([
89021
+ "paired",
89022
+ "rejected",
89023
+ "expired",
89024
+ "failed"
89025
+ ]);
89026
+ var HubPairing = class {
89027
+ deps;
89028
+ sessions = /* @__PURE__ */ new Map();
89029
+ now;
89030
+ constructor(deps) {
89031
+ this.deps = deps;
89032
+ this.now = deps.now ?? Date.now;
89033
+ }
89034
+ /** Offer the hub's CA to the agent at `endpoint` and derive the code. Throws an `Error` with the reason. */
89035
+ async begin(endpoint) {
89036
+ const ca = this.deps.ca();
89037
+ if (ca === null) throw new Error("the mesh CA is not ready — try again in a moment");
89038
+ const hello = await this.deps.transport.hello(endpoint);
89039
+ if (hello.protocol !== 1) throw new Error(`the agent speaks pairing protocol ${String(hello.protocol)}, not 1`);
89040
+ if (hello.state === "paired") throw new Error(`"${hello.nodeId}" is already paired — unpair it first`);
89041
+ this.assertNodeIdUsable(hello.nodeId);
89042
+ 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`);
89043
+ const hubNonce = randomHex(32);
89044
+ const hubUrl = await this.deps.hubUrl();
89045
+ const hubMeshAddress = await this.deps.hubMeshAddress();
89046
+ const commitment = computePairingCommitment(ca.certPem, hubNonce);
89047
+ const answer = await this.deps.transport.offer(endpoint, {
89048
+ protocol: 1,
89049
+ hubNodeId: this.deps.hubNodeId,
89050
+ hubCaCertPem: ca.certPem,
89051
+ commitment,
89052
+ hubUrl,
89053
+ hubMeshAddress,
89054
+ signature: signPairingOffer(ca.keyPem, {
89055
+ use: PAIRING_OFFER_USE,
89056
+ agentNodeId: hello.nodeId,
89057
+ hubNodeId: this.deps.hubNodeId,
89058
+ caFingerprint: caFingerprintCompact(ca.certPem),
89059
+ commitment,
89060
+ hubUrl,
89061
+ hubMeshAddress,
89062
+ iat: this.now()
89063
+ })
89064
+ });
89065
+ try {
89066
+ const csr = await describeCsr(answer.csrPem);
89067
+ if (csr.commonName !== hello.nodeId) throw new Error(`the agent's CSR is for "${csr.commonName}", not "${hello.nodeId}"`);
89068
+ await this.deps.transport.reveal(endpoint, {
89069
+ pairingId: answer.pairingId,
89070
+ hubNonce
89071
+ });
89072
+ const code = computePairingSas({
89073
+ caCertPem: ca.certPem,
89074
+ agentPublicKeySpki: csr.publicKeySpki,
89075
+ hubNonce,
89076
+ agentNonce: answer.agentNonce
89077
+ });
89078
+ const session = {
89079
+ pairingId: answer.pairingId,
89080
+ nodeId: hello.nodeId,
89081
+ endpoint,
89082
+ code,
89083
+ csrPem: answer.csrPem,
89084
+ hubNonce,
89085
+ agentNonce: answer.agentNonce,
89086
+ caCertPem: ca.certPem,
89087
+ hubUrl,
89088
+ hubMeshAddress,
89089
+ expiresAt: Math.min(answer.expiresAt, this.now() + PAIRING_TTL_MS),
89090
+ state: "awaiting-confirmation",
89091
+ hubConfirmed: false,
89092
+ agentConfirmed: false,
89093
+ attempts: 0,
89094
+ failure: null,
89095
+ issued: false
89096
+ };
89097
+ this.remember(session);
89098
+ this.deps.log?.("pairing started", {
89099
+ nodeId: session.nodeId,
89100
+ pairingId: session.pairingId
89101
+ });
89102
+ return this.view(session);
89103
+ } catch (err) {
89104
+ await this.deps.transport.reject(endpoint, answer.pairingId).catch(() => void 0);
89105
+ throw err;
89106
+ }
89107
+ }
89108
+ /** The operator typed the code the AGENT shows. A wrong one counts; the third cancels the pairing. */
89109
+ async confirm(pairingId, code) {
89110
+ const session = this.require(pairingId);
89111
+ this.expireIfDue(session);
89112
+ if (TERMINAL.has(session.state)) return this.view(session);
89113
+ if (!constantTimeEqual(session.code, typeof code === "string" ? code.trim() : code)) {
89114
+ session.attempts += 1;
89115
+ if (session.attempts >= 3) await this.cancel(session, "rejected", "wrong code three times");
89116
+ 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");
89117
+ }
89118
+ session.hubConfirmed = true;
89119
+ return this.advance(pairingId);
89120
+ }
89121
+ async reject(pairingId) {
89122
+ const session = this.require(pairingId);
89123
+ if (!TERMINAL.has(session.state)) await this.cancel(session, "rejected", "rejected on the hub");
89124
+ return this.view(session);
89125
+ }
89126
+ get(pairingId) {
89127
+ const session = this.sessions.get(pairingId);
89128
+ return session === void 0 ? void 0 : this.view(session);
89129
+ }
89130
+ list() {
89131
+ return [...this.sessions.values()].map((s) => this.view(s));
89132
+ }
89133
+ /** Active pairings, for the poller. */
89134
+ activeIds() {
89135
+ return [...this.sessions.values()].filter((s) => !TERMINAL.has(s.state)).map((s) => s.pairingId);
89136
+ }
89137
+ /**
89138
+ * Move a pairing on: notice an expiry, a cancellation or the agent's
89139
+ * confirmation, and complete when both sides confirmed. Never throws.
89140
+ */
89141
+ async advance(pairingId) {
89142
+ const session = this.require(pairingId);
89143
+ if (TERMINAL.has(session.state) || session.state === "completing") return this.view(session);
89144
+ if (this.expireIfDue(session)) {
89145
+ await this.deps.transport.reject(session.endpoint, session.pairingId).catch(() => void 0);
89146
+ return this.view(session);
89147
+ }
89148
+ try {
89149
+ const poll = await this.deps.transport.poll(session.endpoint, session.pairingId);
89150
+ if (poll.state === "pending") session.agentConfirmed = poll.localConfirmed;
89151
+ else if (poll.last?.pairingId === session.pairingId && poll.last.outcome === "completed") {
89152
+ this.finish(session, "paired", null);
89153
+ return this.view(session);
89154
+ } else {
89155
+ const why = poll.last?.pairingId === session.pairingId ? poll.last.outcome : "gone";
89156
+ this.finish(session, "failed", `the agent ended the pairing (${why})`);
89157
+ return this.view(session);
89158
+ }
89159
+ } catch (err) {
89160
+ if (err instanceof PairingHttpError && err.code === "no-pairing") {
89161
+ this.finish(session, "failed", "the agent no longer has this pairing");
89162
+ return this.view(session);
89163
+ }
89164
+ this.deps.log?.("pairing status poll failed", {
89165
+ nodeId: session.nodeId,
89166
+ error: err instanceof Error ? err.message : String(err)
89167
+ });
89168
+ return this.view(session);
89169
+ }
89170
+ if (session.hubConfirmed && session.agentConfirmed) await this.complete(session);
89171
+ return this.view(session);
89172
+ }
89173
+ async complete(session) {
89174
+ const ca = this.deps.ca();
89175
+ if (ca === null || caFingerprintCompact(ca.certPem) !== caFingerprintCompact(session.caCertPem)) {
89176
+ await this.cancel(session, "failed", "the mesh CA changed during the pairing");
89177
+ return;
89178
+ }
89179
+ session.state = "completing";
89180
+ let issuedPem = null;
89181
+ try {
89182
+ const leaf = await issueMeshLeafFromCsr(ca, session.csrPem, { expectedNodeId: session.nodeId });
89183
+ issuedPem = leaf.certPem;
89184
+ await this.deps.recordIssued(session.nodeId, leaf.certPem);
89185
+ session.issued = true;
89186
+ const proof = signPairingProof(ca.keyPem, {
89187
+ v: 1,
89188
+ use: PAIRING_PROOF_USE,
89189
+ pairingId: session.pairingId,
89190
+ nodeId: session.nodeId,
89191
+ hubNonce: session.hubNonce,
89192
+ agentNonce: session.agentNonce,
89193
+ leafFingerprint: leafFingerprintCompact(leaf.certPem),
89194
+ caFingerprint: caFingerprintCompact(ca.certPem),
89195
+ hubMeshAddress: session.hubMeshAddress,
89196
+ hubUrl: session.hubUrl,
89197
+ iat: this.now(),
89198
+ exp: this.now() + PROOF_TTL_MS
89199
+ });
89200
+ await this.deps.transport.complete(session.endpoint, {
89201
+ pairingId: session.pairingId,
89202
+ leafPem: leaf.certPem,
89203
+ caCertPem: ca.certPem,
89204
+ proof
89205
+ });
89206
+ await this.installed(session, leaf.certPem);
89207
+ } catch (err) {
89208
+ const reason = err instanceof Error ? err.message : String(err);
89209
+ if (err instanceof PairingHttpError && err.code === "awaiting-local-confirmation") {
89210
+ session.state = "awaiting-confirmation";
89211
+ session.agentConfirmed = false;
89212
+ return;
89213
+ }
89214
+ let outcome = issuedPem === null ? "absent" : await this.installOutcome(session);
89215
+ if (outcome !== "installed" && issuedPem !== null && this.deps.isLeafPresented?.(session.nodeId, issuedPem) === true) outcome = "installed";
89216
+ if (outcome === "installed" && issuedPem !== null) {
89217
+ await this.installed(session, issuedPem);
89218
+ return;
89219
+ }
89220
+ if (outcome === "absent" && issuedPem !== null) await this.deps.revokeLeaf?.(session.nodeId, issuedPem, "pairing did not complete").catch(() => void 0);
89221
+ 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;
89222
+ await this.cancel(session, "failed", note);
89223
+ }
89224
+ }
89225
+ /** The new leaf is installed: older leaves of the node go, the new one stays. */
89226
+ async installed(session, leafPem) {
89227
+ await this.deps.revokeNode(session.nodeId, "superseded by pairing", leafPem).catch((err) => this.deps.log?.("older leaves of the node could not be revoked", {
89228
+ nodeId: session.nodeId,
89229
+ error: err instanceof Error ? err.message : String(err)
89230
+ }));
89231
+ this.finish(session, "paired", null);
89232
+ this.deps.log?.("agent paired", {
89233
+ nodeId: session.nodeId,
89234
+ pairingId: session.pairingId
89235
+ });
89236
+ }
89237
+ /** After a failed completion call: did the agent install the certificate after all? */
89238
+ async installOutcome(session) {
89239
+ try {
89240
+ const poll = await this.deps.transport.poll(session.endpoint, session.pairingId);
89241
+ if (poll.last?.pairingId === session.pairingId && poll.last.outcome === "completed") return "installed";
89242
+ return "absent";
89243
+ } catch (err) {
89244
+ return "unknown";
89245
+ }
89246
+ }
89247
+ assertNodeIdUsable(nodeId) {
89248
+ if (!isValidAgentNodeId(nodeId)) throw new Error(`"${nodeId}" cannot be an agent node id (letters, digits, . _ - ; not "hub"; at most 63 characters)`);
89249
+ if (this.deps.isNodeConnected(nodeId)) throw new Error(`a node called "${nodeId}" is already connected to this hub — unpair it first`);
89250
+ const lower = nodeId.toLowerCase();
89251
+ const clash = (this.deps.knownNodeIds?.() ?? []).find((known) => known !== nodeId && known.toLowerCase() === lower);
89252
+ if (clash !== void 0) throw new Error(`"${nodeId}" differs only in case from the existing node "${clash}"`);
89253
+ }
89254
+ expireIfDue(session) {
89255
+ if (TERMINAL.has(session.state) || session.expiresAt > this.now()) return false;
89256
+ this.finish(session, "expired", "the pairing was not confirmed on both sides in time");
89257
+ return true;
89258
+ }
89259
+ finish(session, state, failure) {
89260
+ session.state = state;
89261
+ session.failure = failure;
89262
+ }
89263
+ async cancel(session, state, reason) {
89264
+ this.finish(session, state, reason);
89265
+ await this.deps.transport.reject(session.endpoint, session.pairingId).catch(() => void 0);
89266
+ this.deps.log?.("pairing cancelled", {
89267
+ nodeId: session.nodeId,
89268
+ state,
89269
+ reason
89270
+ });
89271
+ }
89272
+ require(pairingId) {
89273
+ const session = this.sessions.get(pairingId);
89274
+ if (session === void 0) throw new Error("no such pairing");
89275
+ return session;
89276
+ }
89277
+ remember(session) {
89278
+ this.sessions.set(session.pairingId, session);
89279
+ if (this.sessions.size <= MAX_SESSIONS) return;
89280
+ for (const [id, s] of this.sessions) {
89281
+ if (this.sessions.size <= MAX_SESSIONS) break;
89282
+ if (TERMINAL.has(s.state)) this.sessions.delete(id);
89283
+ }
89284
+ }
89285
+ view(s) {
89286
+ return {
89287
+ pairingId: s.pairingId,
89288
+ nodeId: s.nodeId,
89289
+ endpoint: s.endpoint,
89290
+ code: s.code,
89291
+ state: s.state,
89292
+ hubConfirmed: s.hubConfirmed,
89293
+ agentConfirmed: s.agentConfirmed,
89294
+ expiresAt: s.expiresAt,
89295
+ failure: s.failure
89296
+ };
89297
+ }
89298
+ };
89299
+ //#endregion
87968
89300
  //#region src/builtins/system-config/cluster-secret-keeper.ts
87969
89301
  /**
87970
89302
  * Keeps the hub's cluster-secret gate in step with System → Cluster (D797,
@@ -88112,22 +89444,22 @@ function errText(err) {
88112
89444
  * agent-ui unlock tickets (D821).
88113
89445
  */
88114
89446
  var mintJoinTokenInputSchema = zod.z.object({
88115
- /** Optional pinned node id for the CSR CN. */
88116
- nodeId: zod.z.string().min(1).optional(),
89447
+ /** The node id the token is for - mandatory: the CSR CN must be exactly this. */
89448
+ nodeId: zod.z.string().min(1),
88117
89449
  /** Override hub HTTPS base (default: local-network publicEndpoint). */
88118
89450
  hubUrl: zod.z.string().url().optional(),
88119
89451
  /** Override `host:6443` TLS mesh address carried in the token. */
88120
89452
  hubMeshAddress: zod.z.string().min(3).optional(),
88121
89453
  /** Seconds until expiry; capped at 1 h. */
88122
89454
  ttlSec: zod.z.number().int().positive().max(3600).optional()
88123
- }).optional();
89455
+ });
88124
89456
  var mintJoinTokenOutputSchema = zod.z.object({
88125
89457
  token: zod.z.string().min(1),
88126
89458
  exp: zod.z.number().int(),
88127
89459
  hubUrl: zod.z.string(),
88128
89460
  hubMeshAddress: zod.z.string(),
88129
89461
  caFingerprint: zod.z.string(),
88130
- nodeId: zod.z.string().optional()
89462
+ nodeId: zod.z.string()
88131
89463
  });
88132
89464
  var mintAgentUiUnlockInputSchema = zod.z.object({
88133
89465
  nodeId: zod.z.string().min(1),
@@ -88280,7 +89612,7 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
88280
89612
  setProcessMeshEnforcer(null);
88281
89613
  setProcessMeshCertificateRegistry(null);
88282
89614
  setProcessMeshTlsListenerState(null);
88283
- require_agent_https_names.setProcessAgentHttpsNames(null);
89615
+ require_agent_https.setProcessAgentHttpsNames(null);
88284
89616
  this.stoppedMesh = true;
88285
89617
  if (this.meshReapTimer !== null) clearInterval(this.meshReapTimer);
88286
89618
  this.meshReapTimer = null;
@@ -88333,12 +89665,12 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
88333
89665
  async loadAgentHttpsNames(store) {
88334
89666
  try {
88335
89667
  await store.declareCollection.mutate({
88336
- collection: require_agent_https_names.AGENT_HTTPS_NAMES_COLLECTION,
88337
- columns: require_agent_https_names.AGENT_HTTPS_NAMES_COLUMNS
89668
+ collection: require_agent_https.AGENT_HTTPS_NAMES_COLLECTION,
89669
+ columns: require_agent_https.AGENT_HTTPS_NAMES_COLUMNS
88338
89670
  });
88339
- const names = new require_agent_https_names.AgentHttpsNames(store);
89671
+ const names = new require_agent_https.AgentHttpsNames(store);
88340
89672
  await names.load();
88341
- require_agent_https_names.setProcessAgentHttpsNames(names);
89673
+ require_agent_https.setProcessAgentHttpsNames(names);
88342
89674
  } catch (err) {
88343
89675
  this.ctx.logger.warn("Agent HTTPS names registry unavailable (agent HTTPS issuance will be refused until it loads)", { meta: { error: err instanceof Error ? err.message : String(err) } });
88344
89676
  }
@@ -88388,7 +89720,7 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
88388
89720
  ca,
88389
89721
  hubUrl,
88390
89722
  hubMeshAddress,
88391
- ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
89723
+ nodeId: input.nodeId,
88392
89724
  ...input.ttlSec !== void 0 ? { ttlSec: input.ttlSec } : {}
88393
89725
  });
88394
89726
  return {
@@ -88397,7 +89729,7 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
88397
89729
  hubUrl,
88398
89730
  hubMeshAddress,
88399
89731
  caFingerprint: caFingerprintCompact(ca.certPem),
88400
- ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {}
89732
+ nodeId: input.nodeId
88401
89733
  };
88402
89734
  }
88403
89735
  /** D821 — short-lived ticket for the agent's `:4444` unlock session. */
@@ -88409,6 +89741,8 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
88409
89741
  if (input.dashboardBaseUrl !== void 0) {
88410
89742
  const origins = await this.readAgentDashboardOrigins();
88411
89743
  if (!isAllowedAgentDashboardReturnTo(input.dashboardBaseUrl, origins)) throw new Error("dashboardBaseUrl is not an allowed agent dashboard origin (:4444 or agentDashboardOrigins)");
89744
+ const known = require_agent_https.getProcessAgentHttpsNames()?.namesOf(input.nodeId) ?? null;
89745
+ 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`);
88412
89746
  }
88413
89747
  const ticket = mintAgentUiUnlockTicket({
88414
89748
  secretHash: require_constants$8.hashClusterSecret(entry.value),
@@ -88687,13 +90021,13 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
88687
90021
  }
88688
90022
  };
88689
90023
  function parseMintJoinTokenInput(input) {
88690
- if (input === null || input === void 0 || typeof input !== "object") return {};
88691
- const out = {};
90024
+ if (input === null || input === void 0 || typeof input !== "object") throw new Error("system-config: mintJoinToken requires { nodeId }");
88692
90025
  const nodeId = Reflect.get(input, "nodeId");
90026
+ if (typeof nodeId !== "string" || nodeId.length === 0) throw new Error("system-config: mintJoinToken requires { nodeId } - the node the token is for");
90027
+ const out = { nodeId };
88693
90028
  const hubUrl = Reflect.get(input, "hubUrl");
88694
90029
  const hubMeshAddress = Reflect.get(input, "hubMeshAddress");
88695
90030
  const ttlSec = Reflect.get(input, "ttlSec");
88696
- if (typeof nodeId === "string") out.nodeId = nodeId;
88697
90031
  if (typeof hubUrl === "string") out.hubUrl = hubUrl;
88698
90032
  if (typeof hubMeshAddress === "string") out.hubMeshAddress = hubMeshAddress;
88699
90033
  if (typeof ttlSec === "number") out.ttlSec = ttlSec;
@@ -88723,8 +90057,12 @@ function readJwtExp(token) {
88723
90057
  exports.AGENT_DASHBOARD_ORIGINS_FIELD = AGENT_DASHBOARD_ORIGINS_FIELD;
88724
90058
  exports.AGENT_UI_UNLOCK_DOMAIN = AGENT_UI_UNLOCK_DOMAIN;
88725
90059
  exports.AGENT_UI_UNLOCK_TTL_MS = AGENT_UI_UNLOCK_TTL_MS;
90060
+ exports.AGENT_UI_UNLOCK_TYP = AGENT_UI_UNLOCK_TYP;
90061
+ exports.AGENT_UI_UNLOCK_USE = AGENT_UI_UNLOCK_USE;
88726
90062
  exports.AGENT_UI_UNLOCK_VERSION = AGENT_UI_UNLOCK_VERSION;
90063
+ exports.AgentPairing = AgentPairing;
88727
90064
  exports.Errors = Errors;
90065
+ exports.HubPairing = HubPairing;
88728
90066
  exports.JoinTokenJtiStore = JoinTokenJtiStore;
88729
90067
  exports.MESH_AUTH_MODE_FIELD = MESH_AUTH_MODE_FIELD;
88730
90068
  exports.MESH_NODE_CERTIFICATES_COLLECTION = MESH_NODE_CERTIFICATES_COLLECTION;
@@ -88733,6 +90071,14 @@ exports.MESH_TLS_PORT_HELLO_FIELD = MESH_TLS_PORT_HELLO_FIELD;
88733
90071
  exports.MeshCaKeeper = MeshCaKeeper;
88734
90072
  exports.MeshCertificateRegistry = MeshCertificateRegistry;
88735
90073
  exports.MeshCrl = MeshCrl;
90074
+ exports.PAIRING_CODE_DIGITS = PAIRING_CODE_DIGITS;
90075
+ exports.PAIRING_MAX_CODE_ATTEMPTS = PAIRING_MAX_CODE_ATTEMPTS;
90076
+ exports.PAIRING_MAX_SESSIONS = PAIRING_MAX_SESSIONS;
90077
+ exports.PAIRING_PATHS = PAIRING_PATHS;
90078
+ exports.PAIRING_PIN_DIGITS = PAIRING_PIN_DIGITS;
90079
+ exports.PAIRING_PROTOCOL = PAIRING_PROTOCOL;
90080
+ exports.PAIRING_TTL_MS = PAIRING_TTL_MS;
90081
+ exports.PairingHttpError = PairingHttpError;
88736
90082
  exports.RENEWAL_ABANDONED = RENEWAL_ABANDONED;
88737
90083
  exports.SYSTEM_CONFIG_CLUSTER_SECTION_ID = SYSTEM_CONFIG_CLUSTER_SECTION_ID;
88738
90084
  exports.ServiceBroker = ServiceBroker;
@@ -88743,8 +90089,11 @@ exports.Transporters = Transporters;
88743
90089
  exports.UnlockTicketJtiStore = UnlockTicketJtiStore;
88744
90090
  exports.caFingerprint = caFingerprint;
88745
90091
  exports.caFingerprintCompact = caFingerprintCompact;
90092
+ exports.computePairingCommitment = computePairingCommitment;
90093
+ exports.computePairingSas = computePairingSas;
88746
90094
  exports.createCidrMatcher = createCidrMatcher;
88747
90095
  exports.createMeshCsrPem = createMeshCsrPem;
90096
+ exports.describeCsr = describeCsr;
88748
90097
  exports.describeMeshLeaf = describeMeshLeaf;
88749
90098
  exports.dropProcessPeer = dropProcessPeer;
88750
90099
  exports.emitProcessNodeRegistered = emitProcessNodeRegistered;
@@ -88759,12 +90108,17 @@ exports.getProcessMeshCertificateRegistry = getProcessMeshCertificateRegistry;
88759
90108
  exports.getProcessMeshTlsListenerState = getProcessMeshTlsListenerState;
88760
90109
  exports.getProcessNodeMeshTransport = getProcessNodeMeshTransport;
88761
90110
  exports.getProcessPeerLeaf = getProcessPeerLeaf;
90111
+ exports.hubHostFromMeshAddress = hubHostFromMeshAddress;
88762
90112
  exports.isAllowedAgentDashboardReturnTo = isAllowedAgentDashboardReturnTo;
88763
90113
  exports.isMeshAuthMode = isMeshAuthMode;
90114
+ exports.isProcessVerifiedMeshPeer = isProcessVerifiedMeshPeer;
90115
+ exports.isValidAgentNodeId = isValidAgentNodeId;
88764
90116
  exports.issueMeshLeafForNode = issueMeshLeafForNode;
88765
90117
  exports.issueMeshLeafFromCsr = issueMeshLeafFromCsr;
90118
+ exports.keyFingerprintShort = keyFingerprintShort;
88766
90119
  exports.loadMeshCa = loadMeshCa;
88767
90120
  exports.mintAgentUiUnlockTicket = mintAgentUiUnlockTicket;
90121
+ exports.mintAgentUiUnlockTicketEs256 = mintAgentUiUnlockTicketEs256;
88768
90122
  exports.mintMeshJoinToken = mintMeshJoinToken;
88769
90123
  exports.normalizeIp = normalizeIp;
88770
90124
  exports.onProcessNodeRegistered = onProcessNodeRegistered;
@@ -88786,6 +90140,9 @@ exports.setProcessMeshEnforcer = setProcessMeshEnforcer;
88786
90140
  exports.setProcessMeshPeerTransportLookup = setProcessMeshPeerTransportLookup;
88787
90141
  exports.setProcessMeshTlsEnabler = setProcessMeshTlsEnabler;
88788
90142
  exports.setProcessMeshTlsListenerState = setProcessMeshTlsListenerState;
90143
+ exports.signPairingProof = signPairingProof;
88789
90144
  exports.systemConfigMeshActions = systemConfigMeshActions;
88790
90145
  exports.verifyAgentUiUnlockTicket = verifyAgentUiUnlockTicket;
90146
+ exports.verifyAgentUiUnlockTicketEs256 = verifyAgentUiUnlockTicketEs256;
88791
90147
  exports.verifyMeshJoinToken = verifyMeshJoinToken;
90148
+ exports.verifyPairingProof = verifyPairingProof;