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