@camstack/server 1.2.577 → 1.2.579
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/agent/agent-config.js +2 -11
- package/dist/agent/agent-http-auth.js +91 -78
- package/dist/agent/agent-http.js +89 -139
- package/dist/agent/agent-hub-registration.js +31 -58
- package/dist/agent/agent-json-file.js +59 -0
- package/dist/agent/agent-mesh-gate.js +57 -0
- package/dist/agent/agent-node-certificates.js +7 -7
- package/dist/agent/agent-pairing-routes.js +1 -4
- package/dist/agent/agent-pairing-store.js +8 -8
- package/dist/agent/agent-service.js +0 -9
- package/dist/agent/main.js +41 -99
- package/dist/api/core/certificates.router.js +2 -2
- package/dist/api/oauth2/consent-page.js +19 -31
- package/dist/api/trpc/trpc.context.js +2 -3
- package/dist/auth/app-confirm-page.js +29 -21
- package/dist/auth/app-page-shell.js +120 -0
- package/dist/auth/app-passkey-page.js +24 -19
- package/dist/auth/app-refused-route.js +37 -0
- package/dist/boot/boot-config.js +7 -5
- package/dist/core/agent/agent-registry.service.js +0 -10
- package/dist/core/moleculer/hub-mesh-guard.js +30 -101
- package/dist/core/moleculer/moleculer.service.js +8 -32
- package/dist/main.js +2 -2
- package/package.json +9 -9
- package/dist/agent/agent-mesh-enroll.js +0 -317
- package/dist/agent/agent-mesh-join.js +0 -140
- package/dist/api/mesh/enroll.routes.js +0 -34
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAgentMeshGate = createAgentMeshGate;
|
|
4
|
+
/**
|
|
5
|
+
* The agent's mesh gate (D802, D857): only the hub may call this agent, and the
|
|
6
|
+
* hub is recognised by its CERTIFICATE — the agent's own TLS listener accepted
|
|
7
|
+
* it only because its leaf chains to the mesh CA this agent was paired with
|
|
8
|
+
* (`TlsTcpTransporter.verifiedPeer`).
|
|
9
|
+
*
|
|
10
|
+
* There is no join to run and nothing to hold: a verdict is a function of what
|
|
11
|
+
* the transport already knows, so the hub's very first packet on a new
|
|
12
|
+
* connection is judged correctly. An agent that is not paired holds no
|
|
13
|
+
* certificate, so it admits no one.
|
|
14
|
+
*
|
|
15
|
+
* Also here: the agent's outbound event scope (D848) — its events go to the hub only.
|
|
16
|
+
*/
|
|
17
|
+
const system_1 = require("@camstack/system");
|
|
18
|
+
const HUB_NODE_ID = 'hub';
|
|
19
|
+
/** At most one "refused" line per sender per this long. */
|
|
20
|
+
const REFUSED_LOG_INTERVAL_MS = 60_000;
|
|
21
|
+
function createAgentMeshGate(opts) {
|
|
22
|
+
const lastLoggedAt = new Map();
|
|
23
|
+
const gate = new system_1.AgentMeshIdentityGate({
|
|
24
|
+
ownNodeId: opts.nodeId,
|
|
25
|
+
hubNodeId: HUB_NODE_ID,
|
|
26
|
+
isVerifiedPeer: opts.isVerifiedPeer,
|
|
27
|
+
onRefused: (nodeId, what) => {
|
|
28
|
+
const at = Date.now();
|
|
29
|
+
const last = lastLoggedAt.get(nodeId);
|
|
30
|
+
if (last !== undefined && at - last < REFUSED_LOG_INTERVAL_MS)
|
|
31
|
+
return;
|
|
32
|
+
if (lastLoggedAt.size >= 64)
|
|
33
|
+
lastLoggedAt.clear();
|
|
34
|
+
lastLoggedAt.set(nodeId, at);
|
|
35
|
+
opts.log?.(`mesh inbound: "${nodeId}" is not the certified hub — refused (first call: ${what})`);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const eventScope = (0, system_1.createAgentEventScope)({
|
|
39
|
+
hubNodeId: HUB_NODE_ID,
|
|
40
|
+
...(opts.log !== undefined ? { log: opts.log } : {}),
|
|
41
|
+
});
|
|
42
|
+
const inbound = (0, system_1.createMeshAccessMiddleware)({
|
|
43
|
+
ownNodeId: opts.nodeId,
|
|
44
|
+
gate,
|
|
45
|
+
logger: {
|
|
46
|
+
warn: (message, meta) => {
|
|
47
|
+
const detail = meta === undefined
|
|
48
|
+
? ''
|
|
49
|
+
: ` ${Object.entries(meta)
|
|
50
|
+
.map(([k, v]) => `${k}=${String(v)}`)
|
|
51
|
+
.join(' ')}`;
|
|
52
|
+
opts.log?.(`${message}${detail}`);
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
return { gate, eventScope, middlewares: [inbound, eventScope.middleware] };
|
|
57
|
+
}
|
|
@@ -26,7 +26,7 @@ exports.createAgentNodeCertificates = createAgentNodeCertificates;
|
|
|
26
26
|
*/
|
|
27
27
|
const node_crypto_1 = require("node:crypto");
|
|
28
28
|
const system_1 = require("@camstack/system");
|
|
29
|
-
const
|
|
29
|
+
const agent_json_file_js_1 = require("./agent-json-file.js");
|
|
30
30
|
/** Let the capability answer before the broker is torn down and rebuilt. */
|
|
31
31
|
const RECONNECT_DELAY_MS = 400;
|
|
32
32
|
/** A swap found pending at boot gets at least this long for the hub to confirm. */
|
|
@@ -57,7 +57,7 @@ function createAgentNodeCertificates(deps) {
|
|
|
57
57
|
let pendingKeyPem = null;
|
|
58
58
|
let rollbackTimer = null;
|
|
59
59
|
const readMesh = () => {
|
|
60
|
-
const raw = (0,
|
|
60
|
+
const raw = (0, agent_json_file_js_1.readJsonFile)(deps.configPath);
|
|
61
61
|
return {
|
|
62
62
|
current: isMeshMaterial(raw['meshTls']) ? raw['meshTls'] : null,
|
|
63
63
|
previous: isMeshMaterial(raw['meshTlsPrevious']) ? raw['meshTlsPrevious'] : null,
|
|
@@ -74,9 +74,9 @@ function createAgentNodeCertificates(deps) {
|
|
|
74
74
|
};
|
|
75
75
|
/** Write `agent.json` with the swap leftovers removed and `meshTls` optionally replaced. */
|
|
76
76
|
const writeSettled = (replaceCurrent) => {
|
|
77
|
-
const raw = (0,
|
|
77
|
+
const raw = (0, agent_json_file_js_1.readJsonFile)(deps.configPath);
|
|
78
78
|
const { meshTlsPrevious: _p, meshSwap: _s, ...rest } = raw;
|
|
79
|
-
(0,
|
|
79
|
+
(0, agent_json_file_js_1.writeJsonFile0600)(deps.configPath, {
|
|
80
80
|
...rest,
|
|
81
81
|
...(replaceCurrent !== undefined ? { meshTls: replaceCurrent } : {}),
|
|
82
82
|
});
|
|
@@ -151,7 +151,7 @@ function createAgentNodeCertificates(deps) {
|
|
|
151
151
|
return;
|
|
152
152
|
}
|
|
153
153
|
writeSettled();
|
|
154
|
-
log.error('No valid mesh leaf on this node: renew it from the hub or
|
|
154
|
+
log.error('No valid mesh leaf on this node: renew it from the hub or pair it again', {
|
|
155
155
|
tags,
|
|
156
156
|
});
|
|
157
157
|
};
|
|
@@ -222,12 +222,12 @@ function createAgentNodeCertificates(deps) {
|
|
|
222
222
|
if (!leaf.checkPrivateKey((0, node_crypto_1.createPrivateKey)(pendingKeyPem))) {
|
|
223
223
|
throw new Error('the new leaf does not match the key this node generated');
|
|
224
224
|
}
|
|
225
|
-
const raw = (0,
|
|
225
|
+
const raw = (0, agent_json_file_js_1.readJsonFile)(deps.configPath);
|
|
226
226
|
const swap = {
|
|
227
227
|
fingerprint: leaf.fingerprint256,
|
|
228
228
|
deadline: now() + input.confirmWithinMs,
|
|
229
229
|
};
|
|
230
|
-
(0,
|
|
230
|
+
(0, agent_json_file_js_1.writeJsonFile0600)(deps.configPath, {
|
|
231
231
|
...raw,
|
|
232
232
|
meshTls: { certPem: input.certPem, keyPem: pendingKeyPem, caCertPem: current.caCertPem },
|
|
233
233
|
meshTlsPrevious: current,
|
|
@@ -9,10 +9,7 @@ const DEFAULT_OFFERS_PER_MINUTE = 10;
|
|
|
9
9
|
const MAX_TRACKED_SOURCES = 512;
|
|
10
10
|
/** True for a request from this machine itself: loopback peer, no forwarding header. */
|
|
11
11
|
function isLocalRequest(req) {
|
|
12
|
-
return (0, agent_http_auth_js_1.isGenuineLoopback)(
|
|
13
|
-
remoteAddress: req.socket.remoteAddress ?? undefined,
|
|
14
|
-
forwarded: (0, agent_http_auth_js_1.hasForwardingHeader)(req.headers),
|
|
15
|
-
});
|
|
12
|
+
return (0, agent_http_auth_js_1.isGenuineLoopback)((0, agent_http_auth_js_1.authInputOf)(req));
|
|
16
13
|
}
|
|
17
14
|
function send(reply, result) {
|
|
18
15
|
if (result.ok)
|
|
@@ -46,12 +46,12 @@ exports.pairedLeafFingerprint = pairedLeafFingerprint;
|
|
|
46
46
|
const node_crypto_1 = require("node:crypto");
|
|
47
47
|
const fs = __importStar(require("node:fs"));
|
|
48
48
|
const system_1 = require("@camstack/system");
|
|
49
|
-
const
|
|
49
|
+
const agent_json_file_js_1 = require("./agent-json-file.js");
|
|
50
50
|
/** Keys that exist only while this agent is paired. */
|
|
51
51
|
const PAIRED_KEYS = ['meshTls', 'meshTlsPrevious', 'meshSwap', 'hubAddress'];
|
|
52
52
|
/** `agent.json` holds mesh material: the agent is paired. */
|
|
53
53
|
function isAgentPaired(configPath) {
|
|
54
|
-
const meshTls = (0,
|
|
54
|
+
const meshTls = (0, agent_json_file_js_1.readJsonFile)(configPath)['meshTls'];
|
|
55
55
|
return (meshTls !== null &&
|
|
56
56
|
typeof meshTls === 'object' &&
|
|
57
57
|
typeof Reflect.get(meshTls, 'certPem') === 'string' &&
|
|
@@ -60,7 +60,7 @@ function isAgentPaired(configPath) {
|
|
|
60
60
|
}
|
|
61
61
|
/** The CA certificate this agent pinned at pairing, or `null` while unpaired. */
|
|
62
62
|
function pinnedMeshCaCert(configPath) {
|
|
63
|
-
const meshTls = (0,
|
|
63
|
+
const meshTls = (0, agent_json_file_js_1.readJsonFile)(configPath)['meshTls'];
|
|
64
64
|
if (meshTls === null || typeof meshTls !== 'object')
|
|
65
65
|
return null;
|
|
66
66
|
const ca = Reflect.get(meshTls, 'caCertPem');
|
|
@@ -68,8 +68,8 @@ function pinnedMeshCaCert(configPath) {
|
|
|
68
68
|
}
|
|
69
69
|
/** Persist a completed pairing: the leaf, its key, the pinned CA and the hub's host. */
|
|
70
70
|
function persistPairing(configPath, pairing) {
|
|
71
|
-
const {
|
|
72
|
-
(0,
|
|
71
|
+
const { meshTlsPrevious: _previous, meshSwap: _swap, ...existing } = (0, agent_json_file_js_1.readJsonFile)(configPath);
|
|
72
|
+
(0, agent_json_file_js_1.writeJsonFile0600)(configPath, {
|
|
73
73
|
...existing,
|
|
74
74
|
hubAddress: pairing.hubAddress,
|
|
75
75
|
meshTls: pairing.meshTls,
|
|
@@ -80,8 +80,8 @@ function persistPairing(configPath, pairing) {
|
|
|
80
80
|
* the hub issued. The agent is unpaired afterwards and accepts offers again.
|
|
81
81
|
*/
|
|
82
82
|
function forgetPairing(configPath, dataDir) {
|
|
83
|
-
const existing = (0,
|
|
84
|
-
(0,
|
|
83
|
+
const existing = (0, agent_json_file_js_1.readJsonFile)(configPath);
|
|
84
|
+
(0, agent_json_file_js_1.writeJsonFile0600)(configPath, Object.fromEntries(Object.entries(existing).filter(([key]) => !PAIRED_KEYS.includes(key))));
|
|
85
85
|
const https = (0, system_1.agentHttpsPaths)(dataDir);
|
|
86
86
|
for (const file of [https.bundlePath, https.certPath, https.pendingKeyPath]) {
|
|
87
87
|
fs.rmSync(file, { force: true });
|
|
@@ -89,7 +89,7 @@ function forgetPairing(configPath, dataDir) {
|
|
|
89
89
|
}
|
|
90
90
|
/** First 16 hex chars of the SHA-256 of the agent's mesh leaf, `null` while unpaired. */
|
|
91
91
|
function pairedLeafFingerprint(configPath) {
|
|
92
|
-
const meshTls = (0,
|
|
92
|
+
const meshTls = (0, agent_json_file_js_1.readJsonFile)(configPath)['meshTls'];
|
|
93
93
|
const certPem = meshTls !== null && typeof meshTls === 'object' ? Reflect.get(meshTls, 'certPem') : null;
|
|
94
94
|
if (typeof certPem !== 'string')
|
|
95
95
|
return null;
|
|
@@ -607,15 +607,6 @@ function createAgentService(deps) {
|
|
|
607
607
|
registerNode: {
|
|
608
608
|
handler(ctx) {
|
|
609
609
|
const { params } = ctx;
|
|
610
|
-
// Cluster-secret gate: when the agent has a secret configured, a child
|
|
611
|
-
// runner must present a matching hash or the registration is rejected.
|
|
612
|
-
if (!(0, system_1.clusterSecretMatches)(deps.expectedClusterSecretHash, params.clusterSecretHash)) {
|
|
613
|
-
throw new moleculer_1.Errors.MoleculerError(`cluster secret mismatch — node "${params.nodeId}" rejected`, 403, system_1.CLUSTER_SECRET_MISMATCH_TYPE);
|
|
614
|
-
}
|
|
615
|
-
const sender = typeof ctx.nodeID === 'string' ? ctx.nodeID : '';
|
|
616
|
-
if (sender.length > 0) {
|
|
617
|
-
deps.meshAuth?.noteChildRegistered(sender, deps.expectedClusterSecretHash !== undefined ? 'authenticated' : 'legacy');
|
|
618
|
-
}
|
|
619
610
|
deps.onChildRegistered?.(params);
|
|
620
611
|
return { ok: true };
|
|
621
612
|
},
|
package/dist/agent/main.js
CHANGED
|
@@ -45,11 +45,10 @@ const single_copy_cleanup_runner_js_1 = require("../single-copy-cleanup-runner.j
|
|
|
45
45
|
const addon_scan_js_1 = require("./addon-scan.js");
|
|
46
46
|
const agent_addon_updates_js_1 = require("./agent-addon-updates.js");
|
|
47
47
|
const agent_hub_registration_js_1 = require("./agent-hub-registration.js");
|
|
48
|
-
const
|
|
48
|
+
const agent_mesh_gate_js_1 = require("./agent-mesh-gate.js");
|
|
49
49
|
const agent_pairing_local_js_1 = require("./agent-pairing-local.js");
|
|
50
50
|
const agent_pairing_store_js_1 = require("./agent-pairing-store.js");
|
|
51
51
|
const agent_config_js_1 = require("./agent-config.js");
|
|
52
|
-
const agent_mesh_enroll_js_1 = require("./agent-mesh-enroll.js");
|
|
53
52
|
const agent_group_runner_js_1 = require("./agent-group-runner.js");
|
|
54
53
|
const agent_http_js_1 = require("./agent-http.js");
|
|
55
54
|
const agent_node_certificates_js_1 = require("./agent-node-certificates.js");
|
|
@@ -88,32 +87,8 @@ const DATA_NODEID_CAPS = new Set(types_1.ALL_CAPABILITY_DEFINITIONS.filter((c) =
|
|
|
88
87
|
// ---------------------------------------------------------------------------
|
|
89
88
|
// Main
|
|
90
89
|
// ---------------------------------------------------------------------------
|
|
91
|
-
/**
|
|
92
|
-
* Load `agent.json`, first redeeming a join token the setup window left there
|
|
93
|
-
* (mesh mTLS Release A — the desktop setup runs before the agent does). A token
|
|
94
|
-
* is single-use, so it is dropped whether or not the hub accepted it; a refusal
|
|
95
|
-
* is logged loudly and the agent still boots (clear, or with the cluster secret).
|
|
96
|
-
*/
|
|
97
|
-
async function loadAgentConfigRedeemingJoinToken(configPath) {
|
|
98
|
-
const loaded = (0, agent_config_js_1.loadAgentConfig)(configPath);
|
|
99
|
-
if (loaded.pendingJoinToken === undefined)
|
|
100
|
-
return loaded;
|
|
101
|
-
try {
|
|
102
|
-
const enrolled = await (0, agent_mesh_enroll_js_1.enrollAndPersist)({
|
|
103
|
-
token: loaded.pendingJoinToken,
|
|
104
|
-
nodeId: loaded.nodeId,
|
|
105
|
-
configPath: loaded.configPath,
|
|
106
|
-
});
|
|
107
|
-
console.log(`[Agent] Enrolled in the mesh CA (hub ${enrolled.hubAddress}) — dialing over mutual TLS`);
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
console.error(`[Agent] Join-token enrollment failed — staying on the previous transport: ${(0, types_1.errMsg)(err)}`);
|
|
111
|
-
(0, agent_mesh_enroll_js_1.discardPendingJoinToken)(loaded.configPath);
|
|
112
|
-
}
|
|
113
|
-
return (0, agent_config_js_1.loadAgentConfig)(configPath, loaded.dataDir);
|
|
114
|
-
}
|
|
115
90
|
async function startAgent(configPath) {
|
|
116
|
-
const config =
|
|
91
|
+
const config = (0, agent_config_js_1.loadAgentConfig)(configPath);
|
|
117
92
|
// Derive CAMSTACK_HUB_URL from the configured hub address so remote agents
|
|
118
93
|
// no longer need a second, redundant env var set by hand (the cross-node
|
|
119
94
|
// source trap). An explicit operator value ALWAYS wins — capture whether it
|
|
@@ -179,58 +154,31 @@ async function startAgent(configPath) {
|
|
|
179
154
|
}
|
|
180
155
|
}
|
|
181
156
|
console.log(`[Agent] Addon packages ready in ${config.addonsDir}`);
|
|
182
|
-
// D802: the mesh
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
let currentSecret = config.secret ?? null;
|
|
187
|
-
let joinState = 'pending';
|
|
188
|
-
const meshJoin = (0, agent_mesh_join_js_1.createAgentMeshJoin)({
|
|
157
|
+
// D802, D857: the inbound mesh gate. Only the hub may call this agent, and the hub is
|
|
158
|
+
// recognised by the certificate the agent's own TLS listener verified. The transport is
|
|
159
|
+
// read at call time because a reconnect replaces it.
|
|
160
|
+
const meshGate = (0, agent_mesh_gate_js_1.createAgentMeshGate)({
|
|
189
161
|
nodeId: config.nodeId,
|
|
190
|
-
secret: () => currentSecret,
|
|
191
|
-
// D857 (transition release): the hub is also accepted by its certificate, verified by this
|
|
192
|
-
// agent's own TLS listener, so a hub that stops answering the join still reaches this agent.
|
|
193
|
-
isPaired: () => (0, agent_pairing_store_js_1.isAgentPaired)(config.configPath),
|
|
194
162
|
isVerifiedPeer: (nodeId) => {
|
|
195
163
|
const tx = (0, system_1.currentBrokerTransporter)(broker).tx;
|
|
196
164
|
return tx instanceof system_1.TlsTcpTransporter && tx.verifiedPeer(nodeId) !== undefined;
|
|
197
165
|
},
|
|
198
|
-
call: (action, params, opts) => broker.call(action, params, opts),
|
|
199
166
|
log: (msg) => console.log(`[Agent] ${msg}`),
|
|
200
|
-
onState: (state) => {
|
|
201
|
-
joinState = state;
|
|
202
|
-
if (state === 'secret-mismatch' || state === 'hub-unverified') {
|
|
203
|
-
hubConnectionStateOverride = 'secret-mismatch';
|
|
204
|
-
consoleLogger.error(state === 'secret-mismatch'
|
|
205
|
-
? "mesh join refused: cluster secret mismatch — copy the cluster secret from the hub's System → Cluster page into this agent's setup"
|
|
206
|
-
: 'mesh join refused: the node answering as the hub could not prove the cluster secret — check the hub address and the secret');
|
|
207
|
-
}
|
|
208
|
-
else if (hubConnectionStateOverride === 'secret-mismatch') {
|
|
209
|
-
hubConnectionStateOverride = null;
|
|
210
|
-
}
|
|
211
|
-
// Every settled join re-sends the full manifest (D3, 2026-10-14): a join
|
|
212
|
-
// runs exactly when the hub's view of this node was reset.
|
|
213
|
-
registration.onJoinSettled(state);
|
|
214
|
-
if (state === 'no-secret') {
|
|
215
|
-
consoleLogger.warn("no cluster secret configured — the hub tolerates this agent only while its System → Cluster is in Transition. Copy the hub's cluster secret into this agent's setup");
|
|
216
|
-
}
|
|
217
|
-
},
|
|
218
167
|
});
|
|
219
|
-
// Mesh mTLS
|
|
220
|
-
// port;
|
|
168
|
+
// Mesh mTLS: with a certificate (pairing, D857) the broker dials the hub's TLS
|
|
169
|
+
// port; an unpaired agent has none and admits no mesh peer at all.
|
|
221
170
|
let currentMeshTls = config.meshTls ?? null;
|
|
222
171
|
// The HTTP/HTTPS listener on the agent port (D856); set once it is up.
|
|
223
172
|
let agentHttpListener = null;
|
|
224
|
-
console.log(`[Agent] Mesh transport: ${currentMeshTls !== null ? 'tls' : '
|
|
173
|
+
console.log(`[Agent] Mesh transport: ${currentMeshTls !== null ? 'tls (paired)' : 'none (unpaired)'}`);
|
|
225
174
|
const broker = (0, system_1.createBroker)({
|
|
226
175
|
nodeID: config.nodeId,
|
|
227
176
|
mode: 'agent',
|
|
228
177
|
hubAddress: config.hubAddress,
|
|
229
178
|
logLevel: config.logLevel,
|
|
230
|
-
middlewares:
|
|
179
|
+
middlewares: meshGate.middlewares,
|
|
231
180
|
...(currentMeshTls !== null ? { meshTls: currentMeshTls } : {}),
|
|
232
181
|
});
|
|
233
|
-
meshJoin.attach(broker);
|
|
234
182
|
/**
|
|
235
183
|
* Reconnect: reload the config from file and replace the broker's TRANSPORT (the new hub
|
|
236
184
|
* address, secret or mesh leaf) IN PLACE. The broker itself is never rebuilt: its services
|
|
@@ -268,11 +216,11 @@ async function startAgent(configPath) {
|
|
|
268
216
|
},
|
|
269
217
|
prepare: () => {
|
|
270
218
|
console.log('[Agent] Reconnecting with updated config...');
|
|
271
|
-
// Reload config from file (UI may have written
|
|
219
|
+
// Reload config from file (pairing, the UI or a local call may have written it)
|
|
272
220
|
const fresh = (0, agent_config_js_1.loadAgentConfig)(undefined, config.dataDir);
|
|
273
221
|
pendingConfig = fresh;
|
|
274
|
-
console.log(`[Agent] New config: hub=${fresh.hubAddress ?? 'none'},
|
|
275
|
-
`mesh=${fresh.meshTls !== undefined ? 'tls' : '
|
|
222
|
+
console.log(`[Agent] New config: hub=${fresh.hubAddress ?? 'none'}, ` +
|
|
223
|
+
`mesh=${fresh.meshTls !== undefined ? 'tls (paired)' : 'none (unpaired)'}`);
|
|
276
224
|
// Re-derive CAMSTACK_HUB_URL when the dashboard has just written a hubAddress. Explicit operator values
|
|
277
225
|
// still win via the boot-captured `hubUrlWasExplicit` flag.
|
|
278
226
|
// KNOWN LIMITATION (Stage-0): runners already spawned before this reconnect
|
|
@@ -284,7 +232,6 @@ async function startAgent(configPath) {
|
|
|
284
232
|
process.env['CAMSTACK_HUB_URL'] = freshHubUrl;
|
|
285
233
|
console.log(`[Agent] Derived CAMSTACK_HUB_URL=${freshHubUrl} from hub address "${fresh.hubAddress}"`);
|
|
286
234
|
}
|
|
287
|
-
currentSecret = fresh.secret ?? null;
|
|
288
235
|
currentMeshTls = fresh.meshTls ?? null;
|
|
289
236
|
},
|
|
290
237
|
build: () => {
|
|
@@ -299,8 +246,8 @@ async function startAgent(configPath) {
|
|
|
299
246
|
...(currentMeshTls !== null ? { meshTls: currentMeshTls } : {}),
|
|
300
247
|
}));
|
|
301
248
|
},
|
|
302
|
-
// The new wire's hub connection
|
|
303
|
-
//
|
|
249
|
+
// The new wire's hub connection re-sends the manifest (`registration.onHubConnected`):
|
|
250
|
+
// the subtree union, every child included (D3).
|
|
304
251
|
start: async () => {
|
|
305
252
|
await (0, system_1.connectBrokerTransport)(broker);
|
|
306
253
|
console.log('[Agent] Reconnected successfully');
|
|
@@ -318,12 +265,12 @@ async function startAgent(configPath) {
|
|
|
318
265
|
// Hub connection state tracking (for richer UI status)
|
|
319
266
|
// ---------------------------------------------------------------------------
|
|
320
267
|
// Tracks the last known discriminated state so the HTTP status endpoint
|
|
321
|
-
// can surface `
|
|
268
|
+
// can surface `certificate-rejected` which is not observable from the broker
|
|
322
269
|
// registry alone. All mutations are synchronous/single-threaded (Node.js
|
|
323
270
|
// event loop) so no locking is needed.
|
|
324
271
|
let hubConnectionStateOverride = null;
|
|
325
272
|
function getHubConnectionState() {
|
|
326
|
-
// If a definitive override is set (e.g.
|
|
273
|
+
// If a definitive override is set (e.g. certificate-rejected), return it
|
|
327
274
|
// regardless of registry state.
|
|
328
275
|
if (hubConnectionStateOverride !== null)
|
|
329
276
|
return hubConnectionStateOverride;
|
|
@@ -338,6 +285,8 @@ async function startAgent(configPath) {
|
|
|
338
285
|
catch {
|
|
339
286
|
/* fall through */
|
|
340
287
|
}
|
|
288
|
+
if (!(0, agent_pairing_store_js_1.isAgentPaired)(config.configPath))
|
|
289
|
+
return 'unpaired';
|
|
341
290
|
const configNow = readConfigFile(config.configPath);
|
|
342
291
|
const hubAddressConfigured = typeof configNow.hubAddress === 'string' && configNow.hubAddress.length > 0;
|
|
343
292
|
return hubAddressConfigured ? 'disconnected' : 'unconfigured';
|
|
@@ -423,12 +372,7 @@ async function startAgent(configPath) {
|
|
|
423
372
|
// carries the agent's root-package identity so the hub's HubNodeRegistry
|
|
424
373
|
// (and the Server management UI) sees every node's exact version.
|
|
425
374
|
const rootPackage = agentUpdateService.getRunningRootPackage();
|
|
426
|
-
return (0, system_1.buildNodeManifest)(config.nodeId, allAddons, allNativeCaps.length > 0 ? allNativeCaps : undefined,
|
|
427
|
-
// D802: the replayable D797 hash goes out ONLY to a hub that predates the
|
|
428
|
-
// challenge–response join. After a successful join nothing is sent.
|
|
429
|
-
joinState === 'unsupported' && currentSecret !== null && currentSecret.length > 0
|
|
430
|
-
? (0, system_1.hashClusterSecret)(currentSecret)
|
|
431
|
-
: undefined, rootPackage.version === null
|
|
375
|
+
return (0, system_1.buildNodeManifest)(config.nodeId, allAddons, allNativeCaps.length > 0 ? allNativeCaps : undefined, rootPackage.version === null
|
|
432
376
|
? undefined
|
|
433
377
|
: { name: rootPackage.name, version: rootPackage.version });
|
|
434
378
|
}
|
|
@@ -437,26 +381,29 @@ async function startAgent(configPath) {
|
|
|
437
381
|
* subtree union (`agent-hub-registration.ts`). Sent:
|
|
438
382
|
* - once boot has loaded every addon (`registration.ready()`);
|
|
439
383
|
* - on every `$agent.registerNode` from a child, and on `$agent.reload`;
|
|
440
|
-
* -
|
|
441
|
-
* saying it lost this node — and when the hub asks for it
|
|
384
|
+
* - every time the hub node connects, and when the hub asks for it
|
|
442
385
|
* (`$agent.requestRegistration`, on the hub's own `$node.connected`).
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
* hub that needs it.
|
|
386
|
+
* D857: no join to wait for and no secret in the manifest — the hub knows
|
|
387
|
+
* who this agent is from its certificate.
|
|
446
388
|
*/
|
|
447
389
|
const registration = (0, agent_hub_registration_js_1.createAgentHubRegistration)({
|
|
448
|
-
join: meshJoin,
|
|
449
390
|
buildManifest: () => aggregateManifest(),
|
|
450
391
|
broker: () => broker,
|
|
451
392
|
signal: registerAbortController.signal,
|
|
452
393
|
log: (msg) => console.log(`[Agent] ${msg}`),
|
|
453
394
|
// The hub acked the manifest — the agent reached its registered state.
|
|
454
395
|
// Run the single-copy GC sweep once (first ack only; no-op after).
|
|
455
|
-
onAcked: () =>
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
396
|
+
onAcked: () => {
|
|
397
|
+
if (hubConnectionStateOverride === 'certificate-rejected')
|
|
398
|
+
hubConnectionStateOverride = null;
|
|
399
|
+
confirmAgentBootHealthy();
|
|
400
|
+
},
|
|
401
|
+
onCertificateRefused: () => {
|
|
402
|
+
if (hubConnectionStateOverride === 'certificate-rejected')
|
|
403
|
+
return;
|
|
404
|
+
consoleLogger.error("the hub does not accept this agent's certificate (revoked, or issued by another CA) — pair it again: unpair it here, then pair it from the hub (System → Cluster → Agents)");
|
|
405
|
+
// Surface it in the UI status.
|
|
406
|
+
hubConnectionStateOverride = 'certificate-rejected';
|
|
460
407
|
},
|
|
461
408
|
});
|
|
462
409
|
const consoleLogger = {
|
|
@@ -615,8 +562,6 @@ async function startAgent(configPath) {
|
|
|
615
562
|
subtree.registerNode(params);
|
|
616
563
|
registration.trigger(`child ${params.nodeId} registered`);
|
|
617
564
|
},
|
|
618
|
-
expectedClusterSecretHash: config.secret ? (0, system_1.hashClusterSecret)(config.secret) : undefined,
|
|
619
|
-
meshAuth: meshJoin.authenticator,
|
|
620
565
|
installFromNpm: async (pkg, version) => {
|
|
621
566
|
await installer.install(pkg, version);
|
|
622
567
|
},
|
|
@@ -1003,8 +948,8 @@ async function startAgent(configPath) {
|
|
|
1003
948
|
// ── D3: Fire the initial upward hub registration carrying the agent's
|
|
1004
949
|
// complete in-process manifest (+ any children that already registered).
|
|
1005
950
|
// Group-runner children call `$agent.registerNode` and trigger a re-fire
|
|
1006
|
-
// via `onChildRegistered`; every
|
|
1007
|
-
// (`registration.
|
|
951
|
+
// via `onChildRegistered`; every hub connection re-fires it
|
|
952
|
+
// (`registration.onHubConnected`), and so does the hub's ask.
|
|
1008
953
|
registration.trigger('boot');
|
|
1009
954
|
// Fire `onHubReachable()` on every in-process addon as soon as the hub
|
|
1010
955
|
// node connects to our broker — this is the safe point for ctx.api.* calls
|
|
@@ -1015,13 +960,9 @@ async function startAgent(configPath) {
|
|
|
1015
960
|
const node = data.node;
|
|
1016
961
|
if (node?.id !== 'hub')
|
|
1017
962
|
return;
|
|
1018
|
-
// Hub connected —
|
|
1019
|
-
//
|
|
1020
|
-
|
|
1021
|
-
hubConnectionStateOverride = null;
|
|
1022
|
-
}
|
|
1023
|
-
// No re-registration here: this connection runs a mesh join, and the
|
|
1024
|
-
// join's settle re-sends the manifest (`registration.onJoinSettled`).
|
|
963
|
+
// Hub connected — its view of this agent was reset, so the full manifest goes again.
|
|
964
|
+
// (A stale `certificate-rejected` clears on the registration's ack.)
|
|
965
|
+
registration.onHubConnected();
|
|
1025
966
|
if (hubReachableFired)
|
|
1026
967
|
return;
|
|
1027
968
|
hubReachableFired = true;
|
|
@@ -1053,7 +994,8 @@ async function startAgent(configPath) {
|
|
|
1053
994
|
}
|
|
1054
995
|
}
|
|
1055
996
|
}
|
|
1056
|
-
//
|
|
997
|
+
// Withdraw the mDNS advertisement, then stop accepting new HTTP/HTTPS connections before
|
|
998
|
+
// the broker goes away.
|
|
1057
999
|
await agentMdns.stop().catch(() => undefined);
|
|
1058
1000
|
await agentHttpListener?.close().catch(() => undefined);
|
|
1059
1001
|
await broker.stop();
|
|
@@ -19,7 +19,7 @@ exports.createCertificatesRouter = createCertificatesRouter;
|
|
|
19
19
|
* recorded from the leaf it presents on its live connection (`observed`).
|
|
20
20
|
* Renewal is a handover that never leaves a node without a working leaf
|
|
21
21
|
* (`mesh-renewal.ts`); revocation puts the serial on the CRL and disconnects
|
|
22
|
-
* the node, which must
|
|
22
|
+
* the node, which must be paired again. Neither forgets a node:
|
|
23
23
|
* that stays `clusterNodes.forgetNode`.
|
|
24
24
|
*/
|
|
25
25
|
const node_crypto_1 = require("node:crypto");
|
|
@@ -473,7 +473,7 @@ function createCertificatesRouter(deps) {
|
|
|
473
473
|
}),
|
|
474
474
|
/**
|
|
475
475
|
* Put the node's mesh leaf on the CRL and disconnect it. The node can no longer
|
|
476
|
-
* connect: it must
|
|
476
|
+
* connect: it must be paired again. It is NOT forgotten - that stays
|
|
477
477
|
* `clusterNodes.forgetNode`.
|
|
478
478
|
*/
|
|
479
479
|
revokeMeshOnNode: trpc_middleware_js_1.adminProcedure
|
|
@@ -1,37 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.renderConsentPage = renderConsentPage;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* hidden query fields + `consent=allow|deny`. */
|
|
4
|
+
const app_page_shell_js_1 = require("../../auth/app-page-shell.js");
|
|
5
|
+
/** Allow/Deny consent screen on the hub's shared page shell (the look of the
|
|
6
|
+
* app sign-in pages). Submitting POSTs to the same path with the hidden query
|
|
7
|
+
* fields + `consent=allow|deny`. */
|
|
9
8
|
function renderConsentPage(input) {
|
|
10
9
|
const hidden = Object.entries(input.hidden)
|
|
11
|
-
.map(([
|
|
12
|
-
.join('
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
</
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<p class="meta">Signed in as <strong>${escapeHtml(input.username)}</strong></p>
|
|
27
|
-
<p class="meta">This will grant: ${escapeHtml(input.scopeSummary)}</p>${input.redirectUri
|
|
28
|
-
? `\n <p class="meta">Sending the authorization to <code>${escapeHtml(input.redirectUri)}</code></p>`
|
|
29
|
-
: ''}
|
|
30
|
-
<form method="POST">
|
|
31
|
-
${hidden}
|
|
32
|
-
<button class="allow" name="consent" value="allow" type="submit">Allow</button>
|
|
33
|
-
<button class="deny" name="consent" value="deny" type="submit">Deny</button>
|
|
34
|
-
</form>
|
|
35
|
-
</div>
|
|
36
|
-
</body></html>`;
|
|
10
|
+
.map(([name, value]) => (0, app_page_shell_js_1.appHiddenField)(name, value))
|
|
11
|
+
.join('');
|
|
12
|
+
const destination = input.redirectUri
|
|
13
|
+
? `<p class="hint">Sending the authorization to <code>${(0, app_page_shell_js_1.escapeHtml)(input.redirectUri)}</code></p>`
|
|
14
|
+
: '';
|
|
15
|
+
return (0, app_page_shell_js_1.renderAppPage)({
|
|
16
|
+
title: `CamStack · Authorize ${input.displayName}`,
|
|
17
|
+
heading: `Authorize ${input.displayName}`,
|
|
18
|
+
body: [
|
|
19
|
+
`<p>Signed in as <strong>${(0, app_page_shell_js_1.escapeHtml)(input.username)}</strong></p>`,
|
|
20
|
+
(0, app_page_shell_js_1.appHint)(`This will grant: ${input.scopeSummary}`),
|
|
21
|
+
destination,
|
|
22
|
+
`<form method="POST">${hidden}<div class="actions">${(0, app_page_shell_js_1.appSubmitButton)('Allow', 'consent', 'allow')}${(0, app_page_shell_js_1.appSubmitButton)('Deny', 'consent', 'deny', true)}</div></form>`,
|
|
23
|
+
].join(''),
|
|
24
|
+
});
|
|
37
25
|
}
|
|
@@ -320,9 +320,8 @@ function makeDeviceScopeLookup(addonRegistry) {
|
|
|
320
320
|
* `principal` names WHO is calling (D802): `mesh` for the hub's own runners
|
|
321
321
|
* (they reach the bridge over UDS, inside the hub's trust boundary), and
|
|
322
322
|
* `node:<nodeId>` for another node — whose id the hub's mesh gate has bound
|
|
323
|
-
* to
|
|
324
|
-
*
|
|
325
|
-
* is tolerated and its id is only what it claims).
|
|
323
|
+
* to its CERTIFICATE before the call got here (`createMeshAccessMiddleware`:
|
|
324
|
+
* the TLS handshake chained to the mesh CA, the leaf CN is that id, D857).
|
|
326
325
|
*
|
|
327
326
|
* Both principals are still admins: that keeps every `adminProcedure` an
|
|
328
327
|
* agent's addons call working. What is NOT reachable from another node is
|