@camstack/server 1.2.487 → 1.2.489
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 +21 -0
- package/dist/agent/agent-http.js +48 -5
- package/dist/agent/agent-mesh-enroll.js +318 -0
- package/dist/agent/agent-mesh-join.js +48 -7
- package/dist/agent/agent-service.js +4 -0
- package/dist/agent/main.js +39 -4
- package/dist/api/core/auth-revoke-credentials.procedures.js +124 -0
- package/dist/api/core/auth.router.js +2 -0
- package/dist/api/core/cap-providers.js +8 -0
- package/dist/api/mesh/enroll.routes.js +28 -0
- package/dist/api/trpc/core-cap-bridge.js +72 -1
- package/dist/core/agent/agent-registry.service.js +13 -0
- package/dist/core/moleculer/moleculer.service.js +37 -0
- package/dist/main.js +5 -1
- package/package.json +7 -7
|
@@ -81,6 +81,20 @@ function ensurePersistedNodeId(configPath, dataDir) {
|
|
|
81
81
|
}
|
|
82
82
|
return id;
|
|
83
83
|
}
|
|
84
|
+
function parseMeshTls(value) {
|
|
85
|
+
if (value === null || typeof value !== 'object')
|
|
86
|
+
return undefined;
|
|
87
|
+
const certPem = Reflect.get(value, 'certPem');
|
|
88
|
+
const keyPem = Reflect.get(value, 'keyPem');
|
|
89
|
+
const caCertPem = Reflect.get(value, 'caCertPem');
|
|
90
|
+
if (typeof certPem !== 'string' || certPem.length === 0)
|
|
91
|
+
return undefined;
|
|
92
|
+
if (typeof keyPem !== 'string' || keyPem.length === 0)
|
|
93
|
+
return undefined;
|
|
94
|
+
if (typeof caCertPem !== 'string' || caCertPem.length === 0)
|
|
95
|
+
return undefined;
|
|
96
|
+
return { certPem, keyPem, caCertPem };
|
|
97
|
+
}
|
|
84
98
|
function loadAgentConfig(configPath, dataDirOverride) {
|
|
85
99
|
const filePath = configPath ?? 'agent.json';
|
|
86
100
|
const dataDir = dataDirOverride ?? process.env.CAMSTACK_DATA ?? './camstack-data';
|
|
@@ -100,12 +114,17 @@ function loadAgentConfig(configPath, dataDirOverride) {
|
|
|
100
114
|
let fileHubAddress;
|
|
101
115
|
let fileName;
|
|
102
116
|
let fileSecret;
|
|
117
|
+
let fileMeshTls;
|
|
118
|
+
let filePendingJoinToken;
|
|
103
119
|
if (fs.existsSync(configFullPath)) {
|
|
104
120
|
try {
|
|
105
121
|
const raw = JSON.parse(fs.readFileSync(configFullPath, 'utf-8'));
|
|
106
122
|
fileHubAddress = typeof raw.hubAddress === 'string' ? raw.hubAddress : undefined;
|
|
107
123
|
fileName = typeof raw.name === 'string' ? raw.name : undefined;
|
|
108
124
|
fileSecret = typeof raw.secret === 'string' ? raw.secret : undefined;
|
|
125
|
+
fileMeshTls = parseMeshTls(raw.meshTls);
|
|
126
|
+
filePendingJoinToken =
|
|
127
|
+
typeof raw.joinToken === 'string' && raw.joinToken.length > 0 ? raw.joinToken : undefined;
|
|
109
128
|
}
|
|
110
129
|
catch {
|
|
111
130
|
/* corrupt config */
|
|
@@ -123,6 +142,8 @@ function loadAgentConfig(configPath, dataDirOverride) {
|
|
|
123
142
|
addonsDir: path.resolve(resolvedDataDir, 'addons'),
|
|
124
143
|
logLevel: process.env.CAMSTACK_LOG_LEVEL ?? 'info',
|
|
125
144
|
secret: effectiveSecret,
|
|
145
|
+
...(fileMeshTls !== undefined ? { meshTls: fileMeshTls } : {}),
|
|
146
|
+
...(filePendingJoinToken !== undefined ? { pendingJoinToken: filePendingJoinToken } : {}),
|
|
126
147
|
configPath: configFullPath,
|
|
127
148
|
statusPort: Number(process.env.CAMSTACK_STATUS_PORT) || 4444,
|
|
128
149
|
};
|
package/dist/agent/agent-http.js
CHANGED
|
@@ -54,6 +54,7 @@ const fs = __importStar(require("node:fs"));
|
|
|
54
54
|
const path = __importStar(require("node:path"));
|
|
55
55
|
const fastify_1 = __importDefault(require("fastify"));
|
|
56
56
|
const agent_http_auth_js_1 = require("./agent-http-auth.js");
|
|
57
|
+
const agent_mesh_enroll_js_1 = require("./agent-mesh-enroll.js");
|
|
57
58
|
const agent_update_routes_js_1 = require("./agent-update-routes.js");
|
|
58
59
|
const derive_hub_url_js_1 = require("./derive-hub-url.js");
|
|
59
60
|
Object.defineProperty(exports, "pickIpv4", { enumerable: true, get: function () { return derive_hub_url_js_1.pickIpv4; } });
|
|
@@ -211,8 +212,12 @@ function getEffectiveConfig(configPath, nodeId) {
|
|
|
211
212
|
name: typeof raw.name === 'string' ? raw.name : nodeId,
|
|
212
213
|
hubAddress: typeof raw.hubAddress === 'string' ? raw.hubAddress : null,
|
|
213
214
|
hasSecret: typeof raw.secret === 'string' && raw.secret.length > 0,
|
|
215
|
+
hasMeshTls: raw.meshTls !== null && typeof raw.meshTls === 'object',
|
|
214
216
|
};
|
|
215
217
|
}
|
|
218
|
+
/** Persisted fields that are credentials: never echoed by `GET /api/agent/config`. */
|
|
219
|
+
const NEVER_ECHOED_CONFIG_KEYS = new Set(['secret', 'meshTls', 'joinToken']);
|
|
220
|
+
const defaultRedeemJoinToken = (token, nodeId) => (0, agent_mesh_enroll_js_1.enrollAgentMesh)({ token, nodeId });
|
|
216
221
|
// ---------------------------------------------------------------------------
|
|
217
222
|
// Factory
|
|
218
223
|
// ---------------------------------------------------------------------------
|
|
@@ -288,6 +293,11 @@ async function createAgentHttpServer(getBroker, config) {
|
|
|
288
293
|
const hubConnectionState = config.getHubConnectionState
|
|
289
294
|
? config.getHubConnectionState()
|
|
290
295
|
: deriveHubConnectionState(nodes, discoveryMode);
|
|
296
|
+
const meshTransport = config.getMeshTransport
|
|
297
|
+
? config.getMeshTransport()
|
|
298
|
+
: eff.hasMeshTls
|
|
299
|
+
? 'tls'
|
|
300
|
+
: 'clear';
|
|
291
301
|
// The address the agent actually reaches the hub at (from the Moleculer
|
|
292
302
|
// registry) — lets the UI show the real endpoint even under UDP discovery
|
|
293
303
|
// where `eff.hubAddress` is null.
|
|
@@ -308,6 +318,7 @@ async function createAgentHttpServer(getBroker, config) {
|
|
|
308
318
|
hubConnectionState,
|
|
309
319
|
discoveryMode,
|
|
310
320
|
hasSecret: eff.hasSecret,
|
|
321
|
+
meshTransport,
|
|
311
322
|
discoveredNodes,
|
|
312
323
|
};
|
|
313
324
|
}
|
|
@@ -322,6 +333,7 @@ async function createAgentHttpServer(getBroker, config) {
|
|
|
322
333
|
hubConnectionState,
|
|
323
334
|
discoveryMode,
|
|
324
335
|
hasSecret: eff.hasSecret,
|
|
336
|
+
meshTransport,
|
|
325
337
|
discoveredNodes,
|
|
326
338
|
addons: [],
|
|
327
339
|
localIps: [],
|
|
@@ -360,21 +372,50 @@ async function createAgentHttpServer(getBroker, config) {
|
|
|
360
372
|
name: eff.name,
|
|
361
373
|
hubAddress: eff.hubAddress,
|
|
362
374
|
hasSecret: eff.hasSecret,
|
|
375
|
+
hasMeshTls: eff.hasMeshTls,
|
|
363
376
|
configPath: config.configPath,
|
|
364
377
|
dataDir: config.dataDir,
|
|
365
|
-
// Include all persisted fields except
|
|
366
|
-
...Object.fromEntries(Object.entries(persisted).filter(([k]) => k
|
|
378
|
+
// Include all persisted fields except credentials (secret, mesh key, pending token)
|
|
379
|
+
...Object.fromEntries(Object.entries(persisted).filter(([k]) => !NEVER_ECHOED_CONFIG_KEYS.has(k))),
|
|
367
380
|
};
|
|
368
381
|
});
|
|
369
382
|
// -- Config write (merge-patch + persist) ---------------------------
|
|
370
383
|
// Fastify (not Express) natively awaits async route handlers and serializes
|
|
371
384
|
// the returned value / catches rejections, so an async handler is correct here.
|
|
372
385
|
// eslint-disable-next-line oxc/no-async-endpoint-handlers
|
|
373
|
-
app.post('/api/agent/config', async (req) => {
|
|
374
|
-
|
|
386
|
+
app.post('/api/agent/config', async (req, reply) => {
|
|
387
|
+
// `joinToken` is redeemed, never stored; `meshTls` only ever comes from an
|
|
388
|
+
// enrollment, never from a request body.
|
|
389
|
+
const body = req.body ?? {};
|
|
390
|
+
const joinToken = body['joinToken'];
|
|
391
|
+
const patch = Object.fromEntries(Object.entries(body).filter(([k]) => k !== 'joinToken' && k !== 'meshTls'));
|
|
375
392
|
const existing = readConfigFile(config.configPath);
|
|
393
|
+
// Join-token enrollment (mesh mTLS Release A): the hub address comes from
|
|
394
|
+
// the token and wins over a `hubAddress` posted in the same form. Nothing is
|
|
395
|
+
// written unless the whole enrollment verified.
|
|
396
|
+
let enrolled = null;
|
|
397
|
+
if (typeof joinToken === 'string' && joinToken.trim().length > 0) {
|
|
398
|
+
try {
|
|
399
|
+
enrolled = await (config.redeemJoinToken ?? defaultRedeemJoinToken)(joinToken, config.nodeId);
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
403
|
+
console.error(`[Agent] join-token enrollment failed: ${message}`);
|
|
404
|
+
return reply
|
|
405
|
+
.status(err instanceof agent_mesh_enroll_js_1.MeshEnrollError ? 400 : 502)
|
|
406
|
+
.send({ success: false, error: message });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
376
409
|
const merged = { ...existing, ...patch };
|
|
410
|
+
if (enrolled !== null) {
|
|
411
|
+
merged['hubAddress'] = enrolled.hubAddress;
|
|
412
|
+
merged['meshTls'] = enrolled.meshTls;
|
|
413
|
+
}
|
|
377
414
|
writeConfigFile(config.configPath, merged);
|
|
415
|
+
if (enrolled !== null) {
|
|
416
|
+
console.log(`[Agent] Enrolled in the mesh CA via join token — hub ${enrolled.hubAddress}, ` +
|
|
417
|
+
'mutual TLS on next reconnect');
|
|
418
|
+
}
|
|
378
419
|
// Apply name change immediately (no reconnect needed)
|
|
379
420
|
if (typeof patch.name === 'string' && patch.name.trim()) {
|
|
380
421
|
try {
|
|
@@ -390,10 +431,12 @@ async function createAgentHttpServer(getBroker, config) {
|
|
|
390
431
|
// `patch.hubAddress !== undefined` was always true and produced
|
|
391
432
|
// false-positive "Restart required" prompts on no-op saves.
|
|
392
433
|
const reconnectRelevant = ['hubAddress', 'secret'];
|
|
393
|
-
const needsReconnect =
|
|
434
|
+
const needsReconnect = enrolled !== null ||
|
|
435
|
+
reconnectRelevant.some((k) => Object.prototype.hasOwnProperty.call(patch, k) && patch[k] !== existing[k]);
|
|
394
436
|
return {
|
|
395
437
|
success: true,
|
|
396
438
|
restartRequired: needsReconnect,
|
|
439
|
+
...(enrolled !== null ? { enrolled: true } : {}),
|
|
397
440
|
};
|
|
398
441
|
});
|
|
399
442
|
// -- Agent reconnect (applies new hub/secret config) ----------------
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.MeshEnrollError = void 0;
|
|
37
|
+
exports.decodeJoinTokenClaims = decodeJoinTokenClaims;
|
|
38
|
+
exports.hubHostFromMeshAddress = hubHostFromMeshAddress;
|
|
39
|
+
exports.enrollAgentMesh = enrollAgentMesh;
|
|
40
|
+
exports.persistEnrollment = persistEnrollment;
|
|
41
|
+
exports.discardPendingJoinToken = discardPendingJoinToken;
|
|
42
|
+
exports.enrollAndPersist = enrollAndPersist;
|
|
43
|
+
/**
|
|
44
|
+
* The agent's side of mesh mTLS enrollment (Release A).
|
|
45
|
+
*
|
|
46
|
+
* The operator mints a single-use join token on the hub (System → Cluster →
|
|
47
|
+
* Add agent) and pastes it into this agent's setup. Redeeming it:
|
|
48
|
+
*
|
|
49
|
+
* 1. decode the token's claims WITHOUT trusting them (the agent holds no CA
|
|
50
|
+
* key yet): `hubUrl`, `hubMeshAddress`, `caFingerprint`, optional `nodeId`;
|
|
51
|
+
* 2. a token pinned to another node id is refused before anything is sent;
|
|
52
|
+
* 3. generate this node's own key pair; the private key never leaves it;
|
|
53
|
+
* 4. `POST <hubUrl>/api/mesh/enroll` with `{ token, csr }`. The hub's HTTPS
|
|
54
|
+
* certificate is NOT the mesh CA, so the TLS peer is not what is pinned:
|
|
55
|
+
* 5. the answer is trusted only if its CA certificate hashes to the token's
|
|
56
|
+
* `caFingerprint` (a MITM with another CA fails here), the token's ES256
|
|
57
|
+
* signature verifies against that CA, and the issued leaf is signed by it,
|
|
58
|
+
* carries this node id as CN and matches the private key;
|
|
59
|
+
* 6. only then are the material and the hub address persisted (`agent.json`,
|
|
60
|
+
* 0600) — the dial port is NOT stored: with a certificate the broker dials
|
|
61
|
+
* the TLS port, so the address keeps the host only.
|
|
62
|
+
*/
|
|
63
|
+
const node_crypto_1 = require("node:crypto");
|
|
64
|
+
const fs = __importStar(require("node:fs"));
|
|
65
|
+
const http = __importStar(require("node:http"));
|
|
66
|
+
const https = __importStar(require("node:https"));
|
|
67
|
+
const path = __importStar(require("node:path"));
|
|
68
|
+
const system_1 = require("@camstack/system");
|
|
69
|
+
class MeshEnrollError extends Error {
|
|
70
|
+
constructor(message) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.name = 'MeshEnrollError';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
exports.MeshEnrollError = MeshEnrollError;
|
|
76
|
+
const ENROLL_PATH = '/api/mesh/enroll';
|
|
77
|
+
const ENROLL_TIMEOUT_MS = 15_000;
|
|
78
|
+
function base64urlJson(segment) {
|
|
79
|
+
return JSON.parse(Buffer.from(segment, 'base64url').toString('utf8'));
|
|
80
|
+
}
|
|
81
|
+
/** Decode the payload of a compact JWS. Untrusted: nothing here is verified. */
|
|
82
|
+
function decodeJoinTokenClaims(token) {
|
|
83
|
+
const parts = token.trim().split('.');
|
|
84
|
+
if (parts.length !== 3 || parts[1] === undefined) {
|
|
85
|
+
throw new MeshEnrollError('join token is malformed (expected a compact JWT)');
|
|
86
|
+
}
|
|
87
|
+
let payload;
|
|
88
|
+
try {
|
|
89
|
+
payload = base64urlJson(parts[1]);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw new MeshEnrollError('join token payload is not valid JSON');
|
|
93
|
+
}
|
|
94
|
+
if (payload === null || typeof payload !== 'object') {
|
|
95
|
+
throw new MeshEnrollError('join token payload is not an object');
|
|
96
|
+
}
|
|
97
|
+
const hubUrl = Reflect.get(payload, 'hubUrl');
|
|
98
|
+
const hubMeshAddress = Reflect.get(payload, 'hubMeshAddress');
|
|
99
|
+
const caFingerprint = Reflect.get(payload, 'caFingerprint');
|
|
100
|
+
const nodeId = Reflect.get(payload, 'nodeId');
|
|
101
|
+
const exp = Reflect.get(payload, 'exp');
|
|
102
|
+
if (typeof hubUrl !== 'string' || hubUrl.length === 0) {
|
|
103
|
+
throw new MeshEnrollError('join token carries no hubUrl');
|
|
104
|
+
}
|
|
105
|
+
if (typeof hubMeshAddress !== 'string' || hubMeshAddress.length === 0) {
|
|
106
|
+
throw new MeshEnrollError('join token carries no hubMeshAddress');
|
|
107
|
+
}
|
|
108
|
+
if (typeof caFingerprint !== 'string' || caFingerprint.length === 0) {
|
|
109
|
+
throw new MeshEnrollError('join token carries no caFingerprint');
|
|
110
|
+
}
|
|
111
|
+
if (typeof exp !== 'number')
|
|
112
|
+
throw new MeshEnrollError('join token carries no expiry');
|
|
113
|
+
return {
|
|
114
|
+
hubUrl,
|
|
115
|
+
hubMeshAddress,
|
|
116
|
+
caFingerprint: normalizeFingerprint(caFingerprint),
|
|
117
|
+
...(typeof nodeId === 'string' && nodeId.length > 0 ? { nodeId } : {}),
|
|
118
|
+
exp,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function normalizeFingerprint(fp) {
|
|
122
|
+
return fp.replace(/:/g, '').toLowerCase();
|
|
123
|
+
}
|
|
124
|
+
/** `host:6000` / `[::1]:6000` / `host` → the host, port dropped. */
|
|
125
|
+
function hubHostFromMeshAddress(address) {
|
|
126
|
+
const trimmed = address.trim();
|
|
127
|
+
const bracketed = /^(\[[^\]]+\])(?::\d+)?$/.exec(trimmed);
|
|
128
|
+
if (bracketed?.[1] !== undefined)
|
|
129
|
+
return bracketed[1];
|
|
130
|
+
const hostPort = /^([^:]+):\d+$/.exec(trimmed);
|
|
131
|
+
return hostPort?.[1] ?? trimmed;
|
|
132
|
+
}
|
|
133
|
+
function compactFingerprint(certPem) {
|
|
134
|
+
return (0, node_crypto_1.createHash)('sha256').update(new node_crypto_1.X509Certificate(certPem).raw).digest('hex');
|
|
135
|
+
}
|
|
136
|
+
/** ES256 compact-JWS signature check against the CA certificate's public key. */
|
|
137
|
+
function tokenSignatureValid(token, caCertPem) {
|
|
138
|
+
const parts = token.trim().split('.');
|
|
139
|
+
if (parts.length !== 3)
|
|
140
|
+
return false;
|
|
141
|
+
const [header, payload, signature] = parts;
|
|
142
|
+
if (header === undefined || payload === undefined || signature === undefined)
|
|
143
|
+
return false;
|
|
144
|
+
try {
|
|
145
|
+
return (0, node_crypto_1.verify)('sha256', Buffer.from(`${header}.${payload}`), { key: new node_crypto_1.X509Certificate(caCertPem).publicKey, dsaEncoding: 'ieee-p1363' }, Buffer.from(signature, 'base64url'));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Node `http(s)` POST. The hub's HTTPS leaf is not the mesh CA, so the CA pin below is the trust. */
|
|
152
|
+
const nodeEnrollFetch = (url, init) => new Promise((resolve, reject) => {
|
|
153
|
+
const target = new URL(url);
|
|
154
|
+
const request = (target.protocol === 'http:' ? http : https).request(target, {
|
|
155
|
+
method: init.method,
|
|
156
|
+
headers: { ...init.headers, 'content-length': Buffer.byteLength(init.body) },
|
|
157
|
+
timeout: ENROLL_TIMEOUT_MS,
|
|
158
|
+
...(target.protocol === 'https:' ? { rejectUnauthorized: false } : {}),
|
|
159
|
+
}, (res) => {
|
|
160
|
+
const chunks = [];
|
|
161
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
162
|
+
res.on('end', () => {
|
|
163
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
164
|
+
resolve({
|
|
165
|
+
status: res.statusCode ?? 0,
|
|
166
|
+
json: () => Promise.resolve(JSON.parse(raw)),
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
res.on('error', reject);
|
|
170
|
+
});
|
|
171
|
+
request.on('timeout', () => request.destroy(new Error(`enroll request timed out`)));
|
|
172
|
+
request.on('error', reject);
|
|
173
|
+
request.end(init.body);
|
|
174
|
+
});
|
|
175
|
+
function readAnswer(status, body) {
|
|
176
|
+
const record = body !== null && typeof body === 'object' ? body : {};
|
|
177
|
+
if (status !== 200 || Reflect.get(record, 'ok') !== true) {
|
|
178
|
+
const error = Reflect.get(record, 'error');
|
|
179
|
+
throw new MeshEnrollError(`hub refused the enrollment (HTTP ${status}): ${typeof error === 'string' ? error : 'no reason given'}`);
|
|
180
|
+
}
|
|
181
|
+
const cert = Reflect.get(record, 'cert');
|
|
182
|
+
const caCert = Reflect.get(record, 'caCert');
|
|
183
|
+
if (typeof cert !== 'string' || typeof caCert !== 'string') {
|
|
184
|
+
throw new MeshEnrollError('hub answered without a certificate and CA certificate');
|
|
185
|
+
}
|
|
186
|
+
return { cert, caCert };
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Redeem a join token. Throws {@link MeshEnrollError} with an operator-readable
|
|
190
|
+
* reason; nothing is persisted here.
|
|
191
|
+
*/
|
|
192
|
+
async function enrollAgentMesh(input) {
|
|
193
|
+
const token = input.token.trim();
|
|
194
|
+
const claims = decodeJoinTokenClaims(token);
|
|
195
|
+
const nowSec = input.nowSec ?? Math.floor(Date.now() / 1000);
|
|
196
|
+
if (claims.exp <= nowSec) {
|
|
197
|
+
throw new MeshEnrollError('join token has expired — mint a new one on the hub');
|
|
198
|
+
}
|
|
199
|
+
if (claims.nodeId !== undefined && claims.nodeId !== input.nodeId) {
|
|
200
|
+
throw new MeshEnrollError(`join token is for node "${claims.nodeId}", but this agent is "${input.nodeId}"`);
|
|
201
|
+
}
|
|
202
|
+
const keys = await (0, system_1.generateMeshKeyPair)();
|
|
203
|
+
const csr = await (0, system_1.createMeshCsrPem)(keys, input.nodeId);
|
|
204
|
+
const keyPem = await (0, system_1.exportMeshPrivateKeyPem)(keys.privateKey);
|
|
205
|
+
const url = `${claims.hubUrl.replace(/\/+$/, '')}${ENROLL_PATH}`;
|
|
206
|
+
const fetchFn = input.fetchFn ?? nodeEnrollFetch;
|
|
207
|
+
let response;
|
|
208
|
+
try {
|
|
209
|
+
response = await fetchFn(url, {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: { 'content-type': 'application/json' },
|
|
212
|
+
body: JSON.stringify({ token, csr }),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
throw new MeshEnrollError(`could not reach the hub at ${claims.hubUrl}: ${err instanceof Error ? err.message : String(err)}`);
|
|
217
|
+
}
|
|
218
|
+
let body;
|
|
219
|
+
try {
|
|
220
|
+
body = await response.json();
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
body = null;
|
|
224
|
+
}
|
|
225
|
+
const answer = readAnswer(response.status, body);
|
|
226
|
+
// Trust nothing the hub said until it proves it holds the CA the token names.
|
|
227
|
+
let fingerprint;
|
|
228
|
+
try {
|
|
229
|
+
fingerprint = compactFingerprint(answer.caCert);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
throw new MeshEnrollError('hub returned an unreadable CA certificate');
|
|
233
|
+
}
|
|
234
|
+
if (fingerprint !== claims.caFingerprint) {
|
|
235
|
+
throw new MeshEnrollError('the CA certificate returned by the hub does not match the token fingerprint — ' +
|
|
236
|
+
'enrollment aborted (is something between this agent and the hub?)');
|
|
237
|
+
}
|
|
238
|
+
if (!tokenSignatureValid(token, answer.caCert)) {
|
|
239
|
+
throw new MeshEnrollError('join token signature does not verify against the hub CA');
|
|
240
|
+
}
|
|
241
|
+
assertLeafIsOurs(answer, keyPem, input.nodeId);
|
|
242
|
+
return {
|
|
243
|
+
meshTls: { certPem: answer.cert, keyPem, caCertPem: answer.caCert },
|
|
244
|
+
hubAddress: hubHostFromMeshAddress(claims.hubMeshAddress),
|
|
245
|
+
claims,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function assertLeafIsOurs(answer, keyPem, nodeId) {
|
|
249
|
+
let leaf;
|
|
250
|
+
try {
|
|
251
|
+
leaf = new node_crypto_1.X509Certificate(answer.cert);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
throw new MeshEnrollError('hub returned an unreadable node certificate');
|
|
255
|
+
}
|
|
256
|
+
const ca = new node_crypto_1.X509Certificate(answer.caCert);
|
|
257
|
+
if (!leaf.verify(ca.publicKey)) {
|
|
258
|
+
throw new MeshEnrollError('the node certificate is not signed by the hub CA');
|
|
259
|
+
}
|
|
260
|
+
if (!new RegExp(`(?:^|\\n)CN=${escapeRegExp(nodeId)}(?:\\n|$)`).test(leaf.subject)) {
|
|
261
|
+
throw new MeshEnrollError(`the node certificate is not issued for "${nodeId}"`);
|
|
262
|
+
}
|
|
263
|
+
if (!leaf.checkPrivateKey((0, node_crypto_1.createPrivateKey)(keyPem))) {
|
|
264
|
+
throw new MeshEnrollError('the node certificate does not match this agent’s private key');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function escapeRegExp(value) {
|
|
268
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
269
|
+
}
|
|
270
|
+
function readJsonFile(file) {
|
|
271
|
+
if (!fs.existsSync(file))
|
|
272
|
+
return {};
|
|
273
|
+
try {
|
|
274
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
275
|
+
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
276
|
+
? Object.fromEntries(Object.entries(parsed))
|
|
277
|
+
: {};
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return {};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function withoutJoinToken(record) {
|
|
284
|
+
return Object.fromEntries(Object.entries(record).filter(([key]) => key !== 'joinToken'));
|
|
285
|
+
}
|
|
286
|
+
function writeJsonFile0600(file, data) {
|
|
287
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
288
|
+
const tmp = `${file}.tmp`;
|
|
289
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
290
|
+
fs.renameSync(tmp, file);
|
|
291
|
+
fs.chmodSync(file, 0o600);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Write the enrollment into `agent.json` (0600): `meshTls` + the host-only
|
|
295
|
+
* `hubAddress`; a pending `joinToken` is removed. The cluster secret, if any,
|
|
296
|
+
* stays — it is still a valid join proof for one release.
|
|
297
|
+
*/
|
|
298
|
+
function persistEnrollment(configPath, enrolled) {
|
|
299
|
+
const next = {
|
|
300
|
+
...withoutJoinToken(readJsonFile(configPath)),
|
|
301
|
+
hubAddress: enrolled.hubAddress,
|
|
302
|
+
meshTls: enrolled.meshTls,
|
|
303
|
+
};
|
|
304
|
+
writeJsonFile0600(configPath, next);
|
|
305
|
+
}
|
|
306
|
+
/** Drop a pending `joinToken` from `agent.json` (consumed — or dead). */
|
|
307
|
+
function discardPendingJoinToken(configPath) {
|
|
308
|
+
const existing = readJsonFile(configPath);
|
|
309
|
+
if (!('joinToken' in existing))
|
|
310
|
+
return;
|
|
311
|
+
writeJsonFile0600(configPath, withoutJoinToken(existing));
|
|
312
|
+
}
|
|
313
|
+
/** Redeem `token` and persist the result. Throws {@link MeshEnrollError}. */
|
|
314
|
+
async function enrollAndPersist(input) {
|
|
315
|
+
const enrolled = await enrollAgentMesh(input);
|
|
316
|
+
persistEnrollment(input.configPath, enrolled);
|
|
317
|
+
return enrolled;
|
|
318
|
+
}
|
|
@@ -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
|
@@ -46,6 +46,7 @@ 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_mesh_join_js_1 = require("./agent-mesh-join.js");
|
|
48
48
|
const agent_config_js_1 = require("./agent-config.js");
|
|
49
|
+
const agent_mesh_enroll_js_1 = require("./agent-mesh-enroll.js");
|
|
49
50
|
const agent_group_runner_js_1 = require("./agent-group-runner.js");
|
|
50
51
|
const agent_http_js_1 = require("./agent-http.js");
|
|
51
52
|
const agent_infra_capabilities_js_1 = require("./agent-infra-capabilities.js");
|
|
@@ -78,8 +79,32 @@ const DATA_NODEID_CAPS = new Set(types_1.ALL_CAPABILITY_DEFINITIONS.filter((c) =
|
|
|
78
79
|
// ---------------------------------------------------------------------------
|
|
79
80
|
// Main
|
|
80
81
|
// ---------------------------------------------------------------------------
|
|
82
|
+
/**
|
|
83
|
+
* Load `agent.json`, first redeeming a join token the setup window left there
|
|
84
|
+
* (mesh mTLS Release A — the desktop setup runs before the agent does). A token
|
|
85
|
+
* is single-use, so it is dropped whether or not the hub accepted it; a refusal
|
|
86
|
+
* is logged loudly and the agent still boots (clear, or with the cluster secret).
|
|
87
|
+
*/
|
|
88
|
+
async function loadAgentConfigRedeemingJoinToken(configPath) {
|
|
89
|
+
const loaded = (0, agent_config_js_1.loadAgentConfig)(configPath);
|
|
90
|
+
if (loaded.pendingJoinToken === undefined)
|
|
91
|
+
return loaded;
|
|
92
|
+
try {
|
|
93
|
+
const enrolled = await (0, agent_mesh_enroll_js_1.enrollAndPersist)({
|
|
94
|
+
token: loaded.pendingJoinToken,
|
|
95
|
+
nodeId: loaded.nodeId,
|
|
96
|
+
configPath: loaded.configPath,
|
|
97
|
+
});
|
|
98
|
+
console.log(`[Agent] Enrolled in the mesh CA (hub ${enrolled.hubAddress}) — dialing over mutual TLS`);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
console.error(`[Agent] Join-token enrollment failed — staying on the previous transport: ${(0, types_1.errMsg)(err)}`);
|
|
102
|
+
(0, agent_mesh_enroll_js_1.discardPendingJoinToken)(loaded.configPath);
|
|
103
|
+
}
|
|
104
|
+
return (0, agent_config_js_1.loadAgentConfig)(configPath, loaded.dataDir);
|
|
105
|
+
}
|
|
81
106
|
async function startAgent(configPath) {
|
|
82
|
-
const config =
|
|
107
|
+
const config = await loadAgentConfigRedeemingJoinToken(configPath);
|
|
83
108
|
// Derive CAMSTACK_HUB_URL from the configured hub address so remote agents
|
|
84
109
|
// no longer need a second, redundant env var set by hand (the cross-node
|
|
85
110
|
// source trap). An explicit operator value ALWAYS wins — capture whether it
|
|
@@ -177,12 +202,17 @@ async function startAgent(configPath) {
|
|
|
177
202
|
}
|
|
178
203
|
},
|
|
179
204
|
});
|
|
205
|
+
// Mesh mTLS (Release A): with a certificate the broker dials the hub's TLS
|
|
206
|
+
// port; without one it keeps dialing clear 6000.
|
|
207
|
+
let currentMeshTls = config.meshTls ?? null;
|
|
208
|
+
console.log(`[Agent] Mesh transport: ${currentMeshTls !== null ? 'tls' : 'clear'}`);
|
|
180
209
|
let broker = (0, system_1.createBroker)({
|
|
181
210
|
nodeID: config.nodeId,
|
|
182
211
|
mode: 'agent',
|
|
183
212
|
hubAddress: config.hubAddress,
|
|
184
213
|
logLevel: config.logLevel,
|
|
185
|
-
middlewares:
|
|
214
|
+
middlewares: meshJoin.middlewares,
|
|
215
|
+
...(currentMeshTls !== null ? { meshTls: currentMeshTls } : {}),
|
|
186
216
|
});
|
|
187
217
|
meshJoin.attach(broker);
|
|
188
218
|
/**
|
|
@@ -200,7 +230,8 @@ async function startAgent(configPath) {
|
|
|
200
230
|
}
|
|
201
231
|
// Reload config from file (UI may have written new hubAddress/secret)
|
|
202
232
|
const fresh = (0, agent_config_js_1.loadAgentConfig)(undefined, config.dataDir);
|
|
203
|
-
console.log(`[Agent] New config: hub=${fresh.hubAddress ?? 'discovery'}, secret=${fresh.secret ? 'yes' : 'none'}`
|
|
233
|
+
console.log(`[Agent] New config: hub=${fresh.hubAddress ?? 'discovery'}, secret=${fresh.secret ? 'yes' : 'none'}, ` +
|
|
234
|
+
`mesh=${fresh.meshTls !== undefined ? 'tls' : 'clear'}`);
|
|
204
235
|
// Re-derive CAMSTACK_HUB_URL for the discovery-mode → dashboard-configured
|
|
205
236
|
// flow (config file freshly written a hubAddress). Explicit operator values
|
|
206
237
|
// still win via the boot-captured `hubUrlWasExplicit` flag.
|
|
@@ -214,12 +245,14 @@ async function startAgent(configPath) {
|
|
|
214
245
|
console.log(`[Agent] Derived CAMSTACK_HUB_URL=${freshHubUrl} from hub address "${fresh.hubAddress}"`);
|
|
215
246
|
}
|
|
216
247
|
currentSecret = fresh.secret ?? null;
|
|
248
|
+
currentMeshTls = fresh.meshTls ?? null;
|
|
217
249
|
broker = (0, system_1.createBroker)({
|
|
218
250
|
nodeID: config.nodeId,
|
|
219
251
|
mode: 'agent',
|
|
220
252
|
hubAddress: fresh.hubAddress,
|
|
221
253
|
logLevel: fresh.logLevel,
|
|
222
|
-
middlewares:
|
|
254
|
+
middlewares: meshJoin.middlewares,
|
|
255
|
+
...(currentMeshTls !== null ? { meshTls: currentMeshTls } : {}),
|
|
223
256
|
});
|
|
224
257
|
meshJoin.attach(broker);
|
|
225
258
|
// The boot-time `$node.connected` listeners belong to the old broker: let
|
|
@@ -520,6 +553,7 @@ async function startAgent(configPath) {
|
|
|
520
553
|
triggerUpwardRegistration();
|
|
521
554
|
},
|
|
522
555
|
expectedClusterSecretHash: config.secret ? (0, system_1.hashClusterSecret)(config.secret) : undefined,
|
|
556
|
+
meshAuth: meshJoin.authenticator,
|
|
523
557
|
installFromNpm: async (pkg, version) => {
|
|
524
558
|
await installer.install(pkg, version);
|
|
525
559
|
},
|
|
@@ -853,6 +887,7 @@ async function startAgent(configPath) {
|
|
|
853
887
|
configPath: config.configPath,
|
|
854
888
|
onReconnect: reconnect,
|
|
855
889
|
getHubConnectionState,
|
|
890
|
+
getMeshTransport: () => (currentMeshTls !== null ? 'tls' : 'clear'),
|
|
856
891
|
updateControls,
|
|
857
892
|
});
|
|
858
893
|
// ── Phase 1: Load core infrastructure addons (in-process) ──
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RevokeSkipReasonSchema = exports.RevocableCredentialKindSchema = exports.REVOKE_CREDENTIALS_MAX = void 0;
|
|
4
|
+
exports.createRevokeCredentialsProcedures = createRevokeCredentialsProcedures;
|
|
5
|
+
const server_1 = require("@trpc/server");
|
|
6
|
+
const zod_1 = require("zod");
|
|
7
|
+
const hub_sessions_js_1 = require("../../auth/hub-sessions.js");
|
|
8
|
+
const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
|
|
9
|
+
/** Upper bound of one bulk call — a page of a table, several times over. */
|
|
10
|
+
exports.REVOKE_CREDENTIALS_MAX = 500;
|
|
11
|
+
/** What a bulk revoke can target. Each has a per-row revoke on the server. */
|
|
12
|
+
exports.RevocableCredentialKindSchema = zod_1.z.enum(['share-link', 'admin-token', 'login-session']);
|
|
13
|
+
exports.RevokeSkipReasonSchema = zod_1.z.enum(['current-session', 'not-active']);
|
|
14
|
+
const RevokeCredentialsInputSchema = zod_1.z
|
|
15
|
+
.object({
|
|
16
|
+
kind: exports.RevocableCredentialKindSchema,
|
|
17
|
+
ids: zod_1.z.array(zod_1.z.string().min(1)).min(1).max(exports.REVOKE_CREDENTIALS_MAX),
|
|
18
|
+
/** The count the operator confirmed. Must equal `ids.length`. */
|
|
19
|
+
confirmCount: zod_1.z.number().int().min(1),
|
|
20
|
+
})
|
|
21
|
+
.refine((input) => new Set(input.ids).size === input.ids.length, {
|
|
22
|
+
message: 'Each id may be listed once',
|
|
23
|
+
path: ['ids'],
|
|
24
|
+
});
|
|
25
|
+
const RevokeCredentialsOutputSchema = zod_1.z.object({
|
|
26
|
+
revoked: zod_1.z.array(zod_1.z.string()),
|
|
27
|
+
skipped: zod_1.z.array(zod_1.z.object({ id: zod_1.z.string(), reason: exports.RevokeSkipReasonSchema })),
|
|
28
|
+
});
|
|
29
|
+
/** Keep the audit row bounded; the ids beyond this are counted, not listed. */
|
|
30
|
+
const AUDIT_IDS_MAX_CHARS = 4000;
|
|
31
|
+
function assertSignedInAdminSession(user) {
|
|
32
|
+
if (user.isScoped === true ||
|
|
33
|
+
user.shareView !== undefined ||
|
|
34
|
+
user.credential !== undefined ||
|
|
35
|
+
user.adminTokenId !== undefined) {
|
|
36
|
+
throw new server_1.TRPCError({
|
|
37
|
+
code: 'FORBIDDEN',
|
|
38
|
+
message: 'Revoking credentials in bulk needs a signed-in admin session',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function requireInternal(registry) {
|
|
43
|
+
const internal = registry === null ? null : (0, hub_sessions_js_1.userManagementInternal)(registry);
|
|
44
|
+
if (internal === null) {
|
|
45
|
+
throw new server_1.TRPCError({
|
|
46
|
+
code: 'PRECONDITION_FAILED',
|
|
47
|
+
message: 'User management is not available',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return internal;
|
|
51
|
+
}
|
|
52
|
+
/** The per-row revoke for `kind`, bound to the caller. `null` = revoked. */
|
|
53
|
+
function revokerFor(kind, caller, deps) {
|
|
54
|
+
if (kind === 'share-link') {
|
|
55
|
+
const service = deps.shareTokens;
|
|
56
|
+
if (service === null) {
|
|
57
|
+
throw new server_1.TRPCError({ code: 'PRECONDITION_FAILED', message: 'Share links are not available' });
|
|
58
|
+
}
|
|
59
|
+
return async (id) => (await service.revoke({ id, callerUserId: caller.id, callerIsAdmin: true }))
|
|
60
|
+
? null
|
|
61
|
+
: 'not-active';
|
|
62
|
+
}
|
|
63
|
+
const internal = requireInternal(deps.registry);
|
|
64
|
+
if (kind === 'admin-token') {
|
|
65
|
+
return async (id) => ((await internal.adminTokens.revoke(id)) ? null : 'not-active');
|
|
66
|
+
}
|
|
67
|
+
return async (id) => {
|
|
68
|
+
if (id === caller.sessionId)
|
|
69
|
+
return 'current-session';
|
|
70
|
+
if (internal.sessions.check(id).state !== 'active')
|
|
71
|
+
return 'not-active';
|
|
72
|
+
return (await internal.sessions.revoke(id, 'revoked-by-admin')) ? null : 'not-active';
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function auditIds(ids) {
|
|
76
|
+
const joined = ids.join(',');
|
|
77
|
+
return joined.length <= AUDIT_IDS_MAX_CHARS ? joined : `${joined.slice(0, AUDIT_IDS_MAX_CHARS)}…`;
|
|
78
|
+
}
|
|
79
|
+
function createRevokeCredentialsProcedures(deps) {
|
|
80
|
+
return {
|
|
81
|
+
/** ADMIN — revoke exactly `ids` of one kind; see the module header. */
|
|
82
|
+
revokeCredentials: trpc_middleware_js_1.adminProcedure
|
|
83
|
+
.input(RevokeCredentialsInputSchema)
|
|
84
|
+
.output(RevokeCredentialsOutputSchema)
|
|
85
|
+
.mutation(async ({ ctx, input }) => {
|
|
86
|
+
assertSignedInAdminSession(ctx.user);
|
|
87
|
+
if (input.confirmCount !== input.ids.length) {
|
|
88
|
+
throw new server_1.TRPCError({
|
|
89
|
+
code: 'BAD_REQUEST',
|
|
90
|
+
message: `Confirmed ${input.confirmCount} but ${input.ids.length} were listed — nothing was revoked`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const revokeOne = revokerFor(input.kind, ctx.user, deps);
|
|
94
|
+
const revoked = [];
|
|
95
|
+
const skipped = [];
|
|
96
|
+
// Sequential on purpose: the stores are single-writer, and a partial
|
|
97
|
+
// failure must leave an exact account of what already happened.
|
|
98
|
+
for (const id of input.ids) {
|
|
99
|
+
const reason = await revokeOne(id);
|
|
100
|
+
if (reason === null)
|
|
101
|
+
revoked.push(id);
|
|
102
|
+
else
|
|
103
|
+
skipped.push({ id, reason });
|
|
104
|
+
}
|
|
105
|
+
const internal = deps.registry === null ? null : (0, hub_sessions_js_1.userManagementInternal)(deps.registry);
|
|
106
|
+
if (internal !== null) {
|
|
107
|
+
const ip = deps.network.clientAddress(ctx.req).ip;
|
|
108
|
+
await internal.audit.record({
|
|
109
|
+
action: 'credentials-revoked',
|
|
110
|
+
userId: ctx.user.id,
|
|
111
|
+
...(ip !== null ? { ip } : {}),
|
|
112
|
+
detail: {
|
|
113
|
+
kind: input.kind,
|
|
114
|
+
requested: input.ids.length,
|
|
115
|
+
revoked: revoked.length,
|
|
116
|
+
skipped: skipped.length,
|
|
117
|
+
ids: auditIds(revoked),
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return { revoked, skipped };
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -35,6 +35,7 @@ const principal_visibility_js_1 = require("../trpc/principal-visibility.js");
|
|
|
35
35
|
const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
|
|
36
36
|
const auth_limits_js_1 = require("./auth-limits.js");
|
|
37
37
|
const auth_admin_tokens_procedures_js_1 = require("./auth-admin-tokens.procedures.js");
|
|
38
|
+
const auth_revoke_credentials_procedures_js_1 = require("./auth-revoke-credentials.procedures.js");
|
|
38
39
|
const auth_reauth_procedures_js_1 = require("./auth-reauth.procedures.js");
|
|
39
40
|
const auth_sessions_procedures_js_1 = require("./auth-sessions.procedures.js");
|
|
40
41
|
/**
|
|
@@ -1080,6 +1081,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1080
1081
|
...(0, auth_sessions_procedures_js_1.createSessionProcedures)({ registry, minter, credentialProcedure }),
|
|
1081
1082
|
...(0, auth_reauth_procedures_js_1.createReauthProcedures)({ auth, registry, moleculer, limiters }),
|
|
1082
1083
|
...(0, auth_admin_tokens_procedures_js_1.createAdminTokenProcedures)({ auth, registry, network }),
|
|
1084
|
+
...(0, auth_revoke_credentials_procedures_js_1.createRevokeCredentialsProcedures)({ registry, shareTokens, network }),
|
|
1083
1085
|
// ── External sign-in identities (D782) ────────────────────────
|
|
1084
1086
|
//
|
|
1085
1087
|
// A signed-in user links an OIDC identity to THEIR OWN account. The
|
|
@@ -245,6 +245,11 @@ function buildToastProvider(toastService, ctx) {
|
|
|
245
245
|
},
|
|
246
246
|
};
|
|
247
247
|
}
|
|
248
|
+
/** `{ meshTransport }` for an online agent, `{}` when the hub cannot tell. */
|
|
249
|
+
function meshTransportField(nodeId) {
|
|
250
|
+
const meshTransport = (0, system_1.getProcessNodeMeshTransport)(nodeId);
|
|
251
|
+
return meshTransport === undefined ? {} : { meshTransport };
|
|
252
|
+
}
|
|
248
253
|
function getLocalIps() {
|
|
249
254
|
const interfaces = os.networkInterfaces();
|
|
250
255
|
const ips = [];
|
|
@@ -411,6 +416,9 @@ async function computeTopologyInternal(agentRegistry, addonRegistry, getNodeRoot
|
|
|
411
416
|
// (hub: the running @camstack/server; agents: their registerNode
|
|
412
417
|
// manifest via HubNodeRegistry). Null when unknown.
|
|
413
418
|
rootPackage: getNodeRootPackage?.(node.info.id, node.isHub) ?? null,
|
|
419
|
+
// Mesh mTLS Release A: observed by the hub's transporter (certificate CN),
|
|
420
|
+
// never self-reported by the agent. Hub row and unknown: no badge.
|
|
421
|
+
...(node.isHub ? {} : meshTransportField(node.info.id)),
|
|
414
422
|
};
|
|
415
423
|
});
|
|
416
424
|
// A2: union in OFFLINE rows for every persisted node no longer present in the
|
|
@@ -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,11 @@
|
|
|
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.REMOTE_PRIVILEGE_ALLOWLIST = exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE = exports.REMOTE_DENIED_NAMESPACES = void 0;
|
|
22
|
+
exports.setRemotePrivilegeLogSink = setRemotePrivilegeLogSink;
|
|
23
|
+
exports.__resetRemotePrivilegeObservationForTests = __resetRemotePrivilegeObservationForTests;
|
|
24
|
+
exports.observeRemotePrivilege = observeRemotePrivilege;
|
|
25
|
+
exports.observeRemotePrivilegeRefused = observeRemotePrivilegeRefused;
|
|
22
26
|
exports.isCoreServiceCapName = isCoreServiceCapName;
|
|
23
27
|
exports.describeMeshOrigin = describeMeshOrigin;
|
|
24
28
|
exports.buildCoreCapService = buildCoreCapService;
|
|
@@ -72,6 +76,64 @@ exports.REMOTE_DENIED_NAMESPACES = new Set([
|
|
|
72
76
|
]);
|
|
73
77
|
/** Moleculer error `type` of a bridged call another node may not make. */
|
|
74
78
|
exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE = 'CORE_CAP_REMOTE_FORBIDDEN';
|
|
79
|
+
/**
|
|
80
|
+
* Hub-core procedure paths another node may call after {@link REMOTE_DENIED_NAMESPACES}.
|
|
81
|
+
* Seeded from armed transport census on the live hub (2026-10): remote agents
|
|
82
|
+
* (`mesh:<agent>`) called none of these; hub runners used this surface only.
|
|
83
|
+
*/
|
|
84
|
+
exports.REMOTE_PRIVILEGE_ALLOWLIST = new Set([
|
|
85
|
+
'addons.listCapabilityProviders',
|
|
86
|
+
'nodes.topology',
|
|
87
|
+
'integrations.getByAddonId',
|
|
88
|
+
'addons.list',
|
|
89
|
+
'system.getLoggingSettings',
|
|
90
|
+
'addons.setCapabilityProviderEnabled',
|
|
91
|
+
'system.getSiteLocation',
|
|
92
|
+
]);
|
|
93
|
+
/**
|
|
94
|
+
* When true, remote bridged calls outside {@link REMOTE_PRIVILEGE_ALLOWLIST} are
|
|
95
|
+
* logged once per `(nodeId, path)` per process — still admin, not refused.
|
|
96
|
+
*/
|
|
97
|
+
exports.REMOTE_PRIVILEGE_LOG_ONLY = false;
|
|
98
|
+
let remotePrivilegeLogSink = null;
|
|
99
|
+
/** Attach the warn sink used by {@link observeRemotePrivilege}. */
|
|
100
|
+
function setRemotePrivilegeLogSink(sink) {
|
|
101
|
+
remotePrivilegeLogSink = sink;
|
|
102
|
+
}
|
|
103
|
+
const remotePrivilegeObserved = new Set();
|
|
104
|
+
/** Clears dedupe state and the sink — tests only. */
|
|
105
|
+
function __resetRemotePrivilegeObservationForTests() {
|
|
106
|
+
remotePrivilegeObserved.clear();
|
|
107
|
+
remotePrivilegeLogSink = null;
|
|
108
|
+
}
|
|
109
|
+
function remotePrivilegeKey(nodeId, path) {
|
|
110
|
+
return `${nodeId}\0${path}`;
|
|
111
|
+
}
|
|
112
|
+
function warnRemotePrivilegeOnce(nodeId, path, message) {
|
|
113
|
+
const key = remotePrivilegeKey(nodeId, path);
|
|
114
|
+
if (remotePrivilegeObserved.has(key))
|
|
115
|
+
return;
|
|
116
|
+
remotePrivilegeObserved.add(key);
|
|
117
|
+
remotePrivilegeLogSink?.warn(message, {
|
|
118
|
+
meta: { nodeId, path, principal: `node:${nodeId}` },
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* One warn per `(nodeId, path)` while {@link REMOTE_PRIVILEGE_LOG_ONLY} is on.
|
|
123
|
+
* Call only after {@link REMOTE_DENIED_NAMESPACES} has passed.
|
|
124
|
+
*/
|
|
125
|
+
function observeRemotePrivilege(nodeId, path) {
|
|
126
|
+
if (!exports.REMOTE_PRIVILEGE_LOG_ONLY)
|
|
127
|
+
return;
|
|
128
|
+
warnRemotePrivilegeOnce(nodeId, path, `mesh privilege: node:${nodeId} called ${path} (log-only; still allowed)`);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* One warn per `(nodeId, path)` when enforcement refuses a path not on
|
|
132
|
+
* {@link REMOTE_PRIVILEGE_ALLOWLIST}. Call immediately before throwing.
|
|
133
|
+
*/
|
|
134
|
+
function observeRemotePrivilegeRefused(nodeId, path) {
|
|
135
|
+
warnRemotePrivilegeOnce(nodeId, path, `mesh privilege: node:${nodeId} called ${path} (refused; not on grant)`);
|
|
136
|
+
}
|
|
75
137
|
/**
|
|
76
138
|
* camelCase → kebab-case. Mirrors `toKebab` in `trpc-links.ts` so the
|
|
77
139
|
* action name matches what `brokerTransportLink` derives from `op.path`.
|
|
@@ -220,6 +282,15 @@ function buildCoreCapService(appRouter, census = (0, http_request_census_singlet
|
|
|
220
282
|
if (remoteNode !== null && exports.REMOTE_DENIED_NAMESPACES.has(namespace)) {
|
|
221
283
|
throw new moleculer_1.Errors.MoleculerClientError(`"${path}" is not callable from another node (caller "${remoteNode}")`, 403, exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE);
|
|
222
284
|
}
|
|
285
|
+
if (remoteNode !== null) {
|
|
286
|
+
if (exports.REMOTE_PRIVILEGE_LOG_ONLY) {
|
|
287
|
+
observeRemotePrivilege(remoteNode, path);
|
|
288
|
+
}
|
|
289
|
+
else if (!exports.REMOTE_PRIVILEGE_ALLOWLIST.has(path)) {
|
|
290
|
+
observeRemotePrivilegeRefused(remoteNode, path);
|
|
291
|
+
throw new moleculer_1.Errors.MoleculerClientError(`"${path}" is not on the remote privilege grant (caller "${remoteNode}")`, 403, exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
223
294
|
const invoke = resolveInvoker(callerFor(remoteNode === null ? 'mesh' : `node:${remoteNode}`), path);
|
|
224
295
|
if (invoke === null)
|
|
225
296
|
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,
|
|
@@ -644,6 +644,8 @@ class MoleculerService {
|
|
|
644
644
|
this.removeNodeFromRegistry(node.id);
|
|
645
645
|
// D802: a node authenticates again on its next connection.
|
|
646
646
|
this.meshGuard.forget(node.id);
|
|
647
|
+
// Release A: its next connection decides again whether it is `tls` or `clear`.
|
|
648
|
+
(0, system_1.getBrokerTlsTransporter)(this.broker)?.forgetPeer(node.id);
|
|
647
649
|
});
|
|
648
650
|
// D8 reverse-leg snapshot-reconcile: on every agent (re)connect, pull
|
|
649
651
|
// the agent's own readiness snapshot (`$agent-readiness.getSnapshot`)
|
|
@@ -690,6 +692,39 @@ class MoleculerService {
|
|
|
690
692
|
probe: (url, options) => this.streamProbe.probe(url, options),
|
|
691
693
|
probeField: (key, value) => this.streamProbe.probeField(key, value),
|
|
692
694
|
}));
|
|
695
|
+
// Release A: when system-config's MeshCaKeeper becomes ready it offers
|
|
696
|
+
// hub leaf material here; the transporter starts TLS on :6443 alongside
|
|
697
|
+
// clear :6000 so enrolled agents can dual-dial without splitting the cluster.
|
|
698
|
+
(0, system_1.setProcessMeshTlsEnabler)(async (material) => {
|
|
699
|
+
const tx = (0, system_1.getBrokerTlsTransporter)(this.broker);
|
|
700
|
+
if (tx === null) {
|
|
701
|
+
logger.warn('Mesh TLS material ready but broker transporter is not TlsTcpTransporter');
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
await tx.enableMeshTls(material);
|
|
705
|
+
logger.info('Mesh mTLS listener enabled (dual-port with clear TCP)');
|
|
706
|
+
});
|
|
707
|
+
// Release A cutover: System → Cluster reads each agent's transport and may
|
|
708
|
+
// close the clear port through this slot (never flipped by this wiring).
|
|
709
|
+
(0, system_1.setProcessMeshClearPortControl)({
|
|
710
|
+
listOnlineAgents: () => this.listAvailableNodeIds()
|
|
711
|
+
.filter((id) => id !== this.brokerSafe.nodeID && !id.includes('/'))
|
|
712
|
+
.map((nodeId) => ({
|
|
713
|
+
nodeId,
|
|
714
|
+
transport: (0, system_1.getBrokerTlsTransporter)(this.broker)?.peerTransport(nodeId) ?? 'clear',
|
|
715
|
+
})),
|
|
716
|
+
isTlsListening: () => (0, system_1.getBrokerTlsTransporter)(this.broker)?.isTlsListening() === true,
|
|
717
|
+
transportOf: (nodeId) => (0, system_1.getBrokerTlsTransporter)(this.broker)?.peerTransport(nodeId),
|
|
718
|
+
apply: async (mode) => {
|
|
719
|
+
const tx = (0, system_1.getBrokerTlsTransporter)(this.broker);
|
|
720
|
+
if (tx === null)
|
|
721
|
+
return;
|
|
722
|
+
if (mode === 'off')
|
|
723
|
+
tx.disableClearTransport();
|
|
724
|
+
else
|
|
725
|
+
await tx.enableClearTransport();
|
|
726
|
+
},
|
|
727
|
+
});
|
|
693
728
|
await this.brokerSafe.start();
|
|
694
729
|
logger.info('Moleculer broker started (TCP transport)');
|
|
695
730
|
this.meshGuard.start(() => this.listAvailableNodeIds());
|
|
@@ -1322,6 +1357,8 @@ class MoleculerService {
|
|
|
1322
1357
|
}
|
|
1323
1358
|
}
|
|
1324
1359
|
async onModuleDestroy() {
|
|
1360
|
+
(0, system_1.setProcessMeshTlsEnabler)(null);
|
|
1361
|
+
(0, system_1.setProcessMeshClearPortControl)(null);
|
|
1325
1362
|
this.meshGuard.stop();
|
|
1326
1363
|
if (this.agentReadinessPullTimer !== null) {
|
|
1327
1364
|
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.489",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"files": [
|
|
@@ -37,18 +37,18 @@
|
|
|
37
37
|
]
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@camstack/addon-admin-ui": "1.2.
|
|
41
|
-
"@camstack/addon-agent-ui": "1.2.
|
|
40
|
+
"@camstack/addon-admin-ui": "1.2.362",
|
|
41
|
+
"@camstack/addon-agent-ui": "1.2.197",
|
|
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.301",
|
|
47
47
|
"@camstack/addon-post-analysis": "1.2.348",
|
|
48
48
|
"@camstack/sdk": "1.2.128",
|
|
49
|
-
"@camstack/system": "1.2.
|
|
50
|
-
"@camstack/types": "1.2.
|
|
51
|
-
"@camstack/ui-library": "1.2.
|
|
49
|
+
"@camstack/system": "1.2.384",
|
|
50
|
+
"@camstack/types": "1.2.334",
|
|
51
|
+
"@camstack/ui-library": "1.2.255",
|
|
52
52
|
"@fastify/compress": "^9.0.0",
|
|
53
53
|
"@fastify/cookie": "^11.0.2",
|
|
54
54
|
"@fastify/cors": "^11.2.0",
|