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