@camstack/system 1.2.431 → 1.2.433

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 (97) hide show
  1. package/dist/{tls-C5IYg3V5.js → agent-https-CHZTxK-q.js} +123 -759
  2. package/dist/{tls-CpcGkeJV.mjs → agent-https-DtRU9A7I.mjs} +89 -611
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/autotrack/index.js +1 -1
  10. package/dist/builtins/autotrack/index.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  13. package/dist/builtins/calendars/calendars.addon.js +1 -1
  14. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  15. package/dist/builtins/camera-grid/index.js +1 -1
  16. package/dist/builtins/camera-grid/index.mjs +1 -1
  17. package/dist/builtins/composer/composer.addon.js +2 -2
  18. package/dist/builtins/composer/composer.addon.mjs +2 -2
  19. package/dist/builtins/console-logging/index.js +1 -1
  20. package/dist/builtins/console-logging/index.mjs +1 -1
  21. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  22. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  23. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  24. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  25. package/dist/builtins/hub-forwarder/index.js +1 -1
  26. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  27. package/dist/builtins/keychain/index.js +3 -3
  28. package/dist/builtins/keychain/index.mjs +3 -3
  29. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  30. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  31. package/dist/builtins/local-auth/local-auth.addon.js +2 -2
  32. package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
  33. package/dist/builtins/local-network/agent-mdns.d.ts +51 -0
  34. package/dist/builtins/local-network/local-network.addon.js +20 -45
  35. package/dist/builtins/local-network/local-network.addon.mjs +2 -28
  36. package/dist/builtins/loki-logging/index.js +1 -1
  37. package/dist/builtins/loki-logging/index.mjs +1 -1
  38. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  39. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  40. package/dist/builtins/platform-probe/index.js +1 -1
  41. package/dist/builtins/platform-probe/index.mjs +1 -1
  42. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  43. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  44. package/dist/builtins/snapshot/index.js +1 -1
  45. package/dist/builtins/snapshot/index.mjs +1 -1
  46. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  47. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  48. package/dist/builtins/sqlite-storage/index.js +1 -1
  49. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  50. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +3 -3
  51. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +3 -3
  52. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  53. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  54. package/dist/builtins/system-config/system-config.addon.js +1370 -28
  55. package/dist/builtins/system-config/system-config.addon.mjs +1344 -25
  56. package/dist/builtins/winston-logging/index.js +1 -1
  57. package/dist/builtins/winston-logging/index.mjs +1 -1
  58. package/dist/{composition-sources-Cw_TXEq9.mjs → composition-sources-8-cpAt1c.mjs} +1 -1
  59. package/dist/{composition-sources-o5MPSlk6.js → composition-sources-BgJWQMjr.js} +1 -1
  60. package/dist/{constants-BcNaVkvI.js → constants-Ah3T_KAW.js} +2 -2
  61. package/dist/{constants-CttE77VF.mjs → constants-Cg9ve13y.mjs} +2 -2
  62. package/dist/{device-ca-B-BTxeCj.mjs → device-ca-DUrcTW5h.mjs} +1 -1
  63. package/dist/{device-ca-CKiqh2Iu.js → device-ca-NNP4Jz0P.js} +1 -1
  64. package/dist/{dist-DxJdci75.js → dist-C6Yvfhpq.js} +360 -1
  65. package/dist/{dist-CXG6muCR.mjs → dist-DJnlnL7P.mjs} +349 -2
  66. package/dist/file-host-worker.js +1 -1
  67. package/dist/file-host-worker.mjs +1 -1
  68. package/dist/hub-mdns-ciao-8_Ed1Za5.mjs +648 -0
  69. package/dist/hub-mdns-ciao-B50jAU5C.js +821 -0
  70. package/dist/index.d.ts +3 -0
  71. package/dist/index.js +176 -67
  72. package/dist/index.mjs +96 -13
  73. package/dist/kernel/index.d.ts +5 -4
  74. package/dist/kernel/moleculer/agent-mesh-authenticator.d.ts +12 -1
  75. package/dist/kernel/moleculer/agent-ui-unlock.d.ts +40 -0
  76. package/dist/kernel/moleculer/mesh-authenticator.d.ts +6 -0
  77. package/dist/kernel/moleculer/mesh-mtls/constants.d.ts +1 -1
  78. package/dist/kernel/moleculer/mesh-mtls/index.d.ts +3 -2
  79. package/dist/kernel/moleculer/mesh-mtls/mesh-ca.d.ts +4 -0
  80. package/dist/kernel/moleculer/mesh-mtls/mesh-certificate-registry.d.ts +2 -2
  81. package/dist/kernel/moleculer/mesh-mtls/mesh-join-token.d.ts +3 -2
  82. package/dist/kernel/moleculer/mesh-mtls/mesh-peer-transport.d.ts +7 -0
  83. package/dist/kernel/moleculer/mesh-mtls/pairing/agent-pairing.d.ts +204 -0
  84. package/dist/kernel/moleculer/mesh-mtls/pairing/hub-pairing.d.ts +115 -0
  85. package/dist/kernel/moleculer/mesh-mtls/pairing/index.d.ts +3 -0
  86. package/dist/kernel/moleculer/mesh-mtls/pairing/pairing-protocol.d.ts +107 -0
  87. package/dist/kernel/moleculer/mesh-mtls/tls-tcp-transporter.d.ts +18 -0
  88. package/dist/{restore-archive-D_wBL1OO.mjs → restore-archive-C3_rzOCx.mjs} +1 -1
  89. package/dist/{restore-archive-CQJNoqRK.js → restore-archive-DqPaRjjY.js} +1 -1
  90. package/dist/{retired-settings-keys-EmDe11cP.mjs → retired-settings-keys-CAWHWDwy.mjs} +1 -1
  91. package/dist/{retired-settings-keys-g1oifXxR.js → retired-settings-keys-DJBQkZok.js} +1 -1
  92. package/dist/tls/agent-https-names.d.ts +2 -0
  93. package/dist/{wal-maintenance-vhPZLYuA.mjs → wal-maintenance-Bc38yI3r.mjs} +0 -0
  94. package/dist/{wal-maintenance-BW1LIHi0.js → wal-maintenance-CPLvZbzA.js} +0 -0
  95. package/package.json +1 -1
  96. package/dist/agent-https-names-CpUSuP4w.js +0 -120
  97. 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-DxJdci75.js");
6
+ const require_dist = require("../../dist-C6Yvfhpq.js");
7
7
  const require_safe_buffer$1 = require("../../safe-buffer-D3aoa-7X.js");
8
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-BcNaVkvI.js");
9
+ const require_agent_https = require("../../agent-https-CHZTxK-q.js");
10
+ const require_constants$8 = require("../../constants-Ah3T_KAW.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,9 +315,93 @@ 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"];
319
+ return {
320
+ v: 1,
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;
286
401
  return {
287
402
  v: 1,
288
403
  nodeId,
404
+ iat,
289
405
  exp,
290
406
  jti
291
407
  };
@@ -304,6 +420,14 @@ var MESH_SIGNING_ALGORITHM = {
304
420
  name: "ECDSA",
305
421
  hash: "SHA-256"
306
422
  };
423
+ /** A node id: letters, digits, `.`, `_`, `-`; at most 63 characters; starts with a letter or digit. */
424
+ var NODE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,62}$/i;
425
+ /** Ids no agent may take: the hub's own, whatever the case (D857). */
426
+ var RESERVED_NODE_IDS = new Set(["hub"]);
427
+ /** True for an id an agent may carry: the pattern, and not a reserved name. */
428
+ function isValidAgentNodeId(nodeId) {
429
+ return NODE_ID_PATTERN.test(nodeId) && !RESERVED_NODE_IDS.has(nodeId.toLowerCase());
430
+ }
307
431
  function freshSerial() {
308
432
  return `00${(0, node_crypto.randomBytes)(16).toString("hex")}`;
309
433
  }
@@ -367,6 +491,7 @@ async function issueMeshLeafFromCsr(ca, csrPem, opts = {}) {
367
491
  if (!await csr.verify(node_crypto.webcrypto)) throw new Error("CSR signature is invalid");
368
492
  const cn = subjectCn(csr.subject);
369
493
  if (cn === null || cn.length === 0) throw new Error("CSR subject has no CN");
494
+ if (opts.allowReserved !== true && !isValidAgentNodeId(cn)) throw new Error(`CSR CN '${cn}' is not an id an agent may carry`);
370
495
  if (opts.expectedNodeId !== void 0 && opts.expectedNodeId !== cn) throw new Error(`CSR CN '${cn}' does not match the token nodeId '${opts.expectedNodeId}'`);
371
496
  const now = opts.now ?? /* @__PURE__ */ new Date();
372
497
  const validDays = opts.validDays ?? 30;
@@ -403,7 +528,8 @@ async function issueMeshLeafForNode(ca, nodeId, opts = {}) {
403
528
  ...await issueMeshLeafFromCsr(ca, await createMeshCsrPem(keys, nodeId), {
404
529
  expectedNodeId: nodeId,
405
530
  now: opts.now,
406
- validDays: opts.validDays
531
+ validDays: opts.validDays,
532
+ allowReserved: true
407
533
  }),
408
534
  keyPem: await exportKeyPem(keys.privateKey)
409
535
  };
@@ -693,6 +819,7 @@ var RETENTION_MS = 90 * 864e5;
693
819
  var RENEWAL_ABANDONED = "renewal-abandoned";
694
820
  var VIAS = [
695
821
  "enroll",
822
+ "pairing",
696
823
  "renewal",
697
824
  "boot",
698
825
  "observed"
@@ -866,9 +993,10 @@ var MeshCertificateRegistry = class {
866
993
  * valid just because the hub once set it aside). Returns the serials put on the CRL.
867
994
  * A leaf revoked this way is never reinstated by `recordObserved`.
868
995
  */
869
- async revokeAllForNode(nodeId, reason, crl) {
996
+ async revokeAllForNode(nodeId, reason, crl, keepFingerprint) {
870
997
  const serials = [];
871
998
  for (const record of this.list(nodeId)) {
999
+ if (keepFingerprint !== void 0 && record.fingerprint === keepFingerprint) continue;
872
1000
  if (record.revokedAt !== null && record.revokedReason !== "renewal-abandoned") continue;
873
1001
  const revoked = {
874
1002
  ...record,
@@ -1318,6 +1446,10 @@ function getProcessNodeMeshTransport(nodeId) {
1318
1446
  function getProcessPeerLeaf(nodeId) {
1319
1447
  return getProcessMeshPeerTransportLookup()?.peerLeaf?.(nodeId);
1320
1448
  }
1449
+ /** True when the hub's transporter holds a certificate-verified mesh peer called `nodeId` (D857). */
1450
+ function isProcessVerifiedMeshPeer(nodeId) {
1451
+ return getProcessMeshPeerTransportLookup()?.isVerifiedPeer?.(nodeId) === true;
1452
+ }
1321
1453
  /** Disconnect `nodeId` (closes its mutual-TLS sockets). `0` when none were open. */
1322
1454
  function dropProcessPeer(nodeId) {
1323
1455
  return getProcessMeshPeerTransportLookup()?.dropPeer?.(nodeId) ?? 0;
@@ -2026,7 +2158,8 @@ var SignJWT = class extends SignJWT_base {
2026
2158
  /**
2027
2159
  * Mesh join tokens — single-use JWS ES256 signed with the hub mesh CA key.
2028
2160
  *
2029
- * Payload: `{ v, hubUrl, hubMeshAddress, caFingerprint, nodeId?, jti, exp }`.
2161
+ * Payload: `{ v, hubUrl, hubMeshAddress, caFingerprint, nodeId, jti, exp }`. The node id is MANDATORY
2162
+ * (D857): a token that does not name the node it is for would let its holder pick any id, `hub` included.
2030
2163
  * `exp` ≤ now + 1 h. `jti` is recorded until expiry so a token cannot be reused.
2031
2164
  */
2032
2165
  /** In-memory + optional persist of spent `jti` until `exp`. */
@@ -2057,6 +2190,7 @@ function processJoinTokenJtiStore() {
2057
2190
  return processJtiStore;
2058
2191
  }
2059
2192
  async function mintMeshJoinToken(input) {
2193
+ if (!isValidAgentNodeId(input.nodeId)) throw new Error(`a join token needs the node id it is for; "${input.nodeId}" is not one an agent may carry`);
2060
2194
  const now = input.now ?? /* @__PURE__ */ new Date();
2061
2195
  const nowSec = Math.floor(now.getTime() / 1e3);
2062
2196
  const exp = nowSec + Math.min(Math.max(1, input.ttlSec ?? 3600), require_constants$8.MESH_JOIN_TOKEN_MAX_TTL_SEC);
@@ -2066,7 +2200,7 @@ async function mintMeshJoinToken(input) {
2066
2200
  hubUrl: input.hubUrl,
2067
2201
  hubMeshAddress: input.hubMeshAddress,
2068
2202
  caFingerprint: caFingerprintCompact(input.ca.certPem),
2069
- ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
2203
+ nodeId: input.nodeId,
2070
2204
  jti,
2071
2205
  exp
2072
2206
  }).setProtectedHeader({
@@ -2094,7 +2228,7 @@ async function verifyMeshJoinToken(input) {
2094
2228
  reason: `token signature invalid: ${msg}`
2095
2229
  };
2096
2230
  }
2097
- const claims = parseClaims(payload);
2231
+ const claims = parseClaims$1(payload);
2098
2232
  if (claims === null) return {
2099
2233
  ok: false,
2100
2234
  reason: "token claims malformed"
@@ -2130,7 +2264,7 @@ async function verifyMeshJoinToken(input) {
2130
2264
  function normalizeFp(fp) {
2131
2265
  return fp.replace(/:/g, "").toLowerCase();
2132
2266
  }
2133
- function parseClaims(payload) {
2267
+ function parseClaims$1(payload) {
2134
2268
  if (payload["v"] !== 1) return null;
2135
2269
  const hubUrl = payload["hubUrl"];
2136
2270
  const hubMeshAddress = payload["hubMeshAddress"];
@@ -2143,12 +2277,13 @@ function parseClaims(payload) {
2143
2277
  if (typeof jti !== "string" || jti.length === 0) return null;
2144
2278
  if (exp === null) return null;
2145
2279
  const nodeId = payload["nodeId"];
2280
+ if (typeof nodeId !== "string" || !isValidAgentNodeId(nodeId)) return null;
2146
2281
  return {
2147
2282
  v: 1,
2148
2283
  hubUrl,
2149
2284
  hubMeshAddress,
2150
2285
  caFingerprint: normalizeFp(caFingerprint),
2151
- ...typeof nodeId === "string" && nodeId.length > 0 ? { nodeId } : {},
2286
+ nodeId,
2152
2287
  jti,
2153
2288
  exp
2154
2289
  };
@@ -87724,6 +87859,26 @@ var TlsTcpTransporter = class extends TcpTransporterBase {
87724
87859
  return this.tlsPeers.has(nodeId) ? "tls" : "clear";
87725
87860
  }
87726
87861
  /**
87862
+ * The certificate identity of `nodeId` (D857): its mutual-TLS handshake chained to the
87863
+ * mesh CA this transporter was built with, its leaf CN is `nodeId`, and the leaf is not
87864
+ * revoked now. This is the ONLY proof of "who is calling" the mesh gates accept:
87865
+ * every inbound packet on a TLS socket is dropped unless its `sender` equals the leaf CN
87866
+ * (see `onIncomingMessage`), so a node id in a delivered packet that is verified here was
87867
+ * claimed by the holder of that node's private key.
87868
+ *
87869
+ * `undefined` for a node that reached us over clear text, never completed a handshake,
87870
+ * or whose serial is on the CRL.
87871
+ */
87872
+ verifiedPeer(nodeId) {
87873
+ const serial = this.tlsPeers.get(nodeId);
87874
+ if (serial === void 0) return void 0;
87875
+ if (serial.length > 0 && this.crl.isRevoked(serial)) return void 0;
87876
+ return {
87877
+ nodeId,
87878
+ serial
87879
+ };
87880
+ }
87881
+ /**
87727
87882
  * The node disconnected: its next connection decides its transport again —
87728
87883
  * unless it already re-handshook (the disconnect can be noticed after the
87729
87884
  * restarted node is back), in which case its live TLS socket keeps it `tls`.
@@ -87965,6 +88120,1168 @@ function isTlsSocket(socket) {
87965
88120
  return typeof socket.getPeerCertificate === "function" && typeof socket.encrypted === "boolean" && socket.encrypted === true;
87966
88121
  }
87967
88122
  //#endregion
88123
+ //#region src/kernel/moleculer/mesh-mtls/pairing/pairing-protocol.ts
88124
+ /**
88125
+ * Agent pairing — the cryptographic core (D857).
88126
+ *
88127
+ * Pairing makes an agent hold a certificate from the hub's mesh CA, with no
88128
+ * shared secret anywhere. The hub starts it (LAN discovery or a typed
88129
+ * address); the operator confirms the SAME six digits on the hub and in the
88130
+ * agent's own page. Everything the human compares is derived here.
88131
+ *
88132
+ * hub → agent offer { hub CA cert, commit = H(caFp ‖ hubNonce), hubUrl, hubMeshAddress }
88133
+ * agent → hub answer { pairingId, CSR (mesh key made on the agent), agentNonce }
88134
+ * hub → agent reveal { hubNonce } (agent checks it against the commit)
88135
+ * both SAS = 6 digits of H(caFp ‖ agentPublicKey ‖ hubNonce ‖ agentNonce)
88136
+ * operator reads the code on one screen and types it on the other, on both sides
88137
+ * hub → agent complete { leaf, proof } (only after both confirmations)
88138
+ *
88139
+ * Why a commitment: a six-digit code is 20 bits. An attacker between the two
88140
+ * who could still choose a value AFTER seeing the others could grind a CA key
88141
+ * whose code equals the real one in a million tries. The hub therefore commits
88142
+ * to its nonce before it sees the agent's answer and reveals it only after,
88143
+ * so nothing the attacker controls is chosen after everything the code is
88144
+ * derived from is fixed. A substituted CA changes the code with probability
88145
+ * 999 999 / 1 000 000 per attempt, and a wrong code ends the pairing after
88146
+ * three tries.
88147
+ *
88148
+ * `proof` is an ES256 JWS by the mesh CA key over the transcript (pairing id,
88149
+ * node id, both nonces, the leaf fingerprint, the CA fingerprint). The agent
88150
+ * verifies it with the CA certificate it was OFFERED and the operator
88151
+ * confirmed — never with one that arrives in the same message.
88152
+ */
88153
+ var PAIRING_PROTOCOL = 1;
88154
+ var PAIRING_DOMAIN = "camstack-pairing/v1";
88155
+ var PAIRING_PROOF_TYP = "camstack-pairing-complete+jwt";
88156
+ var PAIRING_PROOF_USE = "pairing-complete";
88157
+ var PAIRING_OFFER_TYP = "camstack-pairing-offer+jwt";
88158
+ var PAIRING_OFFER_USE = "pairing-offer";
88159
+ /** An offer signature older than this (or from further ahead than 60 s) is refused. */
88160
+ var PAIRING_OFFER_MAX_AGE_MS = 2 * 6e4;
88161
+ /** A pairing that is not confirmed on both sides within this window is cancelled on both. */
88162
+ var PAIRING_TTL_MS = 5 * 6e4;
88163
+ /** Wrong codes typed before the pairing is cancelled. */
88164
+ var PAIRING_MAX_CODE_ATTEMPTS = 3;
88165
+ var PAIRING_CODE_DIGITS = 6;
88166
+ /** Pending pairings an agent holds at once, one per hub CA (offer squatting, D857). */
88167
+ var PAIRING_MAX_SESSIONS = 4;
88168
+ /** The local PIN: single use, shown only on the agent's own machine. */
88169
+ var PAIRING_PIN_DIGITS = 8;
88170
+ /** A pairing proof is valid this long from `iat`, never longer. */
88171
+ var PAIRING_PROOF_MAX_LIFETIME_MS = 5 * 6e4;
88172
+ /** Agent endpoints, all under the agent API (`:4444`). */
88173
+ var PAIRING_PATHS = {
88174
+ hello: "/api/agent/pairing/hello",
88175
+ offer: "/api/agent/pairing/offer",
88176
+ reveal: "/api/agent/pairing/reveal",
88177
+ /** Public: who asks to pair - never a code, a pairing id or a PIN. */
88178
+ status: "/api/agent/pairing/status",
88179
+ /** The hub's poll, authorized by the pairing id: did the agent's operator confirm? */
88180
+ poll: "/api/agent/pairing/poll",
88181
+ complete: "/api/agent/pairing/complete",
88182
+ /** The agent's operator: the hub's code + the local PIN (or genuine loopback). */
88183
+ confirm: "/api/agent/pairing/confirm",
88184
+ reject: "/api/agent/pairing/reject",
88185
+ /** The hub withdraws its pairing, authorized by the pairing id. */
88186
+ cancel: "/api/agent/pairing/cancel",
88187
+ /** The agent's operator clears lockouts and pending offers (loopback, or the local reset code). */
88188
+ reset: "/api/agent/pairing/reset",
88189
+ forget: "/api/agent/pairing/forget"
88190
+ };
88191
+ /** A fresh single-use local PIN: digits only, uniformly random. */
88192
+ function randomPin() {
88193
+ return String((0, node_crypto.randomInt)(0, 10 ** 8)).padStart(8, "0");
88194
+ }
88195
+ function randomHex(bytes) {
88196
+ return (0, node_crypto.randomBytes)(bytes).toString("hex");
88197
+ }
88198
+ function newPairingId() {
88199
+ return (0, node_crypto.randomBytes)(16).toString("base64url");
88200
+ }
88201
+ /** The hub's commitment to its nonce, bound to the CA certificate it offers. */
88202
+ function computePairingCommitment(caCertPem, hubNonce) {
88203
+ return (0, node_crypto.createHash)("sha256").update(`${PAIRING_DOMAIN}|commit|${caFingerprintCompact(caCertPem)}|${hubNonce}`, "utf8").digest("hex");
88204
+ }
88205
+ function commitmentsEqual(expected, presented) {
88206
+ return constantTimeEqual(expected, presented);
88207
+ }
88208
+ /** The six digits both screens show. */
88209
+ function computePairingSas(input) {
88210
+ const value = (0, node_crypto.createHash)("sha256").update([
88211
+ PAIRING_DOMAIN,
88212
+ "sas",
88213
+ caFingerprintCompact(input.caCertPem),
88214
+ input.agentPublicKeySpki,
88215
+ input.hubNonce,
88216
+ input.agentNonce
88217
+ ].join("|"), "utf8").digest().readUInt32BE(0) % 10 ** 6;
88218
+ return String(value).padStart(6, "0");
88219
+ }
88220
+ function constantTimeEqual(expected, presented) {
88221
+ if (typeof presented !== "string") return false;
88222
+ const a = Buffer.from(expected, "utf8");
88223
+ const b = Buffer.from(presented, "utf8");
88224
+ return a.length === b.length && (0, node_crypto.timingSafeEqual)(a, b);
88225
+ }
88226
+ async function describeCsr(csrPem) {
88227
+ const csr = new require_x509_es.Pkcs10CertificateRequest(csrPem);
88228
+ if (!await csr.verify(node_crypto.webcrypto)) throw new Error("the agent CSR signature is invalid");
88229
+ const cn = subjectCn(csr.subject);
88230
+ if (cn === null || cn.length === 0) throw new Error("the agent CSR has no CN");
88231
+ return {
88232
+ commonName: cn,
88233
+ publicKeySpki: Buffer.from(csr.publicKey.rawData).toString("base64")
88234
+ };
88235
+ }
88236
+ function b64urlJson(value) {
88237
+ return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
88238
+ }
88239
+ function leafFingerprintCompact(certPem) {
88240
+ return (0, node_crypto.createHash)("sha256").update(new node_crypto.X509Certificate(certPem).raw).digest("hex");
88241
+ }
88242
+ function signPairingProof(caKeyPem, claims) {
88243
+ const signingInput = `${b64urlJson({
88244
+ alg: "ES256",
88245
+ typ: PAIRING_PROOF_TYP
88246
+ })}.${b64urlJson(claims)}`;
88247
+ return `${signingInput}.${(0, node_crypto.sign)("sha256", Buffer.from(signingInput, "utf8"), {
88248
+ key: (0, node_crypto.createPrivateKey)(caKeyPem),
88249
+ dsaEncoding: "ieee-p1363"
88250
+ }).toString("base64url")}`;
88251
+ }
88252
+ /**
88253
+ * The claims of `proof` when it was signed by `caCertPem`'s key, else `null`.
88254
+ * Shape, expiry and the binding to a session are the caller's to check.
88255
+ */
88256
+ function verifyPairingProof(caCertPem, proof) {
88257
+ const parts = proof.split(".");
88258
+ if (parts.length !== 3) return null;
88259
+ const [headerB64, payloadB64, signatureB64] = parts;
88260
+ if (headerB64 === void 0 || payloadB64 === void 0 || signatureB64 === void 0) return null;
88261
+ let header;
88262
+ let payload;
88263
+ try {
88264
+ header = JSON.parse(Buffer.from(headerB64, "base64url").toString("utf8"));
88265
+ payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
88266
+ } catch {
88267
+ return null;
88268
+ }
88269
+ if (header === null || typeof header !== "object") return null;
88270
+ if (Reflect.get(header, "alg") !== "ES256" || Reflect.get(header, "typ") !== "camstack-pairing-complete+jwt") return null;
88271
+ let valid = false;
88272
+ try {
88273
+ valid = (0, node_crypto.verify)("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), {
88274
+ key: new node_crypto.X509Certificate(caCertPem).publicKey,
88275
+ dsaEncoding: "ieee-p1363"
88276
+ }, Buffer.from(signatureB64, "base64url"));
88277
+ } catch {
88278
+ return null;
88279
+ }
88280
+ return valid ? parseClaims(payload) : null;
88281
+ }
88282
+ function parseClaims(value) {
88283
+ if (value === null || typeof value !== "object") return null;
88284
+ const str = (key) => {
88285
+ const v = Reflect.get(value, key);
88286
+ return typeof v === "string" && v.length > 0 ? v : null;
88287
+ };
88288
+ const pairingId = str("pairingId");
88289
+ const nodeId = str("nodeId");
88290
+ const hubNonce = str("hubNonce");
88291
+ const agentNonce = str("agentNonce");
88292
+ const leafFingerprint = str("leafFingerprint");
88293
+ const caFingerprint = str("caFingerprint");
88294
+ const hubMeshAddress = str("hubMeshAddress");
88295
+ const hubUrl = str("hubUrl");
88296
+ const exp = Reflect.get(value, "exp");
88297
+ const iat = Reflect.get(value, "iat");
88298
+ if (Reflect.get(value, "v") !== 1) return null;
88299
+ if (Reflect.get(value, "use") !== "pairing-complete") return null;
88300
+ 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;
88301
+ return {
88302
+ v: 1,
88303
+ use: PAIRING_PROOF_USE,
88304
+ pairingId,
88305
+ nodeId,
88306
+ hubNonce,
88307
+ agentNonce,
88308
+ leafFingerprint,
88309
+ caFingerprint,
88310
+ hubMeshAddress,
88311
+ hubUrl,
88312
+ iat,
88313
+ exp
88314
+ };
88315
+ }
88316
+ /** A short public fingerprint of a public key, for the mDNS TXT record. */
88317
+ function keyFingerprintShort(publicKeySpkiB64) {
88318
+ return (0, node_crypto.createHash)("sha256").update(Buffer.from(publicKeySpkiB64, "base64")).digest("hex").slice(0, 16);
88319
+ }
88320
+ function signPairingOffer(caKeyPem, claims) {
88321
+ const signingInput = `${b64urlJson({
88322
+ alg: "ES256",
88323
+ typ: PAIRING_OFFER_TYP
88324
+ })}.${b64urlJson(claims)}`;
88325
+ return `${signingInput}.${(0, node_crypto.sign)("sha256", Buffer.from(signingInput, "utf8"), {
88326
+ key: (0, node_crypto.createPrivateKey)(caKeyPem),
88327
+ dsaEncoding: "ieee-p1363"
88328
+ }).toString("base64url")}`;
88329
+ }
88330
+ /** The claims of `token` when `caCertPem`'s key signed it as a pairing offer, else `null`. */
88331
+ function verifyPairingOffer(caCertPem, token) {
88332
+ const parts = token.split(".");
88333
+ if (parts.length !== 3) return null;
88334
+ const [headerB64, payloadB64, signatureB64] = parts;
88335
+ if (headerB64 === void 0 || payloadB64 === void 0 || signatureB64 === void 0) return null;
88336
+ let header;
88337
+ let payload;
88338
+ try {
88339
+ header = JSON.parse(Buffer.from(headerB64, "base64url").toString("utf8"));
88340
+ payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
88341
+ } catch {
88342
+ return null;
88343
+ }
88344
+ if (header === null || typeof header !== "object") return null;
88345
+ if (Reflect.get(header, "alg") !== "ES256" || Reflect.get(header, "typ") !== "camstack-pairing-offer+jwt") return null;
88346
+ let valid = false;
88347
+ try {
88348
+ valid = (0, node_crypto.verify)("sha256", Buffer.from(`${headerB64}.${payloadB64}`, "utf8"), {
88349
+ key: new node_crypto.X509Certificate(caCertPem).publicKey,
88350
+ dsaEncoding: "ieee-p1363"
88351
+ }, Buffer.from(signatureB64, "base64url"));
88352
+ } catch {
88353
+ return null;
88354
+ }
88355
+ if (!valid || payload === null || typeof payload !== "object") return null;
88356
+ const str = (key) => {
88357
+ const v = Reflect.get(payload, key);
88358
+ return typeof v === "string" && v.length > 0 ? v : null;
88359
+ };
88360
+ const iat = Reflect.get(payload, "iat");
88361
+ const agentNodeId = str("agentNodeId");
88362
+ const hubNodeId = str("hubNodeId");
88363
+ const caFingerprint = str("caFingerprint");
88364
+ const commitment = str("commitment");
88365
+ const hubUrl = str("hubUrl");
88366
+ const hubMeshAddress = str("hubMeshAddress");
88367
+ if (Reflect.get(payload, "use") !== "pairing-offer") return null;
88368
+ if (typeof iat !== "number" || !Number.isFinite(iat)) return null;
88369
+ if (agentNodeId === null || hubNodeId === null || caFingerprint === null || commitment === null || hubUrl === null || hubMeshAddress === null) return null;
88370
+ return {
88371
+ use: PAIRING_OFFER_USE,
88372
+ agentNodeId,
88373
+ hubNodeId,
88374
+ caFingerprint,
88375
+ commitment,
88376
+ hubUrl,
88377
+ hubMeshAddress,
88378
+ iat
88379
+ };
88380
+ }
88381
+ //#endregion
88382
+ //#region src/kernel/moleculer/mesh-mtls/pairing/agent-pairing.ts
88383
+ /**
88384
+ * The agent's side of pairing (D857): up to four pending pairings (one per hub CA), their keys
88385
+ * in memory only, installed ONLY after the agent's own operator confirmed the same six digits
88386
+ * the hub shows AND proved presence at this agent with a one-time local PIN, and the hub's
88387
+ * transcript proof verified under the CA the agent was offered.
88388
+ *
88389
+ * Why a PIN: the agent's page is opened from another PC and its API is reachable by the whole
88390
+ * LAN. Anything the network can read, the network can use to "confirm" - so the code, the pairing
88391
+ * id and the PIN are shown ONLY on the agent's own machine (its console, a 0600 file, the local
88392
+ * window). A request that is not genuinely local must present the PIN of the pairing it confirms.
88393
+ *
88394
+ * unpaired ──offer──▶ pending ──reveal──▶ (code known) ──confirm(code + PIN) + complete──▶ paired
88395
+ * │ │
88396
+ * └── reject / expiry / 3 wrong codes / 3 wrong PINs ──▶ unpaired
88397
+ *
88398
+ * Offers are rationed: pairings that ended without the local operator confirming them count
88399
+ * against a budget (3 per 10 minutes, then exponential lockout) and against the hub CA that made
88400
+ * them (so a loop of "offer, read the code, abort" cannot grind the six digits). A paired agent
88401
+ * refuses every offer.
88402
+ */
88403
+ /** Abandoned offers allowed in {@link BUDGET_WINDOW_MS} before the lockout starts. */
88404
+ var BUDGET_OFFERS = 3;
88405
+ var BUDGET_WINDOW_MS = 10 * 6e4;
88406
+ var LOCKOUT_BASE_MS = 5 * 6e4;
88407
+ var LOCKOUT_MAX_MS = 60 * 6e4;
88408
+ /** Per hub CA: nothing for the first abandonment, then this x 2^(n-2), capped. */
88409
+ var CA_BACKOFF_BASE_MS = 3e4;
88410
+ var CA_BACKOFF_MAX_MS = 10 * 6e4;
88411
+ /** Wrong PINs (any session) before confirming is locked for a while. */
88412
+ var PIN_FAILURE_LIMIT = 5;
88413
+ var PIN_LOCK_MS = 2 * 6e4;
88414
+ var PIN_LOCK_MAX_MS = 60 * 6e4;
88415
+ /** Wrong PINs across ALL sources within one pairing lifetime before every pending pairing is cancelled. */
88416
+ var PIN_GLOBAL_CEILING = 200;
88417
+ var MAX_TRACKED_SOURCES = 256;
88418
+ /** Outcomes kept by pairing id so the hub can learn how its pairing ended. */
88419
+ var OUTCOME_LIMIT = 16;
88420
+ var OUTCOME_TTL_MS = 15 * 6e4;
88421
+ var MAX_FIELD = 2048;
88422
+ function fail(status, error, message, retryAfterMs) {
88423
+ return {
88424
+ ok: false,
88425
+ status,
88426
+ error,
88427
+ message,
88428
+ ...retryAfterMs !== void 0 ? { retryAfterMs } : {}
88429
+ };
88430
+ }
88431
+ function isText(value, max = MAX_FIELD) {
88432
+ return typeof value === "string" && value.length > 0 && value.length <= max;
88433
+ }
88434
+ function isHttpUrl(value) {
88435
+ try {
88436
+ const u = new URL(value);
88437
+ return u.protocol === "https:" || u.protocol === "http:";
88438
+ } catch {
88439
+ return false;
88440
+ }
88441
+ }
88442
+ /** The hub CA must be a self-signed CA certificate, valid now. */
88443
+ function readCaCertificate(pem, nowMs) {
88444
+ try {
88445
+ const cert = new node_crypto.X509Certificate(pem);
88446
+ if (!cert.ca || !cert.verify(cert.publicKey)) return null;
88447
+ if (Date.parse(cert.validTo) <= nowMs || Date.parse(cert.validFrom) > nowMs) return null;
88448
+ return cert;
88449
+ } catch {
88450
+ return null;
88451
+ }
88452
+ }
88453
+ /** `host:6443` / `[::1]:6443` / `host` → the host, port dropped. */
88454
+ function hubHostFromMeshAddress(address) {
88455
+ const trimmed = address.trim();
88456
+ const bracketed = /^(\[[^\]]+\])(?::\d+)?$/.exec(trimmed);
88457
+ if (bracketed?.[1] !== void 0) return bracketed[1];
88458
+ return /^([^:]+):\d+$/.exec(trimmed)?.[1] ?? trimmed;
88459
+ }
88460
+ var AgentPairing = class {
88461
+ deps;
88462
+ sessions = /* @__PURE__ */ new Map();
88463
+ /** Outcome of each recent pairing BY id (bounded): ending others never overwrites an answer. */
88464
+ outcomes = /* @__PURE__ */ new Map();
88465
+ /** When pairings that never reached local confirmation ended. */
88466
+ abandoned = [];
88467
+ lockedUntil = 0;
88468
+ perCa = /* @__PURE__ */ new Map();
88469
+ pinBySource = /* @__PURE__ */ new Map();
88470
+ globalPinFailures = [];
88471
+ resetCode;
88472
+ /** Commitments already offered (signed offers are single use) until their signature is stale. */
88473
+ seenCommitments = /* @__PURE__ */ new Map();
88474
+ now;
88475
+ constructor(deps) {
88476
+ this.deps = deps;
88477
+ this.now = deps.now ?? Date.now;
88478
+ this.resetCode = randomPin();
88479
+ deps.onResetCode?.(this.resetCode);
88480
+ }
88481
+ hello() {
88482
+ this.expireIfDue();
88483
+ const expiries = [...this.sessions.values()].map((s) => s.expiresAt);
88484
+ return {
88485
+ protocol: 1,
88486
+ nodeId: this.deps.nodeId,
88487
+ version: this.deps.version,
88488
+ state: this.state(),
88489
+ pendingExpiresAt: expiries.length === 0 ? null : Math.min(...expiries)
88490
+ };
88491
+ }
88492
+ state() {
88493
+ this.expireIfDue();
88494
+ if (this.deps.isPaired()) return "paired";
88495
+ return this.sessions.size > 0 ? "pending" : "unpaired";
88496
+ }
88497
+ /** Who is asking to pair, and until when. No code, no pairing id, no PIN. */
88498
+ publicView() {
88499
+ this.expireIfDue();
88500
+ return {
88501
+ state: this.state(),
88502
+ pending: [...this.sessions.values()].map((s) => ({
88503
+ hub: {
88504
+ nodeId: s.hub.nodeId,
88505
+ url: s.hub.url,
88506
+ caFingerprint: s.hub.caFingerprint
88507
+ },
88508
+ expiresAt: s.expiresAt
88509
+ }))
88510
+ };
88511
+ }
88512
+ /** The view for the agent's own machine: also the pairing ids, the codes and the PINs. */
88513
+ localView() {
88514
+ this.expireIfDue();
88515
+ return {
88516
+ state: this.state(),
88517
+ pending: [...this.sessions.values()].map((s) => ({
88518
+ pairingId: s.pairingId,
88519
+ hub: {
88520
+ nodeId: s.hub.nodeId,
88521
+ url: s.hub.url,
88522
+ caFingerprint: s.hub.caFingerprint
88523
+ },
88524
+ expiresAt: s.expiresAt,
88525
+ pin: s.pin,
88526
+ code: s.code,
88527
+ localConfirmed: s.localConfirmed
88528
+ }))
88529
+ };
88530
+ }
88531
+ async offer(body) {
88532
+ this.expireIfDue();
88533
+ if (this.deps.isPaired()) return fail(409, "already-paired", "this agent is paired — unpair it first (from the hub or here)");
88534
+ 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`);
88535
+ const request = parseOffer(body);
88536
+ if (request === null) return fail(400, "bad-offer", "the offer is malformed");
88537
+ if (request.protocol !== 1) return fail(400, "unsupported-protocol", `pairing protocol ${String(request.protocol)} is not supported`);
88538
+ if (readCaCertificate(request.hubCaCertPem, this.now()) === null) return fail(400, "bad-ca", "the offered CA is not a valid self-signed CA certificate");
88539
+ if (!/^[0-9a-f]{64}$/.test(request.commitment)) return fail(400, "bad-offer", "the commitment is malformed");
88540
+ if (!isHttpUrl(request.hubUrl)) return fail(400, "bad-offer", "hubUrl is not an http(s) URL");
88541
+ const caFingerprint = caFingerprintCompact(request.hubCaCertPem);
88542
+ const refused = this.checkOfferSignature(request, caFingerprint);
88543
+ if (refused !== null) return refused;
88544
+ const rationed = this.rationOffer(caFingerprint);
88545
+ if (rationed !== null) return rationed;
88546
+ for (const existing of [...this.sessions.values()]) if (existing.hub.caFingerprint === caFingerprint) this.end(existing, "failed");
88547
+ if (this.sessions.size >= 4 && !this.makeRoom()) return fail(409, "too-many-pending", "too many pairings are pending on this agent");
88548
+ const keys = await generateMeshKeyPair();
88549
+ const csrPem = await createMeshCsrPem(keys, this.deps.nodeId);
88550
+ const keyPem = await exportMeshPrivateKeyPem(keys.privateKey);
88551
+ const publicKeySpki = Buffer.from(await node_crypto.webcrypto.subtle.exportKey("spki", keys.publicKey)).toString("base64");
88552
+ if (this.sessions.size >= 4 && !this.makeRoom()) return fail(409, "too-many-pending", "too many pairings are pending on this agent");
88553
+ const session = {
88554
+ pairingId: newPairingId(),
88555
+ hub: {
88556
+ nodeId: request.hubNodeId,
88557
+ caCertPem: request.hubCaCertPem,
88558
+ caFingerprint,
88559
+ commitment: request.commitment,
88560
+ url: request.hubUrl,
88561
+ meshAddress: request.hubMeshAddress
88562
+ },
88563
+ keyPem,
88564
+ csrPem,
88565
+ publicKeySpki,
88566
+ agentNonce: randomHex(32),
88567
+ pin: randomPin(),
88568
+ expiresAt: this.now() + PAIRING_TTL_MS,
88569
+ hubNonce: null,
88570
+ code: null,
88571
+ localConfirmed: false,
88572
+ attempts: 0,
88573
+ touched: false,
88574
+ createdAt: this.now()
88575
+ };
88576
+ this.sessions.set(session.pairingId, session);
88577
+ this.deps.onStateChange?.();
88578
+ this.announceLocally(session);
88579
+ this.deps.log?.("pairing requested by a hub", {
88580
+ hubUrl: session.hub.url,
88581
+ caFingerprint: session.hub.caFingerprint
88582
+ });
88583
+ return {
88584
+ ok: true,
88585
+ answer: {
88586
+ pairingId: session.pairingId,
88587
+ csrPem,
88588
+ agentNonce: session.agentNonce,
88589
+ expiresAt: session.expiresAt
88590
+ }
88591
+ };
88592
+ }
88593
+ /** The hub reveals its nonce; the agent checks it against the commitment and derives the code. */
88594
+ reveal(body) {
88595
+ const session = this.pending(body);
88596
+ if ("ok" in session) return session;
88597
+ const hubNonce = field(body, "hubNonce");
88598
+ if (!isText(hubNonce, 256)) return fail(400, "bad-reveal", "hubNonce is missing");
88599
+ if (session.hubNonce !== null) return fail(409, "already-revealed", "the nonce was already revealed");
88600
+ if (!commitmentsEqual(computePairingCommitment(session.hub.caCertPem, hubNonce), session.hub.commitment)) {
88601
+ this.end(session, "failed");
88602
+ return fail(400, "commitment-mismatch", "the revealed nonce does not match the hub commitment");
88603
+ }
88604
+ session.touched = true;
88605
+ session.hubNonce = hubNonce;
88606
+ session.code = computePairingSas({
88607
+ caCertPem: session.hub.caCertPem,
88608
+ agentPublicKeySpki: session.publicKeySpki,
88609
+ hubNonce,
88610
+ agentNonce: session.agentNonce
88611
+ });
88612
+ this.announceLocally(session);
88613
+ return { ok: true };
88614
+ }
88615
+ /** The hub polls with the pairing id: did the agent's operator confirm, did it end? */
88616
+ poll(body) {
88617
+ this.expireIfDue();
88618
+ const pairingId = field(body, "pairingId");
88619
+ const session = typeof pairingId === "string" ? this.sessions.get(pairingId) : void 0;
88620
+ if (session !== void 0) {
88621
+ session.touched = true;
88622
+ return {
88623
+ ok: true,
88624
+ poll: {
88625
+ state: "pending",
88626
+ localConfirmed: session.localConfirmed,
88627
+ last: null
88628
+ }
88629
+ };
88630
+ }
88631
+ const known = typeof pairingId === "string" ? this.outcomeOf(pairingId) : null;
88632
+ if (typeof pairingId === "string" && known !== null) return {
88633
+ ok: true,
88634
+ poll: {
88635
+ state: "idle",
88636
+ localConfirmed: false,
88637
+ last: {
88638
+ pairingId,
88639
+ outcome: known
88640
+ }
88641
+ }
88642
+ };
88643
+ return fail(404, "no-pairing", "no such pairing is pending");
88644
+ }
88645
+ /**
88646
+ * The operator typed the code the HUB shows. From the agent's own machine that is enough; from
88647
+ * anywhere else the one-time PIN of THIS pairing (read on the agent's console) must come with
88648
+ * it. A wrong code counts against the pairing (the third cancels it); a wrong PIN counts against
88649
+ * the agent (five lock confirming for five minutes).
88650
+ */
88651
+ confirm(body, ctx) {
88652
+ const found = this.localTarget(body, ctx);
88653
+ if ("ok" in found) return found;
88654
+ const session = found;
88655
+ if (session.code === null) return fail(409, "not-ready", "the hub has not revealed its nonce yet");
88656
+ const code = field(body, "code");
88657
+ if (!constantTimeEqual(session.code, typeof code === "string" ? code.trim() : code)) {
88658
+ session.attempts += 1;
88659
+ if (session.attempts >= 3) {
88660
+ this.end(session, "code-mismatch");
88661
+ return fail(403, "code-mismatch", "wrong code three times — the pairing was cancelled");
88662
+ }
88663
+ return fail(403, "code-mismatch", "that is not the code this agent computed");
88664
+ }
88665
+ session.localConfirmed = true;
88666
+ return { ok: true };
88667
+ }
88668
+ reject(body, ctx) {
88669
+ const found = this.localTarget(body, ctx);
88670
+ if ("ok" in found) return found;
88671
+ this.end(found, "rejected");
88672
+ return { ok: true };
88673
+ }
88674
+ /** The hub withdraws its own pairing (authorized by the pairing id it was given). */
88675
+ cancel(body) {
88676
+ const session = this.pending(body);
88677
+ if ("ok" in session) return session;
88678
+ this.end(session, "rejected");
88679
+ return { ok: true };
88680
+ }
88681
+ async complete(body) {
88682
+ const session = this.pending(body);
88683
+ if ("ok" in session) return session;
88684
+ if (session.hubNonce === null || session.code === null) return fail(409, "not-ready", "the hub has not revealed its nonce yet");
88685
+ if (!session.localConfirmed) return fail(409, "awaiting-local-confirmation", "the operator has not confirmed on this agent");
88686
+ const leafPem = field(body, "leafPem");
88687
+ const caCertPem = field(body, "caCertPem");
88688
+ const proof = field(body, "proof");
88689
+ if (!isText(leafPem, 16384) || !isText(caCertPem, 16384) || !isText(proof, 16384)) return fail(400, "bad-complete", "the completion is malformed");
88690
+ const refusal = this.checkCompletion(session, leafPem, caCertPem, proof);
88691
+ if (refusal !== null) {
88692
+ this.end(session, "failed");
88693
+ return refusal;
88694
+ }
88695
+ try {
88696
+ await this.deps.install({
88697
+ meshTls: {
88698
+ certPem: leafPem,
88699
+ keyPem: session.keyPem,
88700
+ caCertPem: session.hub.caCertPem
88701
+ },
88702
+ hubAddress: hubHostFromMeshAddress(session.hub.meshAddress),
88703
+ hubUrl: session.hub.url
88704
+ });
88705
+ } catch (err) {
88706
+ this.end(session, "failed");
88707
+ return fail(500, "install-failed", err instanceof Error ? err.message : String(err));
88708
+ }
88709
+ this.end(session, "completed");
88710
+ for (const other of [...this.sessions.values()]) this.end(other, "failed", false);
88711
+ this.deps.log?.("paired with the hub", { hubUrl: session.hub.url });
88712
+ return { ok: true };
88713
+ }
88714
+ /** The agent forgot its hub (local action): the pairing slate is clean. */
88715
+ noteUnpaired() {
88716
+ for (const s of [...this.sessions.values()]) this.end(s, "failed", false);
88717
+ this.deps.onStateChange?.();
88718
+ }
88719
+ /** Remaining lock for a source, or `null`. Loopback never calls this. */
88720
+ sourceLock(source, at) {
88721
+ const entry = this.pinBySource.get(source);
88722
+ return entry !== void 0 && at < entry.lockedUntil ? entry.lockedUntil - at : null;
88723
+ }
88724
+ /** A wrong PIN or reset code from `source`: five in five minutes lock THAT source, doubling. */
88725
+ noteWrongSecret(source, at) {
88726
+ if (this.pinBySource.size >= MAX_TRACKED_SOURCES && !this.pinBySource.has(source)) this.pinBySource.clear();
88727
+ const prev = this.pinBySource.get(source) ?? {
88728
+ failures: [],
88729
+ strikes: 0,
88730
+ lockedUntil: 0
88731
+ };
88732
+ const failures = [...prev.failures.filter((t) => at - t < 5 * 6e4), at];
88733
+ if (failures.length >= PIN_FAILURE_LIMIT) {
88734
+ const lockedUntil = at + Math.min(PIN_LOCK_MAX_MS, PIN_LOCK_MS * 2 ** prev.strikes);
88735
+ this.deps.log?.("confirming is locked for a source: too many wrong PINs", { source });
88736
+ this.pinBySource.set(source, {
88737
+ failures: [],
88738
+ strikes: prev.strikes + 1,
88739
+ lockedUntil
88740
+ });
88741
+ } else this.pinBySource.set(source, {
88742
+ ...prev,
88743
+ failures
88744
+ });
88745
+ this.globalPinFailures = [...this.globalPinFailures.filter((t) => at - t < PAIRING_TTL_MS), at];
88746
+ if (this.globalPinFailures.length >= PIN_GLOBAL_CEILING) {
88747
+ this.globalPinFailures = [];
88748
+ this.deps.log?.("too many wrong PINs overall: every pending pairing is cancelled", {});
88749
+ for (const session of [...this.sessions.values()]) this.end(session, "failed", false);
88750
+ }
88751
+ }
88752
+ /**
88753
+ * The operator clears the lockouts, the budget and every pending offer. From this machine
88754
+ * (genuine loopback) or with the LOCAL RESET CODE printed at boot; the code is renewed on use.
88755
+ */
88756
+ reset(body, ctx) {
88757
+ const at = this.now();
88758
+ if (!ctx.local) {
88759
+ const key = `reset:${ctx.source ?? "unknown"}`;
88760
+ const locked = this.sourceLock(key, at);
88761
+ if (locked !== null) return fail(429, "reset-locked", "too many wrong codes", locked);
88762
+ const code = field(body, "code");
88763
+ if (typeof code !== "string" || !constantTimeEqual(this.resetCode, code.trim())) {
88764
+ this.noteWrongSecret(key, at);
88765
+ return fail(401, "reset-code-required", "the reset code printed on the agent is required");
88766
+ }
88767
+ }
88768
+ for (const session of [...this.sessions.values()]) this.end(session, "failed", false);
88769
+ this.abandoned = [];
88770
+ this.lockedUntil = 0;
88771
+ this.perCa.clear();
88772
+ this.pinBySource.clear();
88773
+ this.globalPinFailures = [];
88774
+ this.resetCode = randomPin();
88775
+ this.deps.onResetCode?.(this.resetCode);
88776
+ this.deps.log?.("pairing was reset by the local operator", {});
88777
+ return { ok: true };
88778
+ }
88779
+ /** Verify the CA-key signature of an offer and spend its commitment. */
88780
+ checkOfferSignature(request, caFingerprint) {
88781
+ const claims = verifyPairingOffer(request.hubCaCertPem, request.signature);
88782
+ const at = this.now();
88783
+ 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) {
88784
+ this.deps.log?.("an offer without a valid CA signature was refused", {});
88785
+ return fail(403, "bad-offer-signature", "the offer is not signed by the CA it presents");
88786
+ }
88787
+ for (const [commitment, until] of this.seenCommitments) if (until <= at) this.seenCommitments.delete(commitment);
88788
+ if (this.seenCommitments.has(request.commitment)) return fail(409, "offer-replayed", "this offer was already used");
88789
+ this.seenCommitments.set(request.commitment, at + PAIRING_OFFER_MAX_AGE_MS + 6e4);
88790
+ return null;
88791
+ }
88792
+ /**
88793
+ * All slots taken: a signed offer makes room by dropping the oldest pairing whose hub never showed
88794
+ * activity (no reveal, no poll) and that no operator confirmed. `false` when there is none.
88795
+ */
88796
+ makeRoom() {
88797
+ const oldest = [...this.sessions.values()].filter((s) => !s.touched && !s.localConfirmed).toSorted((a, b) => a.createdAt - b.createdAt)[0];
88798
+ if (oldest === void 0) return false;
88799
+ this.deps.log?.("a pending pairing made room for a newer signed offer", { caFingerprint: oldest.hub.caFingerprint });
88800
+ this.end(oldest, "failed", false);
88801
+ return true;
88802
+ }
88803
+ /** Offers that never reached local confirmation are rationed - globally and per hub CA. */
88804
+ rationOffer(caFingerprint) {
88805
+ const at = this.now();
88806
+ if (at < this.lockedUntil) {
88807
+ this.deps.log?.("offer refused: the budget of abandoned pairings is spent", { retryAfterMs: this.lockedUntil - at });
88808
+ return fail(429, "offer-budget", "too many pairings were abandoned recently - try again later", this.lockedUntil - at);
88809
+ }
88810
+ const ca = this.perCa.get(caFingerprint);
88811
+ if (ca !== void 0 && at < ca.until) {
88812
+ this.deps.log?.("offer refused: this hub abandoned a pairing a moment ago", {
88813
+ caFingerprint,
88814
+ retryAfterMs: ca.until - at
88815
+ });
88816
+ return fail(429, "offer-backoff", "this hub abandoned a pairing a moment ago - wait before offering again", ca.until - at);
88817
+ }
88818
+ return null;
88819
+ }
88820
+ /** A pairing ended without the local operator confirming it: it counts against the budget. */
88821
+ noteAbandoned(session) {
88822
+ const at = this.now();
88823
+ this.abandoned = [...this.abandoned.filter((t) => at - t < BUDGET_WINDOW_MS), at];
88824
+ if (this.abandoned.length >= BUDGET_OFFERS) {
88825
+ const over = this.abandoned.length - BUDGET_OFFERS;
88826
+ const lock = Math.min(LOCKOUT_MAX_MS, LOCKOUT_BASE_MS * 2 ** over);
88827
+ this.lockedUntil = at + lock;
88828
+ this.deps.log?.("pairing offers are locked: too many were abandoned", {
88829
+ abandoned: this.abandoned.length,
88830
+ lockMs: lock
88831
+ });
88832
+ }
88833
+ const count = (this.perCa.get(session.hub.caFingerprint)?.count ?? 0) + 1;
88834
+ const backoff = count < 2 ? 0 : Math.min(CA_BACKOFF_MAX_MS, CA_BACKOFF_BASE_MS * 2 ** (count - 2));
88835
+ this.perCa.set(session.hub.caFingerprint, {
88836
+ count,
88837
+ until: at + backoff
88838
+ });
88839
+ }
88840
+ /** The pairing this local request means, else the refusal. */
88841
+ localTarget(body, ctx) {
88842
+ this.expireIfDue();
88843
+ if (this.sessions.size === 0) return fail(404, "no-pairing", "no pairing is pending");
88844
+ const pin = field(body, "pin");
88845
+ if (!ctx.local) {
88846
+ const at = this.now();
88847
+ const source = ctx.source ?? "unknown";
88848
+ const locked = this.sourceLock(source, at);
88849
+ if (locked !== null) return fail(429, "pin-locked", "too many wrong PINs - try again later", locked);
88850
+ const match = typeof pin === "string" ? [...this.sessions.values()].find((s) => constantTimeEqual(s.pin, pin.trim())) : void 0;
88851
+ if (match === void 0) {
88852
+ this.noteWrongSecret(source, at);
88853
+ return fail(401, "pin-required", "the PIN shown on this agent's own machine (console, file or local window) is required");
88854
+ }
88855
+ return match;
88856
+ }
88857
+ if (typeof pin === "string" && pin.trim().length > 0) {
88858
+ const match = [...this.sessions.values()].find((s) => constantTimeEqual(s.pin, pin.trim()));
88859
+ if (match !== void 0) return match;
88860
+ }
88861
+ const only = [...this.sessions.values()];
88862
+ if (only.length === 1 && only[0] !== void 0) return only[0];
88863
+ return fail(409, "ambiguous", "several pairings are pending - give the PIN of the one you mean");
88864
+ }
88865
+ announceLocally(session) {
88866
+ this.deps.onLocalSecret?.({
88867
+ pairingId: session.pairingId,
88868
+ pin: session.pin,
88869
+ code: session.code,
88870
+ hub: {
88871
+ nodeId: session.hub.nodeId,
88872
+ url: session.hub.url,
88873
+ caFingerprint: session.hub.caFingerprint
88874
+ },
88875
+ expiresAt: session.expiresAt
88876
+ });
88877
+ }
88878
+ checkCompletion(session, leafPem, caCertPem, proof) {
88879
+ if (caFingerprintOf(caCertPem) !== session.hub.caFingerprint) return fail(400, "ca-mismatch", "the completion carries a CA other than the one offered");
88880
+ const claims = verifyPairingProof(session.hub.caCertPem, proof);
88881
+ if (claims === null) return fail(400, "bad-proof", "the pairing proof does not verify");
88882
+ 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");
88883
+ const at = this.now();
88884
+ 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");
88885
+ try {
88886
+ if (claims.leafFingerprint !== leafFingerprintCompact(leafPem)) return fail(400, "bad-leaf", "the certificate is not the one the proof names");
88887
+ const leaf = new node_crypto.X509Certificate(leafPem);
88888
+ const ca = new node_crypto.X509Certificate(session.hub.caCertPem);
88889
+ if (!leaf.verify(ca.publicKey)) return fail(400, "bad-leaf", "the certificate is not signed by the offered CA");
88890
+ if (subjectCn(leaf.subject.replaceAll("\n", ",")) !== this.deps.nodeId) return fail(400, "bad-leaf", `the certificate is not issued for "${this.deps.nodeId}"`);
88891
+ if (!leaf.checkPrivateKey((0, node_crypto.createPrivateKey)(session.keyPem))) return fail(400, "bad-leaf", "the certificate does not match this agent's private key");
88892
+ if (Date.parse(leaf.validTo) <= this.now()) return fail(400, "bad-leaf", "the certificate is already expired");
88893
+ } catch {
88894
+ return fail(400, "bad-leaf", "the certificate could not be read");
88895
+ }
88896
+ return null;
88897
+ }
88898
+ /** The live session `body.pairingId` names, else the refusal. */
88899
+ pending(body) {
88900
+ this.expireIfDue();
88901
+ const pairingId = field(body, "pairingId");
88902
+ if (this.sessions.size === 0) return fail(404, "no-pairing", "no pairing is pending");
88903
+ const session = typeof pairingId === "string" ? this.sessions.get(pairingId) : void 0;
88904
+ if (session === void 0) return fail(403, "wrong-pairing", "that is not a pending pairing");
88905
+ return session;
88906
+ }
88907
+ expireIfDue() {
88908
+ for (const s of [...this.sessions.values()]) if (s.expiresAt <= this.now()) this.end(s, "expired");
88909
+ }
88910
+ outcomeOf(pairingId) {
88911
+ const entry = this.outcomes.get(pairingId);
88912
+ if (entry === void 0) return null;
88913
+ if (this.now() - entry.at > OUTCOME_TTL_MS) {
88914
+ this.outcomes.delete(pairingId);
88915
+ return null;
88916
+ }
88917
+ return entry.outcome;
88918
+ }
88919
+ rememberOutcome(pairingId, outcome) {
88920
+ this.outcomes.set(pairingId, {
88921
+ outcome,
88922
+ at: this.now()
88923
+ });
88924
+ while (this.outcomes.size > OUTCOME_LIMIT) {
88925
+ const oldest = this.outcomes.keys().next().value;
88926
+ if (oldest === void 0) break;
88927
+ this.outcomes.delete(oldest);
88928
+ }
88929
+ }
88930
+ end(session, outcome, count = true) {
88931
+ if (!this.sessions.delete(session.pairingId)) return;
88932
+ this.rememberOutcome(session.pairingId, outcome);
88933
+ if (outcome === "completed") this.perCa.delete(session.hub.caFingerprint);
88934
+ else if (count && !session.localConfirmed) this.noteAbandoned(session);
88935
+ this.deps.onLocalSecretCleared?.(session.pairingId);
88936
+ this.deps.onStateChange?.();
88937
+ if (outcome !== "completed") this.deps.log?.("pairing cancelled", {
88938
+ outcome,
88939
+ hubUrl: session.hub.url
88940
+ });
88941
+ }
88942
+ };
88943
+ function field(body, key) {
88944
+ return body !== null && typeof body === "object" ? Reflect.get(body, key) : void 0;
88945
+ }
88946
+ function caFingerprintOf(pem) {
88947
+ try {
88948
+ return caFingerprintCompact(pem);
88949
+ } catch {
88950
+ return null;
88951
+ }
88952
+ }
88953
+ function parseOffer(body) {
88954
+ if (body === null || typeof body !== "object") return null;
88955
+ const protocol = Reflect.get(body, "protocol");
88956
+ const hubNodeId = Reflect.get(body, "hubNodeId");
88957
+ const hubCaCertPem = Reflect.get(body, "hubCaCertPem");
88958
+ const commitment = Reflect.get(body, "commitment");
88959
+ const hubUrl = Reflect.get(body, "hubUrl");
88960
+ const hubMeshAddress = Reflect.get(body, "hubMeshAddress");
88961
+ const signature = Reflect.get(body, "signature");
88962
+ if (typeof protocol !== "number") return null;
88963
+ if (!isText(hubNodeId, 128) || !isText(hubCaCertPem, 16384) || !isText(commitment, 128)) return null;
88964
+ if (!isText(hubUrl) || !isText(hubMeshAddress, 255) || !isText(signature, 4096)) return null;
88965
+ return {
88966
+ protocol,
88967
+ hubNodeId,
88968
+ hubCaCertPem,
88969
+ commitment,
88970
+ hubUrl,
88971
+ hubMeshAddress,
88972
+ signature
88973
+ };
88974
+ }
88975
+ //#endregion
88976
+ //#region src/kernel/moleculer/mesh-mtls/pairing/hub-pairing.ts
88977
+ /**
88978
+ * The hub's side of pairing (D857): starts a pairing with an agent it found
88979
+ * (mDNS) or was given an address for, computes the same six digits the agent
88980
+ * shows, and signs the agent's certificate ONLY after the operator confirmed
88981
+ * them on the hub AND the agent's own operator confirmed them there.
88982
+ *
88983
+ * Either confirmation may come first. The hub learns the agent's by polling the
88984
+ * agent's pairing status (`advance`), never by being told: nothing the agent
88985
+ * says is trusted beyond "my operator confirmed", and even that only matters
88986
+ * together with the hub's own confirmation.
88987
+ *
88988
+ * Transport-agnostic: `AgentPairingTransport` is the HTTP client in production
88989
+ * and an in-memory shim (or a man in the middle) in specs.
88990
+ */
88991
+ /** A refusal the agent sent, kept with its machine-readable code. */
88992
+ var PairingHttpError = class extends Error {
88993
+ status;
88994
+ code;
88995
+ constructor(status, code, message) {
88996
+ super(message);
88997
+ this.status = status;
88998
+ this.code = code;
88999
+ this.name = "PairingHttpError";
89000
+ }
89001
+ };
89002
+ /** Pairings kept (active and just ended) so the page can show how one ended. */
89003
+ var MAX_SESSIONS = 16;
89004
+ var PROOF_TTL_MS = Math.min(2 * 6e4, PAIRING_PROOF_MAX_LIFETIME_MS);
89005
+ var TERMINAL = new Set([
89006
+ "paired",
89007
+ "rejected",
89008
+ "expired",
89009
+ "failed"
89010
+ ]);
89011
+ var HubPairing = class {
89012
+ deps;
89013
+ sessions = /* @__PURE__ */ new Map();
89014
+ now;
89015
+ constructor(deps) {
89016
+ this.deps = deps;
89017
+ this.now = deps.now ?? Date.now;
89018
+ }
89019
+ /** Offer the hub's CA to the agent at `endpoint` and derive the code. Throws an `Error` with the reason. */
89020
+ async begin(endpoint) {
89021
+ const ca = this.deps.ca();
89022
+ if (ca === null) throw new Error("the mesh CA is not ready — try again in a moment");
89023
+ const hello = await this.deps.transport.hello(endpoint);
89024
+ if (hello.protocol !== 1) throw new Error(`the agent speaks pairing protocol ${String(hello.protocol)}, not 1`);
89025
+ if (hello.state === "paired") throw new Error(`"${hello.nodeId}" is already paired — unpair it first`);
89026
+ this.assertNodeIdUsable(hello.nodeId);
89027
+ 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`);
89028
+ const hubNonce = randomHex(32);
89029
+ const hubUrl = await this.deps.hubUrl();
89030
+ const hubMeshAddress = await this.deps.hubMeshAddress();
89031
+ const commitment = computePairingCommitment(ca.certPem, hubNonce);
89032
+ const answer = await this.deps.transport.offer(endpoint, {
89033
+ protocol: 1,
89034
+ hubNodeId: this.deps.hubNodeId,
89035
+ hubCaCertPem: ca.certPem,
89036
+ commitment,
89037
+ hubUrl,
89038
+ hubMeshAddress,
89039
+ signature: signPairingOffer(ca.keyPem, {
89040
+ use: PAIRING_OFFER_USE,
89041
+ agentNodeId: hello.nodeId,
89042
+ hubNodeId: this.deps.hubNodeId,
89043
+ caFingerprint: caFingerprintCompact(ca.certPem),
89044
+ commitment,
89045
+ hubUrl,
89046
+ hubMeshAddress,
89047
+ iat: this.now()
89048
+ })
89049
+ });
89050
+ try {
89051
+ const csr = await describeCsr(answer.csrPem);
89052
+ if (csr.commonName !== hello.nodeId) throw new Error(`the agent's CSR is for "${csr.commonName}", not "${hello.nodeId}"`);
89053
+ await this.deps.transport.reveal(endpoint, {
89054
+ pairingId: answer.pairingId,
89055
+ hubNonce
89056
+ });
89057
+ const code = computePairingSas({
89058
+ caCertPem: ca.certPem,
89059
+ agentPublicKeySpki: csr.publicKeySpki,
89060
+ hubNonce,
89061
+ agentNonce: answer.agentNonce
89062
+ });
89063
+ const session = {
89064
+ pairingId: answer.pairingId,
89065
+ nodeId: hello.nodeId,
89066
+ endpoint,
89067
+ code,
89068
+ csrPem: answer.csrPem,
89069
+ hubNonce,
89070
+ agentNonce: answer.agentNonce,
89071
+ caCertPem: ca.certPem,
89072
+ hubUrl,
89073
+ hubMeshAddress,
89074
+ expiresAt: Math.min(answer.expiresAt, this.now() + PAIRING_TTL_MS),
89075
+ state: "awaiting-confirmation",
89076
+ hubConfirmed: false,
89077
+ agentConfirmed: false,
89078
+ attempts: 0,
89079
+ failure: null,
89080
+ issued: false
89081
+ };
89082
+ this.remember(session);
89083
+ this.deps.log?.("pairing started", {
89084
+ nodeId: session.nodeId,
89085
+ pairingId: session.pairingId
89086
+ });
89087
+ return this.view(session);
89088
+ } catch (err) {
89089
+ await this.deps.transport.reject(endpoint, answer.pairingId).catch(() => void 0);
89090
+ throw err;
89091
+ }
89092
+ }
89093
+ /** The operator typed the code the AGENT shows. A wrong one counts; the third cancels the pairing. */
89094
+ async confirm(pairingId, code) {
89095
+ const session = this.require(pairingId);
89096
+ this.expireIfDue(session);
89097
+ if (TERMINAL.has(session.state)) return this.view(session);
89098
+ if (!constantTimeEqual(session.code, typeof code === "string" ? code.trim() : code)) {
89099
+ session.attempts += 1;
89100
+ if (session.attempts >= 3) await this.cancel(session, "rejected", "wrong code three times");
89101
+ 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");
89102
+ }
89103
+ session.hubConfirmed = true;
89104
+ return this.advance(pairingId);
89105
+ }
89106
+ async reject(pairingId) {
89107
+ const session = this.require(pairingId);
89108
+ if (!TERMINAL.has(session.state)) await this.cancel(session, "rejected", "rejected on the hub");
89109
+ return this.view(session);
89110
+ }
89111
+ get(pairingId) {
89112
+ const session = this.sessions.get(pairingId);
89113
+ return session === void 0 ? void 0 : this.view(session);
89114
+ }
89115
+ list() {
89116
+ return [...this.sessions.values()].map((s) => this.view(s));
89117
+ }
89118
+ /** Active pairings, for the poller. */
89119
+ activeIds() {
89120
+ return [...this.sessions.values()].filter((s) => !TERMINAL.has(s.state)).map((s) => s.pairingId);
89121
+ }
89122
+ /**
89123
+ * Move a pairing on: notice an expiry, a cancellation or the agent's
89124
+ * confirmation, and complete when both sides confirmed. Never throws.
89125
+ */
89126
+ async advance(pairingId) {
89127
+ const session = this.require(pairingId);
89128
+ if (TERMINAL.has(session.state) || session.state === "completing") return this.view(session);
89129
+ if (this.expireIfDue(session)) {
89130
+ await this.deps.transport.reject(session.endpoint, session.pairingId).catch(() => void 0);
89131
+ return this.view(session);
89132
+ }
89133
+ try {
89134
+ const poll = await this.deps.transport.poll(session.endpoint, session.pairingId);
89135
+ if (poll.state === "pending") session.agentConfirmed = poll.localConfirmed;
89136
+ else if (poll.last?.pairingId === session.pairingId && poll.last.outcome === "completed") {
89137
+ this.finish(session, "paired", null);
89138
+ return this.view(session);
89139
+ } else {
89140
+ const why = poll.last?.pairingId === session.pairingId ? poll.last.outcome : "gone";
89141
+ this.finish(session, "failed", `the agent ended the pairing (${why})`);
89142
+ return this.view(session);
89143
+ }
89144
+ } catch (err) {
89145
+ if (err instanceof PairingHttpError && err.code === "no-pairing") {
89146
+ this.finish(session, "failed", "the agent no longer has this pairing");
89147
+ return this.view(session);
89148
+ }
89149
+ this.deps.log?.("pairing status poll failed", {
89150
+ nodeId: session.nodeId,
89151
+ error: err instanceof Error ? err.message : String(err)
89152
+ });
89153
+ return this.view(session);
89154
+ }
89155
+ if (session.hubConfirmed && session.agentConfirmed) await this.complete(session);
89156
+ return this.view(session);
89157
+ }
89158
+ async complete(session) {
89159
+ const ca = this.deps.ca();
89160
+ if (ca === null || caFingerprintCompact(ca.certPem) !== caFingerprintCompact(session.caCertPem)) {
89161
+ await this.cancel(session, "failed", "the mesh CA changed during the pairing");
89162
+ return;
89163
+ }
89164
+ session.state = "completing";
89165
+ let issuedPem = null;
89166
+ try {
89167
+ const leaf = await issueMeshLeafFromCsr(ca, session.csrPem, { expectedNodeId: session.nodeId });
89168
+ issuedPem = leaf.certPem;
89169
+ await this.deps.recordIssued(session.nodeId, leaf.certPem);
89170
+ session.issued = true;
89171
+ const proof = signPairingProof(ca.keyPem, {
89172
+ v: 1,
89173
+ use: PAIRING_PROOF_USE,
89174
+ pairingId: session.pairingId,
89175
+ nodeId: session.nodeId,
89176
+ hubNonce: session.hubNonce,
89177
+ agentNonce: session.agentNonce,
89178
+ leafFingerprint: leafFingerprintCompact(leaf.certPem),
89179
+ caFingerprint: caFingerprintCompact(ca.certPem),
89180
+ hubMeshAddress: session.hubMeshAddress,
89181
+ hubUrl: session.hubUrl,
89182
+ iat: this.now(),
89183
+ exp: this.now() + PROOF_TTL_MS
89184
+ });
89185
+ await this.deps.transport.complete(session.endpoint, {
89186
+ pairingId: session.pairingId,
89187
+ leafPem: leaf.certPem,
89188
+ caCertPem: ca.certPem,
89189
+ proof
89190
+ });
89191
+ await this.installed(session, leaf.certPem);
89192
+ } catch (err) {
89193
+ const reason = err instanceof Error ? err.message : String(err);
89194
+ if (err instanceof PairingHttpError && err.code === "awaiting-local-confirmation") {
89195
+ session.state = "awaiting-confirmation";
89196
+ session.agentConfirmed = false;
89197
+ return;
89198
+ }
89199
+ let outcome = issuedPem === null ? "absent" : await this.installOutcome(session);
89200
+ if (outcome !== "installed" && issuedPem !== null && this.deps.isLeafPresented?.(session.nodeId, issuedPem) === true) outcome = "installed";
89201
+ if (outcome === "installed" && issuedPem !== null) {
89202
+ await this.installed(session, issuedPem);
89203
+ return;
89204
+ }
89205
+ if (outcome === "absent" && issuedPem !== null) await this.deps.revokeLeaf?.(session.nodeId, issuedPem, "pairing did not complete").catch(() => void 0);
89206
+ 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;
89207
+ await this.cancel(session, "failed", note);
89208
+ }
89209
+ }
89210
+ /** The new leaf is installed: older leaves of the node go, the new one stays. */
89211
+ async installed(session, leafPem) {
89212
+ await this.deps.revokeNode(session.nodeId, "superseded by pairing", leafPem).catch((err) => this.deps.log?.("older leaves of the node could not be revoked", {
89213
+ nodeId: session.nodeId,
89214
+ error: err instanceof Error ? err.message : String(err)
89215
+ }));
89216
+ this.finish(session, "paired", null);
89217
+ this.deps.log?.("agent paired", {
89218
+ nodeId: session.nodeId,
89219
+ pairingId: session.pairingId
89220
+ });
89221
+ }
89222
+ /** After a failed completion call: did the agent install the certificate after all? */
89223
+ async installOutcome(session) {
89224
+ try {
89225
+ const poll = await this.deps.transport.poll(session.endpoint, session.pairingId);
89226
+ if (poll.last?.pairingId === session.pairingId && poll.last.outcome === "completed") return "installed";
89227
+ return "absent";
89228
+ } catch (err) {
89229
+ return "unknown";
89230
+ }
89231
+ }
89232
+ assertNodeIdUsable(nodeId) {
89233
+ if (!isValidAgentNodeId(nodeId)) throw new Error(`"${nodeId}" cannot be an agent node id (letters, digits, . _ - ; not "hub"; at most 63 characters)`);
89234
+ if (this.deps.isNodeConnected(nodeId)) throw new Error(`a node called "${nodeId}" is already connected to this hub — unpair it first`);
89235
+ const lower = nodeId.toLowerCase();
89236
+ const clash = (this.deps.knownNodeIds?.() ?? []).find((known) => known !== nodeId && known.toLowerCase() === lower);
89237
+ if (clash !== void 0) throw new Error(`"${nodeId}" differs only in case from the existing node "${clash}"`);
89238
+ }
89239
+ expireIfDue(session) {
89240
+ if (TERMINAL.has(session.state) || session.expiresAt > this.now()) return false;
89241
+ this.finish(session, "expired", "the pairing was not confirmed on both sides in time");
89242
+ return true;
89243
+ }
89244
+ finish(session, state, failure) {
89245
+ session.state = state;
89246
+ session.failure = failure;
89247
+ }
89248
+ async cancel(session, state, reason) {
89249
+ this.finish(session, state, reason);
89250
+ await this.deps.transport.reject(session.endpoint, session.pairingId).catch(() => void 0);
89251
+ this.deps.log?.("pairing cancelled", {
89252
+ nodeId: session.nodeId,
89253
+ state,
89254
+ reason
89255
+ });
89256
+ }
89257
+ require(pairingId) {
89258
+ const session = this.sessions.get(pairingId);
89259
+ if (session === void 0) throw new Error("no such pairing");
89260
+ return session;
89261
+ }
89262
+ remember(session) {
89263
+ this.sessions.set(session.pairingId, session);
89264
+ if (this.sessions.size <= MAX_SESSIONS) return;
89265
+ for (const [id, s] of this.sessions) {
89266
+ if (this.sessions.size <= MAX_SESSIONS) break;
89267
+ if (TERMINAL.has(s.state)) this.sessions.delete(id);
89268
+ }
89269
+ }
89270
+ view(s) {
89271
+ return {
89272
+ pairingId: s.pairingId,
89273
+ nodeId: s.nodeId,
89274
+ endpoint: s.endpoint,
89275
+ code: s.code,
89276
+ state: s.state,
89277
+ hubConfirmed: s.hubConfirmed,
89278
+ agentConfirmed: s.agentConfirmed,
89279
+ expiresAt: s.expiresAt,
89280
+ failure: s.failure
89281
+ };
89282
+ }
89283
+ };
89284
+ //#endregion
87968
89285
  //#region src/builtins/system-config/cluster-secret-keeper.ts
87969
89286
  /**
87970
89287
  * Keeps the hub's cluster-secret gate in step with System → Cluster (D797,
@@ -88112,22 +89429,22 @@ function errText(err) {
88112
89429
  * agent-ui unlock tickets (D821).
88113
89430
  */
88114
89431
  var mintJoinTokenInputSchema = zod.z.object({
88115
- /** Optional pinned node id for the CSR CN. */
88116
- nodeId: zod.z.string().min(1).optional(),
89432
+ /** The node id the token is for - mandatory: the CSR CN must be exactly this. */
89433
+ nodeId: zod.z.string().min(1),
88117
89434
  /** Override hub HTTPS base (default: local-network publicEndpoint). */
88118
89435
  hubUrl: zod.z.string().url().optional(),
88119
89436
  /** Override `host:6443` TLS mesh address carried in the token. */
88120
89437
  hubMeshAddress: zod.z.string().min(3).optional(),
88121
89438
  /** Seconds until expiry; capped at 1 h. */
88122
89439
  ttlSec: zod.z.number().int().positive().max(3600).optional()
88123
- }).optional();
89440
+ });
88124
89441
  var mintJoinTokenOutputSchema = zod.z.object({
88125
89442
  token: zod.z.string().min(1),
88126
89443
  exp: zod.z.number().int(),
88127
89444
  hubUrl: zod.z.string(),
88128
89445
  hubMeshAddress: zod.z.string(),
88129
89446
  caFingerprint: zod.z.string(),
88130
- nodeId: zod.z.string().optional()
89447
+ nodeId: zod.z.string()
88131
89448
  });
88132
89449
  var mintAgentUiUnlockInputSchema = zod.z.object({
88133
89450
  nodeId: zod.z.string().min(1),
@@ -88280,7 +89597,7 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
88280
89597
  setProcessMeshEnforcer(null);
88281
89598
  setProcessMeshCertificateRegistry(null);
88282
89599
  setProcessMeshTlsListenerState(null);
88283
- require_agent_https_names.setProcessAgentHttpsNames(null);
89600
+ require_agent_https.setProcessAgentHttpsNames(null);
88284
89601
  this.stoppedMesh = true;
88285
89602
  if (this.meshReapTimer !== null) clearInterval(this.meshReapTimer);
88286
89603
  this.meshReapTimer = null;
@@ -88333,12 +89650,12 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
88333
89650
  async loadAgentHttpsNames(store) {
88334
89651
  try {
88335
89652
  await store.declareCollection.mutate({
88336
- collection: require_agent_https_names.AGENT_HTTPS_NAMES_COLLECTION,
88337
- columns: require_agent_https_names.AGENT_HTTPS_NAMES_COLUMNS
89653
+ collection: require_agent_https.AGENT_HTTPS_NAMES_COLLECTION,
89654
+ columns: require_agent_https.AGENT_HTTPS_NAMES_COLUMNS
88338
89655
  });
88339
- const names = new require_agent_https_names.AgentHttpsNames(store);
89656
+ const names = new require_agent_https.AgentHttpsNames(store);
88340
89657
  await names.load();
88341
- require_agent_https_names.setProcessAgentHttpsNames(names);
89658
+ require_agent_https.setProcessAgentHttpsNames(names);
88342
89659
  } catch (err) {
88343
89660
  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
89661
  }
@@ -88388,7 +89705,7 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
88388
89705
  ca,
88389
89706
  hubUrl,
88390
89707
  hubMeshAddress,
88391
- ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
89708
+ nodeId: input.nodeId,
88392
89709
  ...input.ttlSec !== void 0 ? { ttlSec: input.ttlSec } : {}
88393
89710
  });
88394
89711
  return {
@@ -88397,7 +89714,7 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
88397
89714
  hubUrl,
88398
89715
  hubMeshAddress,
88399
89716
  caFingerprint: caFingerprintCompact(ca.certPem),
88400
- ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {}
89717
+ nodeId: input.nodeId
88401
89718
  };
88402
89719
  }
88403
89720
  /** D821 — short-lived ticket for the agent's `:4444` unlock session. */
@@ -88409,6 +89726,8 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
88409
89726
  if (input.dashboardBaseUrl !== void 0) {
88410
89727
  const origins = await this.readAgentDashboardOrigins();
88411
89728
  if (!isAllowedAgentDashboardReturnTo(input.dashboardBaseUrl, origins)) throw new Error("dashboardBaseUrl is not an allowed agent dashboard origin (:4444 or agentDashboardOrigins)");
89729
+ const known = require_agent_https.getProcessAgentHttpsNames()?.namesOf(input.nodeId) ?? null;
89730
+ 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
89731
  }
88413
89732
  const ticket = mintAgentUiUnlockTicket({
88414
89733
  secretHash: require_constants$8.hashClusterSecret(entry.value),
@@ -88687,13 +90006,13 @@ var SystemConfigAddon = class extends require_dist.BaseAddon {
88687
90006
  }
88688
90007
  };
88689
90008
  function parseMintJoinTokenInput(input) {
88690
- if (input === null || input === void 0 || typeof input !== "object") return {};
88691
- const out = {};
90009
+ if (input === null || input === void 0 || typeof input !== "object") throw new Error("system-config: mintJoinToken requires { nodeId }");
88692
90010
  const nodeId = Reflect.get(input, "nodeId");
90011
+ if (typeof nodeId !== "string" || nodeId.length === 0) throw new Error("system-config: mintJoinToken requires { nodeId } - the node the token is for");
90012
+ const out = { nodeId };
88693
90013
  const hubUrl = Reflect.get(input, "hubUrl");
88694
90014
  const hubMeshAddress = Reflect.get(input, "hubMeshAddress");
88695
90015
  const ttlSec = Reflect.get(input, "ttlSec");
88696
- if (typeof nodeId === "string") out.nodeId = nodeId;
88697
90016
  if (typeof hubUrl === "string") out.hubUrl = hubUrl;
88698
90017
  if (typeof hubMeshAddress === "string") out.hubMeshAddress = hubMeshAddress;
88699
90018
  if (typeof ttlSec === "number") out.ttlSec = ttlSec;
@@ -88723,8 +90042,12 @@ function readJwtExp(token) {
88723
90042
  exports.AGENT_DASHBOARD_ORIGINS_FIELD = AGENT_DASHBOARD_ORIGINS_FIELD;
88724
90043
  exports.AGENT_UI_UNLOCK_DOMAIN = AGENT_UI_UNLOCK_DOMAIN;
88725
90044
  exports.AGENT_UI_UNLOCK_TTL_MS = AGENT_UI_UNLOCK_TTL_MS;
90045
+ exports.AGENT_UI_UNLOCK_TYP = AGENT_UI_UNLOCK_TYP;
90046
+ exports.AGENT_UI_UNLOCK_USE = AGENT_UI_UNLOCK_USE;
88726
90047
  exports.AGENT_UI_UNLOCK_VERSION = AGENT_UI_UNLOCK_VERSION;
90048
+ exports.AgentPairing = AgentPairing;
88727
90049
  exports.Errors = Errors;
90050
+ exports.HubPairing = HubPairing;
88728
90051
  exports.JoinTokenJtiStore = JoinTokenJtiStore;
88729
90052
  exports.MESH_AUTH_MODE_FIELD = MESH_AUTH_MODE_FIELD;
88730
90053
  exports.MESH_NODE_CERTIFICATES_COLLECTION = MESH_NODE_CERTIFICATES_COLLECTION;
@@ -88733,6 +90056,14 @@ exports.MESH_TLS_PORT_HELLO_FIELD = MESH_TLS_PORT_HELLO_FIELD;
88733
90056
  exports.MeshCaKeeper = MeshCaKeeper;
88734
90057
  exports.MeshCertificateRegistry = MeshCertificateRegistry;
88735
90058
  exports.MeshCrl = MeshCrl;
90059
+ exports.PAIRING_CODE_DIGITS = PAIRING_CODE_DIGITS;
90060
+ exports.PAIRING_MAX_CODE_ATTEMPTS = PAIRING_MAX_CODE_ATTEMPTS;
90061
+ exports.PAIRING_MAX_SESSIONS = PAIRING_MAX_SESSIONS;
90062
+ exports.PAIRING_PATHS = PAIRING_PATHS;
90063
+ exports.PAIRING_PIN_DIGITS = PAIRING_PIN_DIGITS;
90064
+ exports.PAIRING_PROTOCOL = PAIRING_PROTOCOL;
90065
+ exports.PAIRING_TTL_MS = PAIRING_TTL_MS;
90066
+ exports.PairingHttpError = PairingHttpError;
88736
90067
  exports.RENEWAL_ABANDONED = RENEWAL_ABANDONED;
88737
90068
  exports.SYSTEM_CONFIG_CLUSTER_SECTION_ID = SYSTEM_CONFIG_CLUSTER_SECTION_ID;
88738
90069
  exports.ServiceBroker = ServiceBroker;
@@ -88743,8 +90074,11 @@ exports.Transporters = Transporters;
88743
90074
  exports.UnlockTicketJtiStore = UnlockTicketJtiStore;
88744
90075
  exports.caFingerprint = caFingerprint;
88745
90076
  exports.caFingerprintCompact = caFingerprintCompact;
90077
+ exports.computePairingCommitment = computePairingCommitment;
90078
+ exports.computePairingSas = computePairingSas;
88746
90079
  exports.createCidrMatcher = createCidrMatcher;
88747
90080
  exports.createMeshCsrPem = createMeshCsrPem;
90081
+ exports.describeCsr = describeCsr;
88748
90082
  exports.describeMeshLeaf = describeMeshLeaf;
88749
90083
  exports.dropProcessPeer = dropProcessPeer;
88750
90084
  exports.emitProcessNodeRegistered = emitProcessNodeRegistered;
@@ -88759,12 +90093,17 @@ exports.getProcessMeshCertificateRegistry = getProcessMeshCertificateRegistry;
88759
90093
  exports.getProcessMeshTlsListenerState = getProcessMeshTlsListenerState;
88760
90094
  exports.getProcessNodeMeshTransport = getProcessNodeMeshTransport;
88761
90095
  exports.getProcessPeerLeaf = getProcessPeerLeaf;
90096
+ exports.hubHostFromMeshAddress = hubHostFromMeshAddress;
88762
90097
  exports.isAllowedAgentDashboardReturnTo = isAllowedAgentDashboardReturnTo;
88763
90098
  exports.isMeshAuthMode = isMeshAuthMode;
90099
+ exports.isProcessVerifiedMeshPeer = isProcessVerifiedMeshPeer;
90100
+ exports.isValidAgentNodeId = isValidAgentNodeId;
88764
90101
  exports.issueMeshLeafForNode = issueMeshLeafForNode;
88765
90102
  exports.issueMeshLeafFromCsr = issueMeshLeafFromCsr;
90103
+ exports.keyFingerprintShort = keyFingerprintShort;
88766
90104
  exports.loadMeshCa = loadMeshCa;
88767
90105
  exports.mintAgentUiUnlockTicket = mintAgentUiUnlockTicket;
90106
+ exports.mintAgentUiUnlockTicketEs256 = mintAgentUiUnlockTicketEs256;
88768
90107
  exports.mintMeshJoinToken = mintMeshJoinToken;
88769
90108
  exports.normalizeIp = normalizeIp;
88770
90109
  exports.onProcessNodeRegistered = onProcessNodeRegistered;
@@ -88786,6 +90125,9 @@ exports.setProcessMeshEnforcer = setProcessMeshEnforcer;
88786
90125
  exports.setProcessMeshPeerTransportLookup = setProcessMeshPeerTransportLookup;
88787
90126
  exports.setProcessMeshTlsEnabler = setProcessMeshTlsEnabler;
88788
90127
  exports.setProcessMeshTlsListenerState = setProcessMeshTlsListenerState;
90128
+ exports.signPairingProof = signPairingProof;
88789
90129
  exports.systemConfigMeshActions = systemConfigMeshActions;
88790
90130
  exports.verifyAgentUiUnlockTicket = verifyAgentUiUnlockTicket;
90131
+ exports.verifyAgentUiUnlockTicketEs256 = verifyAgentUiUnlockTicketEs256;
88791
90132
  exports.verifyMeshJoinToken = verifyMeshJoinToken;
90133
+ exports.verifyPairingProof = verifyPairingProof;