@camstack/server 1.2.487 → 1.2.488
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-mesh-join.js +48 -7
- package/dist/agent/agent-service.js +4 -0
- package/dist/agent/main.js +3 -2
- package/dist/api/mesh/enroll.routes.js +28 -0
- package/dist/api/trpc/core-cap-bridge.js +42 -1
- package/dist/core/agent/agent-registry.service.js +13 -0
- package/dist/core/moleculer/moleculer.service.js +13 -0
- package/dist/main.js +5 -1
- package/package.json +4 -4
|
@@ -3,12 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createAgentMeshJoin = createAgentMeshJoin;
|
|
4
4
|
/**
|
|
5
5
|
* The agent's mesh join (D802): authenticate to the hub on every connection,
|
|
6
|
-
* before anything else reaches it, and check that the hub is the hub
|
|
6
|
+
* before anything else reaches it, and check that the hub is the hub — plus
|
|
7
|
+
* the agent's inbound mesh gate so only that proven hub (and this agent's own
|
|
8
|
+
* registered children) may call it.
|
|
7
9
|
*
|
|
8
|
-
* `MeshJoinClient` (in `@camstack/system`) holds the protocol and the
|
|
9
|
-
* gate;
|
|
10
|
-
*
|
|
11
|
-
* - the hub
|
|
10
|
+
* `MeshJoinClient` (in `@camstack/system`) holds the outbound protocol and the
|
|
11
|
+
* call gate; `AgentMeshAuthenticator` + `createMeshAccessMiddleware` hold the
|
|
12
|
+
* inbound side. This file binds both to the agent's broker lifecycle:
|
|
13
|
+
* - the hub connects → a join runs; a settled mutual proof stamps the hub;
|
|
14
|
+
* - the hub disconnects → back to `pending` (calls wait for the next join)
|
|
15
|
+
* and the hub is forgotten on the inbound gate;
|
|
16
|
+
* - any other node disconnects → forgotten on the inbound gate;
|
|
12
17
|
* - the hub says "not authenticated" (it restarted and lost us) → join again;
|
|
13
18
|
* - the join settles `unavailable` (the hub cannot read its secret yet) →
|
|
14
19
|
* retried every {@link REJOIN_INTERVAL_MS} while connected.
|
|
@@ -33,6 +38,18 @@ function createAgentMeshJoin(opts) {
|
|
|
33
38
|
clearTimeout(rejoinTimer);
|
|
34
39
|
rejoinTimer = null;
|
|
35
40
|
};
|
|
41
|
+
const authenticator = new system_1.AgentMeshAuthenticator({
|
|
42
|
+
ownNodeId: opts.nodeId,
|
|
43
|
+
hubNodeId: HUB_NODE_ID,
|
|
44
|
+
hasSecret: () => {
|
|
45
|
+
const s = opts.secret();
|
|
46
|
+
return s !== null && s.length > 0;
|
|
47
|
+
},
|
|
48
|
+
onTolerated: (nodeId, action) => {
|
|
49
|
+
opts.log?.(`mesh inbound: node "${nodeId}" is calling without having authenticated — ` +
|
|
50
|
+
`tolerated while this agent has no cluster secret (first call: ${action})`);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
36
53
|
const client = new system_1.MeshJoinClient({
|
|
37
54
|
nodeId: opts.nodeId,
|
|
38
55
|
secret: opts.secret,
|
|
@@ -40,6 +57,7 @@ function createAgentMeshJoin(opts) {
|
|
|
40
57
|
hubNodeId: HUB_NODE_ID,
|
|
41
58
|
...(opts.log !== undefined ? { log: opts.log } : {}),
|
|
42
59
|
onSettled: (state) => {
|
|
60
|
+
authenticator.noteHubJoinState(state);
|
|
43
61
|
opts.onState?.(state);
|
|
44
62
|
clearRejoin();
|
|
45
63
|
if (state === 'unavailable' && connected) {
|
|
@@ -55,11 +73,29 @@ function createAgentMeshJoin(opts) {
|
|
|
55
73
|
};
|
|
56
74
|
const rejoin = () => {
|
|
57
75
|
client.reset();
|
|
76
|
+
authenticator.noteHubJoinState('pending');
|
|
58
77
|
void runJoin();
|
|
59
78
|
};
|
|
79
|
+
const joinMiddleware = client.middleware(rejoin);
|
|
80
|
+
const inboundMiddleware = (0, system_1.createMeshAccessMiddleware)({
|
|
81
|
+
ownNodeId: opts.nodeId,
|
|
82
|
+
authenticator,
|
|
83
|
+
logger: {
|
|
84
|
+
warn: (message, meta) => {
|
|
85
|
+
const detail = meta === undefined
|
|
86
|
+
? ''
|
|
87
|
+
: ` ${Object.entries(meta)
|
|
88
|
+
.map(([k, v]) => `${k}=${String(v)}`)
|
|
89
|
+
.join(' ')}`;
|
|
90
|
+
opts.log?.(`${message}${detail}`);
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
});
|
|
60
94
|
return {
|
|
61
95
|
client,
|
|
62
|
-
|
|
96
|
+
authenticator,
|
|
97
|
+
middleware: joinMiddleware,
|
|
98
|
+
middlewares: [inboundMiddleware, joinMiddleware],
|
|
63
99
|
attach(broker) {
|
|
64
100
|
broker.localBus.on('$node.connected', (data) => {
|
|
65
101
|
if (nodeIdOf(data) !== HUB_NODE_ID)
|
|
@@ -68,11 +104,16 @@ function createAgentMeshJoin(opts) {
|
|
|
68
104
|
rejoin();
|
|
69
105
|
});
|
|
70
106
|
broker.localBus.on('$node.disconnected', (data) => {
|
|
71
|
-
|
|
107
|
+
const id = nodeIdOf(data);
|
|
108
|
+
if (id === undefined)
|
|
109
|
+
return;
|
|
110
|
+
authenticator.forget(id);
|
|
111
|
+
if (id !== HUB_NODE_ID)
|
|
72
112
|
return;
|
|
73
113
|
connected = false;
|
|
74
114
|
clearRejoin();
|
|
75
115
|
client.reset();
|
|
116
|
+
authenticator.noteHubJoinState('pending');
|
|
76
117
|
});
|
|
77
118
|
},
|
|
78
119
|
stop: clearRejoin,
|
|
@@ -546,6 +546,10 @@ function createAgentService(deps) {
|
|
|
546
546
|
if (!(0, system_1.clusterSecretMatches)(deps.expectedClusterSecretHash, params.clusterSecretHash)) {
|
|
547
547
|
throw new moleculer_1.Errors.MoleculerError(`cluster secret mismatch — node "${params.nodeId}" rejected`, 403, system_1.CLUSTER_SECRET_MISMATCH_TYPE);
|
|
548
548
|
}
|
|
549
|
+
const sender = typeof ctx.nodeID === 'string' ? ctx.nodeID : '';
|
|
550
|
+
if (sender.length > 0) {
|
|
551
|
+
deps.meshAuth?.noteChildRegistered(sender, deps.expectedClusterSecretHash !== undefined ? 'authenticated' : 'legacy');
|
|
552
|
+
}
|
|
549
553
|
deps.onChildRegistered?.(params);
|
|
550
554
|
return { ok: true };
|
|
551
555
|
},
|
package/dist/agent/main.js
CHANGED
|
@@ -182,7 +182,7 @@ async function startAgent(configPath) {
|
|
|
182
182
|
mode: 'agent',
|
|
183
183
|
hubAddress: config.hubAddress,
|
|
184
184
|
logLevel: config.logLevel,
|
|
185
|
-
middlewares:
|
|
185
|
+
middlewares: meshJoin.middlewares,
|
|
186
186
|
});
|
|
187
187
|
meshJoin.attach(broker);
|
|
188
188
|
/**
|
|
@@ -219,7 +219,7 @@ async function startAgent(configPath) {
|
|
|
219
219
|
mode: 'agent',
|
|
220
220
|
hubAddress: fresh.hubAddress,
|
|
221
221
|
logLevel: fresh.logLevel,
|
|
222
|
-
middlewares:
|
|
222
|
+
middlewares: meshJoin.middlewares,
|
|
223
223
|
});
|
|
224
224
|
meshJoin.attach(broker);
|
|
225
225
|
// The boot-time `$node.connected` listeners belong to the old broker: let
|
|
@@ -520,6 +520,7 @@ async function startAgent(configPath) {
|
|
|
520
520
|
triggerUpwardRegistration();
|
|
521
521
|
},
|
|
522
522
|
expectedClusterSecretHash: config.secret ? (0, system_1.hashClusterSecret)(config.secret) : undefined,
|
|
523
|
+
meshAuth: meshJoin.authenticator,
|
|
523
524
|
installFromNpm: async (pkg, version) => {
|
|
524
525
|
await installer.install(pkg, version);
|
|
525
526
|
},
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerMeshEnrollRoute = registerMeshEnrollRoute;
|
|
4
|
+
const system_1 = require("@camstack/system");
|
|
5
|
+
function registerMeshEnrollRoute(fastify) {
|
|
6
|
+
fastify.post('/api/mesh/enroll', async (request, reply) => {
|
|
7
|
+
const caState = (0, system_1.getProcessMeshCaState)();
|
|
8
|
+
if (caState.state !== 'ready') {
|
|
9
|
+
return reply.status(503).send({
|
|
10
|
+
ok: false,
|
|
11
|
+
error: `mesh CA unavailable: ${caState.reason}`,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
const body = request.body ?? {};
|
|
15
|
+
const result = await (0, system_1.enrollMeshNode)({ token: body.token ?? '', csr: body.csr ?? '' }, { ca: caState.ca });
|
|
16
|
+
if (!result.ok) {
|
|
17
|
+
return reply.status(result.status).send({ ok: false, error: result.error });
|
|
18
|
+
}
|
|
19
|
+
return reply.send({
|
|
20
|
+
ok: true,
|
|
21
|
+
cert: result.cert,
|
|
22
|
+
caCert: result.caCert,
|
|
23
|
+
nodeId: result.nodeId,
|
|
24
|
+
notAfter: result.notAfter,
|
|
25
|
+
serialNumber: result.serialNumber,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
* routes the call exactly like any addon-provided cap.
|
|
19
19
|
*/
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE = exports.REMOTE_DENIED_NAMESPACES = void 0;
|
|
21
|
+
exports.REMOTE_PRIVILEGE_LOG_ONLY = exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE = exports.REMOTE_DENIED_NAMESPACES = void 0;
|
|
22
|
+
exports.setRemotePrivilegeLogSink = setRemotePrivilegeLogSink;
|
|
23
|
+
exports.__resetRemotePrivilegeObservationForTests = __resetRemotePrivilegeObservationForTests;
|
|
24
|
+
exports.observeRemotePrivilege = observeRemotePrivilege;
|
|
22
25
|
exports.isCoreServiceCapName = isCoreServiceCapName;
|
|
23
26
|
exports.describeMeshOrigin = describeMeshOrigin;
|
|
24
27
|
exports.buildCoreCapService = buildCoreCapService;
|
|
@@ -72,6 +75,42 @@ exports.REMOTE_DENIED_NAMESPACES = new Set([
|
|
|
72
75
|
]);
|
|
73
76
|
/** Moleculer error `type` of a bridged call another node may not make. */
|
|
74
77
|
exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE = 'CORE_CAP_REMOTE_FORBIDDEN';
|
|
78
|
+
/**
|
|
79
|
+
* When true, a remote node's allowed bridged calls are logged once per
|
|
80
|
+
* `(nodeId, path)` per process — still admin, not refused. Granting least
|
|
81
|
+
* privilege waits on census; this is the log-only week before that.
|
|
82
|
+
*/
|
|
83
|
+
exports.REMOTE_PRIVILEGE_LOG_ONLY = true;
|
|
84
|
+
let remotePrivilegeLogSink = null;
|
|
85
|
+
/** Attach the warn sink used by {@link observeRemotePrivilege}. */
|
|
86
|
+
function setRemotePrivilegeLogSink(sink) {
|
|
87
|
+
remotePrivilegeLogSink = sink;
|
|
88
|
+
}
|
|
89
|
+
const remotePrivilegeObserved = new Set();
|
|
90
|
+
/** Clears dedupe state and the sink — tests only. */
|
|
91
|
+
function __resetRemotePrivilegeObservationForTests() {
|
|
92
|
+
remotePrivilegeObserved.clear();
|
|
93
|
+
remotePrivilegeLogSink = null;
|
|
94
|
+
}
|
|
95
|
+
function remotePrivilegeKey(nodeId, path) {
|
|
96
|
+
return `${nodeId}\0${path}`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* One warn per `(nodeId, path)` while {@link REMOTE_PRIVILEGE_LOG_ONLY} is on.
|
|
100
|
+
* Call only after {@link REMOTE_DENIED_NAMESPACES} has passed.
|
|
101
|
+
*/
|
|
102
|
+
function observeRemotePrivilege(nodeId, path) {
|
|
103
|
+
if (!exports.REMOTE_PRIVILEGE_LOG_ONLY)
|
|
104
|
+
return;
|
|
105
|
+
const key = remotePrivilegeKey(nodeId, path);
|
|
106
|
+
if (remotePrivilegeObserved.has(key))
|
|
107
|
+
return;
|
|
108
|
+
remotePrivilegeObserved.add(key);
|
|
109
|
+
const message = `mesh privilege: node:${nodeId} called ${path} (log-only; still allowed)`;
|
|
110
|
+
remotePrivilegeLogSink?.warn(message, {
|
|
111
|
+
meta: { nodeId, path, principal: `node:${nodeId}` },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
75
114
|
/**
|
|
76
115
|
* camelCase → kebab-case. Mirrors `toKebab` in `trpc-links.ts` so the
|
|
77
116
|
* action name matches what `brokerTransportLink` derives from `op.path`.
|
|
@@ -220,6 +259,8 @@ function buildCoreCapService(appRouter, census = (0, http_request_census_singlet
|
|
|
220
259
|
if (remoteNode !== null && exports.REMOTE_DENIED_NAMESPACES.has(namespace)) {
|
|
221
260
|
throw new moleculer_1.Errors.MoleculerClientError(`"${path}" is not callable from another node (caller "${remoteNode}")`, 403, exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE);
|
|
222
261
|
}
|
|
262
|
+
if (remoteNode !== null)
|
|
263
|
+
observeRemotePrivilege(remoteNode, path);
|
|
223
264
|
const invoke = resolveInvoker(callerFor(remoteNode === null ? 'mesh' : `node:${remoteNode}`), path);
|
|
224
265
|
if (invoke === null)
|
|
225
266
|
throw new Error(`$core-caps: "${path}" is not invocable`);
|
|
@@ -385,6 +385,19 @@ class AgentRegistryService {
|
|
|
385
385
|
tags: { nodeId },
|
|
386
386
|
meta: { historyOutcome, historyRemoved, registryRemoved, stillListed },
|
|
387
387
|
});
|
|
388
|
+
// Topology is push-only (`cluster.topology-snapshot`). Without this event
|
|
389
|
+
// the Cluster page kept the forgotten row until the 30 s heartbeat — the
|
|
390
|
+
// operator had to refresh by hand. `TopologyEmitterService` listens for
|
|
391
|
+
// `AgentUnregistered` and re-emits immediately.
|
|
392
|
+
if (historyRemoved || registryRemoved) {
|
|
393
|
+
this.eventBus.emit({
|
|
394
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
395
|
+
timestamp: new Date(),
|
|
396
|
+
source: { type: 'core', id: 'agent-registry' },
|
|
397
|
+
category: types_1.EventCategory.AgentUnregistered,
|
|
398
|
+
data: { agentId: nodeId },
|
|
399
|
+
});
|
|
400
|
+
}
|
|
388
401
|
return {
|
|
389
402
|
forgotten: historyRemoved || registryRemoved,
|
|
390
403
|
historyRemoved,
|
|
@@ -690,6 +690,18 @@ class MoleculerService {
|
|
|
690
690
|
probe: (url, options) => this.streamProbe.probe(url, options),
|
|
691
691
|
probeField: (key, value) => this.streamProbe.probeField(key, value),
|
|
692
692
|
}));
|
|
693
|
+
// Release A: when system-config's MeshCaKeeper becomes ready it offers
|
|
694
|
+
// hub leaf material here; the transporter starts TLS on :6443 alongside
|
|
695
|
+
// clear :6000 so enrolled agents can dual-dial without splitting the cluster.
|
|
696
|
+
(0, system_1.setProcessMeshTlsEnabler)(async (material) => {
|
|
697
|
+
const tx = (0, system_1.getBrokerTlsTransporter)(this.broker);
|
|
698
|
+
if (tx === null) {
|
|
699
|
+
logger.warn('Mesh TLS material ready but broker transporter is not TlsTcpTransporter');
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
await tx.enableMeshTls(material);
|
|
703
|
+
logger.info('Mesh mTLS listener enabled (dual-port with clear TCP)');
|
|
704
|
+
});
|
|
693
705
|
await this.brokerSafe.start();
|
|
694
706
|
logger.info('Moleculer broker started (TCP transport)');
|
|
695
707
|
this.meshGuard.start(() => this.listAvailableNodeIds());
|
|
@@ -1322,6 +1334,7 @@ class MoleculerService {
|
|
|
1322
1334
|
}
|
|
1323
1335
|
}
|
|
1324
1336
|
async onModuleDestroy() {
|
|
1337
|
+
(0, system_1.setProcessMeshTlsEnabler)(null);
|
|
1325
1338
|
this.meshGuard.stop();
|
|
1326
1339
|
if (this.agentReadinessPullTimer !== null) {
|
|
1327
1340
|
clearInterval(this.agentReadinessPullTimer);
|
package/dist/main.js
CHANGED
|
@@ -59,6 +59,7 @@ const ws_1 = require("@trpc/server/adapters/ws");
|
|
|
59
59
|
const ws_2 = require("ws");
|
|
60
60
|
const addon_upload_1 = require("./api/addon-upload");
|
|
61
61
|
const auth_whoami_1 = require("./api/auth-whoami");
|
|
62
|
+
const enroll_routes_js_1 = require("./api/mesh/enroll.routes.js");
|
|
62
63
|
const backup_download_js_1 = require("./api/backup-download.js");
|
|
63
64
|
const cap_providers_1 = require("./api/core/cap-providers");
|
|
64
65
|
const logging_settings_1 = require("./api/core/logging-settings");
|
|
@@ -457,6 +458,7 @@ async function bootstrap() {
|
|
|
457
458
|
// Mirrors the addon-upload auth chain so the CLI can ping for
|
|
458
459
|
// token-still-valid without consuming a real cap.
|
|
459
460
|
await (0, auth_whoami_1.registerAuthWhoamiRoute)(fastify, uploadAuthService, uploadAddonRegistry);
|
|
461
|
+
(0, enroll_routes_js_1.registerMeshEnrollRoute)(fastify);
|
|
460
462
|
console.log('[bootstrap] Auth whoami route registered at GET /api/auth/whoami');
|
|
461
463
|
}
|
|
462
464
|
catch (err) {
|
|
@@ -715,7 +717,9 @@ async function bootstrap() {
|
|
|
715
717
|
// an operator armed earlier. The logger is attached now so a restored census
|
|
716
718
|
// has somewhere to report before the first request arrives.
|
|
717
719
|
const httpRequestCensus = (0, http_request_census_singleton_1.getHttpRequestCensus)();
|
|
718
|
-
|
|
720
|
+
const trpcLogger = loggingService.createLogger('tRPC');
|
|
721
|
+
httpRequestCensus.setLogger(trpcLogger);
|
|
722
|
+
(0, core_cap_bridge_1.setRemotePrivilegeLogSink)(trpcLogger);
|
|
719
723
|
try {
|
|
720
724
|
const authService = app.get(auth_service_1.AuthService);
|
|
721
725
|
// Normally already built and bridged onto the mesh in `beforeAddonBoot`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.488",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"files": [
|
|
@@ -37,16 +37,16 @@
|
|
|
37
37
|
]
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@camstack/addon-admin-ui": "1.2.
|
|
40
|
+
"@camstack/addon-admin-ui": "1.2.361",
|
|
41
41
|
"@camstack/addon-agent-ui": "1.2.196",
|
|
42
42
|
"@camstack/addon-auth": "1.2.135",
|
|
43
43
|
"@camstack/addon-decoder-nodeav": "1.2.122",
|
|
44
44
|
"@camstack/addon-notifiers": "1.2.129",
|
|
45
45
|
"@camstack/addon-pipeline": "1.2.347",
|
|
46
|
-
"@camstack/addon-pipeline-orchestrator": "1.2.
|
|
46
|
+
"@camstack/addon-pipeline-orchestrator": "1.2.300",
|
|
47
47
|
"@camstack/addon-post-analysis": "1.2.348",
|
|
48
48
|
"@camstack/sdk": "1.2.128",
|
|
49
|
-
"@camstack/system": "1.2.
|
|
49
|
+
"@camstack/system": "1.2.383",
|
|
50
50
|
"@camstack/types": "1.2.333",
|
|
51
51
|
"@camstack/ui-library": "1.2.254",
|
|
52
52
|
"@fastify/compress": "^9.0.0",
|