@camstack/server 1.0.7 → 1.1.0
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/api/addon-upload.js +67 -8
- package/dist/api/deploy-stage-registry.js +60 -0
- package/dist/api/trpc/generated-cap-routers.js +9 -0
- package/dist/core/addon/addon-package.service.js +5 -3
- package/dist/core/addon/addon-registry.service.js +7 -3
- package/dist/core/addon/addon-settings-provider.js +7 -3
- package/dist/core/auth/auth.service.js +0 -3
- package/dist/core/config/config.service.js +0 -3
- package/dist/core/feature/feature.service.js +0 -3
- package/dist/core/moleculer/moleculer.service.js +5 -5
- package/dist/main.js +8 -8
- package/package.json +1 -1
package/dist/api/addon-upload.js
CHANGED
|
@@ -33,6 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getDeployStageRegistry = getDeployStageRegistry;
|
|
37
|
+
exports.registerDeployBundleRoute = registerDeployBundleRoute;
|
|
38
|
+
exports.buildHubHttpSource = buildHubHttpSource;
|
|
36
39
|
exports.registerAddonUploadRoute = registerAddonUploadRoute;
|
|
37
40
|
const fs = __importStar(require("node:fs"));
|
|
38
41
|
const path = __importStar(require("node:path"));
|
|
@@ -40,6 +43,34 @@ const os = __importStar(require("node:os"));
|
|
|
40
43
|
const node_child_process_1 = require("node:child_process");
|
|
41
44
|
const scope_access_js_1 = require("./trpc/scope-access.js");
|
|
42
45
|
const addon_package_service_js_1 = require("../core/addon/addon-package.service.js");
|
|
46
|
+
const deploy_stage_registry_js_1 = require("./deploy-stage-registry.js");
|
|
47
|
+
const deployStageRegistry = new deploy_stage_registry_js_1.DeployStageRegistry();
|
|
48
|
+
function getDeployStageRegistry() {
|
|
49
|
+
return deployStageRegistry;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* One-time-token route agents pull a staged addon tgz from. Bearer token is
|
|
53
|
+
* the per-deploy token minted in `DeployStageRegistry.stage()` and carried to
|
|
54
|
+
* the agent in the `$agent.deploy` `hub-http` source descriptor.
|
|
55
|
+
*/
|
|
56
|
+
function registerDeployBundleRoute(fastify) {
|
|
57
|
+
fastify.get('/api/addons/deploy-bundle/:id', (request, reply) => {
|
|
58
|
+
const auth = request.headers.authorization ?? '';
|
|
59
|
+
const token = auth.startsWith('Bearer ') ? auth.slice('Bearer '.length) : '';
|
|
60
|
+
if (!token) {
|
|
61
|
+
reply.status(401).send({ error: 'missing bearer token' });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const buffer = deployStageRegistry.consume(request.params.id, token);
|
|
65
|
+
if (!buffer) {
|
|
66
|
+
reply
|
|
67
|
+
.status(deployStageRegistry.peekExists(request.params.id) ? 401 : 404)
|
|
68
|
+
.send({ error: 'bundle not available' });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
reply.header('content-type', 'application/octet-stream').send(buffer);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
43
74
|
/**
|
|
44
75
|
* Validate a `cst_*` scoped token via the `user-management` cap singleton.
|
|
45
76
|
*
|
|
@@ -76,6 +107,20 @@ function isUploadAllowed(scoped) {
|
|
|
76
107
|
const TARBALL_EXTENSIONS = ['.tgz', '.tar.gz'];
|
|
77
108
|
const MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
78
109
|
const AGENT_DEPLOY_TIMEOUT_MS = 60_000;
|
|
110
|
+
const AGENT_DEPLOY_CONTROL_TIMEOUT_MS = 30_000;
|
|
111
|
+
function hubBundleBaseUrl() {
|
|
112
|
+
return process.env['CAMSTACK_HUB_PUBLIC_URL'] ?? 'https://127.0.0.1:4443';
|
|
113
|
+
}
|
|
114
|
+
function buildHubHttpSource(buffer, hubBaseUrl) {
|
|
115
|
+
const staged = getDeployStageRegistry().stage(buffer);
|
|
116
|
+
return {
|
|
117
|
+
kind: 'hub-http',
|
|
118
|
+
url: `${hubBaseUrl.replace(/\/$/, '')}/api/addons/deploy-bundle/${staged.id}`,
|
|
119
|
+
token: staged.token,
|
|
120
|
+
sha256: staged.sha256,
|
|
121
|
+
bytes: staged.bytes,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
79
124
|
function isTarball(filename) {
|
|
80
125
|
return TARBALL_EXTENSIONS.some((ext) => filename.endsWith(ext));
|
|
81
126
|
}
|
|
@@ -119,6 +164,7 @@ async function registerAddonUploadRoute(fastify, addonBridge, authService, molec
|
|
|
119
164
|
await fastify.register(Promise.resolve().then(() => __importStar(require('@fastify/multipart'))), {
|
|
120
165
|
limits: { fileSize: MAX_UPLOAD_BYTES },
|
|
121
166
|
});
|
|
167
|
+
registerDeployBundleRoute(fastify);
|
|
122
168
|
fastify.post('/api/addons/upload', async (request, reply) => {
|
|
123
169
|
const authHeader = request.headers.authorization;
|
|
124
170
|
if (!authHeader) {
|
|
@@ -206,11 +252,12 @@ async function registerAddonUploadRoute(fastify, addonBridge, authService, molec
|
|
|
206
252
|
// the hub AND broadcasts to every connected agent that can run any of
|
|
207
253
|
// the package's addons (i.e. anything not marked hub-only). Any other
|
|
208
254
|
// explicit `nodeId` value routes only to that agent via `$agent.deploy`.
|
|
255
|
+
const baseUrl = hubBundleBaseUrl();
|
|
209
256
|
if (!nodeId || nodeId === 'hub') {
|
|
210
|
-
return installToHub(reply, addonBridge, addonRegistry, addonPackageService, moleculer, logger, data.filename, buffer);
|
|
257
|
+
return installToHub(reply, addonBridge, addonRegistry, addonPackageService, moleculer, logger, data.filename, buffer, baseUrl);
|
|
211
258
|
}
|
|
212
259
|
const agentAddonId = addonIdHint ?? manifest.name;
|
|
213
|
-
return deployToAgent(reply, moleculer, nodeId, agentAddonId, buffer);
|
|
260
|
+
return deployToAgent(reply, moleculer, nodeId, agentAddonId, buffer, baseUrl);
|
|
214
261
|
});
|
|
215
262
|
}
|
|
216
263
|
/**
|
|
@@ -248,7 +295,7 @@ function packageHasAgentDeployable(addonsDir, packageName) {
|
|
|
248
295
|
* without an agent restart. Agents that fail are reported per-node — one
|
|
249
296
|
* unreachable agent must not block the others or the hub install.
|
|
250
297
|
*/
|
|
251
|
-
async function propagateToAgents(moleculer, logger, packageName, buffer) {
|
|
298
|
+
async function propagateToAgents(moleculer, logger, packageName, buffer, hubBaseUrl) {
|
|
252
299
|
const broker = moleculer.broker;
|
|
253
300
|
const nodes = broker.registry?.getNodeList?.({ onlyAvailable: true }) ?? [];
|
|
254
301
|
// Moleculer reports both top-level nodes (`hub`, `dev-agent-0`, …) AND
|
|
@@ -270,7 +317,13 @@ async function propagateToAgents(moleculer, logger, packageName, buffer) {
|
|
|
270
317
|
const results = [];
|
|
271
318
|
for (const nodeId of agentNodeIds) {
|
|
272
319
|
try {
|
|
273
|
-
|
|
320
|
+
// The hub currently only ever sends the `hub-http` source (agent streams
|
|
321
|
+
// the tgz directly from this hub). The `{kind:'npm'}` source is a reserved
|
|
322
|
+
// follow-up (published-addon optimization) and is intentionally not produced
|
|
323
|
+
// yet. Wiring it later must also make the agent's npm path evict the addon's
|
|
324
|
+
// declaration ids before `$agent.reload`, otherwise an npm update pins the
|
|
325
|
+
// stale version.
|
|
326
|
+
const deployRaw = await broker.call('$agent.deploy', { addonId: packageName, source: buildHubHttpSource(buffer, hubBaseUrl) }, { nodeID: nodeId, timeout: AGENT_DEPLOY_CONTROL_TIMEOUT_MS });
|
|
274
327
|
if (!isAgentDeployResponse(deployRaw)) {
|
|
275
328
|
results.push({ nodeId, success: false, error: 'malformed deploy response' });
|
|
276
329
|
continue;
|
|
@@ -302,7 +355,7 @@ async function propagateToAgents(moleculer, logger, packageName, buffer) {
|
|
|
302
355
|
* Without this the CLI push was write-to-disk-only and required a server
|
|
303
356
|
* restart to actually run the new code.
|
|
304
357
|
*/
|
|
305
|
-
async function installToHub(reply, addonBridge, addonRegistry, addonPackageService, moleculer, logger, filename, buffer) {
|
|
358
|
+
async function installToHub(reply, addonBridge, addonRegistry, addonPackageService, moleculer, logger, filename, buffer, hubBaseUrl) {
|
|
306
359
|
const tmpDir = path.join(os.tmpdir(), `camstack-addon-upload-${Date.now()}`);
|
|
307
360
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
308
361
|
const tgzPath = path.join(tmpDir, filename);
|
|
@@ -401,7 +454,7 @@ async function installToHub(reply, addonBridge, addonRegistry, addonPackageServi
|
|
|
401
454
|
});
|
|
402
455
|
}
|
|
403
456
|
if (propagatable) {
|
|
404
|
-
void propagateToAgents(moleculer, logger, result.name, buffer).then((agentResults) => {
|
|
457
|
+
void propagateToAgents(moleculer, logger, result.name, buffer, hubBaseUrl).then((agentResults) => {
|
|
405
458
|
logger.info('propagation done', {
|
|
406
459
|
meta: { packageName: result.name, agents: agentResults },
|
|
407
460
|
});
|
|
@@ -434,10 +487,16 @@ function isAgentDeployResponse(value) {
|
|
|
434
487
|
return false;
|
|
435
488
|
return true;
|
|
436
489
|
}
|
|
437
|
-
async function deployToAgent(reply, moleculer, nodeId, addonId, buffer) {
|
|
490
|
+
async function deployToAgent(reply, moleculer, nodeId, addonId, buffer, hubBaseUrl) {
|
|
438
491
|
try {
|
|
439
492
|
const broker = moleculer.broker;
|
|
440
|
-
|
|
493
|
+
// The hub currently only ever sends the `hub-http` source (agent streams
|
|
494
|
+
// the tgz directly from this hub). The `{kind:'npm'}` source is a reserved
|
|
495
|
+
// follow-up (published-addon optimization) and is intentionally not produced
|
|
496
|
+
// yet. Wiring it later must also make the agent's npm path evict the addon's
|
|
497
|
+
// declaration ids before `$agent.reload`, otherwise an npm update pins the
|
|
498
|
+
// stale version.
|
|
499
|
+
const raw = await broker.call('$agent.deploy', { addonId, source: buildHubHttpSource(buffer, hubBaseUrl) }, { nodeID: nodeId, timeout: AGENT_DEPLOY_CONTROL_TIMEOUT_MS });
|
|
441
500
|
if (!isAgentDeployResponse(raw)) {
|
|
442
501
|
return reply.status(502).send({ error: 'Agent deploy returned malformed response' });
|
|
443
502
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DeployStageRegistry = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
/**
|
|
6
|
+
* In-memory, single-use, TTL-bounded store of staged addon tarballs the hub
|
|
7
|
+
* serves to agents over HTTP. One entry per deploy target; the token is
|
|
8
|
+
* consumed on the first successful read so a bundle URL is never reusable.
|
|
9
|
+
*/
|
|
10
|
+
class DeployStageRegistry {
|
|
11
|
+
entries = new Map();
|
|
12
|
+
ttlMs;
|
|
13
|
+
now;
|
|
14
|
+
constructor(opts) {
|
|
15
|
+
this.ttlMs = opts?.ttlMs ?? 300_000;
|
|
16
|
+
this.now = opts?.now ?? Date.now;
|
|
17
|
+
}
|
|
18
|
+
stage(buffer) {
|
|
19
|
+
// Reclaim abandoned (never-consumed) entries before inserting a new one.
|
|
20
|
+
this.sweepExpired();
|
|
21
|
+
const id = (0, node_crypto_1.randomUUID)();
|
|
22
|
+
const token = (0, node_crypto_1.randomUUID)();
|
|
23
|
+
const sha256 = (0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex');
|
|
24
|
+
this.entries.set(id, { token, buffer, sha256, expiresAt: this.now() + this.ttlMs });
|
|
25
|
+
return { id, token, sha256, bytes: buffer.length };
|
|
26
|
+
}
|
|
27
|
+
consume(id, token) {
|
|
28
|
+
const entry = this.entries.get(id);
|
|
29
|
+
if (!entry)
|
|
30
|
+
return null;
|
|
31
|
+
if (this.now() > entry.expiresAt) {
|
|
32
|
+
this.entries.delete(id);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
// Constant-time comparison: different byte lengths → not equal (no timing oracle).
|
|
36
|
+
const entryBuf = Buffer.from(entry.token);
|
|
37
|
+
const tokenBuf = Buffer.from(token);
|
|
38
|
+
if (entryBuf.length !== tokenBuf.length || !(0, node_crypto_1.timingSafeEqual)(entryBuf, tokenBuf))
|
|
39
|
+
return null;
|
|
40
|
+
this.entries.delete(id); // single-use
|
|
41
|
+
return entry.buffer;
|
|
42
|
+
}
|
|
43
|
+
peekExists(id) {
|
|
44
|
+
const e = this.entries.get(id);
|
|
45
|
+
if (!e)
|
|
46
|
+
return false;
|
|
47
|
+
if (this.now() > e.expiresAt) {
|
|
48
|
+
this.entries.delete(id);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
sweepExpired() {
|
|
54
|
+
const t = this.now();
|
|
55
|
+
for (const [id, e] of this.entries)
|
|
56
|
+
if (t > e.expiresAt)
|
|
57
|
+
this.entries.delete(id);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.DeployStageRegistry = DeployStageRegistry;
|
|
@@ -3913,6 +3913,15 @@ function createCapRouter_metricsProvider(getProvider, createRemoteProxy) {
|
|
|
3913
3913
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
3914
3914
|
return p.killProcess(methodInput);
|
|
3915
3915
|
}),
|
|
3916
|
+
dumpHeapSnapshot: trpc_middleware_js_1.adminProcedure
|
|
3917
|
+
.input(types_55.metricsProviderCapability.methods.dumpHeapSnapshot.input.loose())
|
|
3918
|
+
.output(types_55.metricsProviderCapability.methods.dumpHeapSnapshot.output)
|
|
3919
|
+
.mutation(async ({ input, ctx }) => {
|
|
3920
|
+
const { nodeId, ...methodInput } = input;
|
|
3921
|
+
const p = resolveProvider('metrics-provider', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
3922
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
3923
|
+
return p.dumpHeapSnapshot(methodInput);
|
|
3924
|
+
}),
|
|
3916
3925
|
});
|
|
3917
3926
|
}
|
|
3918
3927
|
function createCapRouter_motion(getProvider, createRemoteProxy) {
|
|
@@ -793,11 +793,11 @@ class AddonPackageService {
|
|
|
793
793
|
// hub/node_modules/@camstack/ — addons resolve everything from
|
|
794
794
|
// their own inlined deps.
|
|
795
795
|
const addonsDir = this.resolveAddonsDir();
|
|
796
|
-
const {
|
|
796
|
+
const { installPackageFromNpm } = await Promise.resolve().then(() => __importStar(require('@camstack/system')));
|
|
797
797
|
const dirName = name.replace(/^@camstack\//, '');
|
|
798
798
|
const targetDir = path.join(addonsDir, dirName);
|
|
799
799
|
const packageSpec = version ? `${name}@${version}` : name;
|
|
800
|
-
|
|
800
|
+
await installPackageFromNpm(packageSpec, targetDir);
|
|
801
801
|
updatedVersion = this.getInstalledPackageVersion(name);
|
|
802
802
|
// Invalidate cache after update
|
|
803
803
|
this.cachedUpdates = null;
|
|
@@ -1794,7 +1794,9 @@ async function packTarball(pkg, version, destRoot, registry) {
|
|
|
1794
1794
|
await execFileAsync('npm', args, { timeout: 60_000, killSignal: 'SIGKILL' });
|
|
1795
1795
|
const tgz = fs.readdirSync(dir).find((f) => f.endsWith('.tgz'));
|
|
1796
1796
|
if (tgz === undefined) {
|
|
1797
|
-
throw new Error(`download failed for ${pkg}@${version}: ${(0, types_1.errMsg)(httpErr)}
|
|
1797
|
+
throw new Error(`download failed for ${pkg}@${version}: ${(0, types_1.errMsg)(httpErr)}`, {
|
|
1798
|
+
cause: httpErr,
|
|
1799
|
+
});
|
|
1798
1800
|
}
|
|
1799
1801
|
return path.join(dir, tgz);
|
|
1800
1802
|
}
|
|
@@ -119,14 +119,14 @@ function parseSerializableRouteDescriptors(raw) {
|
|
|
119
119
|
throw new Error('addon-routes: route descriptor is not an object');
|
|
120
120
|
}
|
|
121
121
|
const method = Reflect.get(entry, 'method');
|
|
122
|
-
const
|
|
123
|
-
if (typeof method !== 'string' || typeof
|
|
122
|
+
const routePath = Reflect.get(entry, 'path');
|
|
123
|
+
if (typeof method !== 'string' || typeof routePath !== 'string') {
|
|
124
124
|
throw new Error('addon-routes: route descriptor missing method/path');
|
|
125
125
|
}
|
|
126
126
|
const description = Reflect.get(entry, 'description');
|
|
127
127
|
return {
|
|
128
128
|
method: asRouteMethod(method),
|
|
129
|
-
path,
|
|
129
|
+
path: routePath,
|
|
130
130
|
access: asRouteAccess(Reflect.get(entry, 'access')),
|
|
131
131
|
...(typeof description === 'string' ? { description } : {}),
|
|
132
132
|
};
|
|
@@ -2431,6 +2431,10 @@ class AddonRegistryService {
|
|
|
2431
2431
|
capabilityRegistry: this.capabilityRegistry,
|
|
2432
2432
|
streamProbe: kernelStreamProbe,
|
|
2433
2433
|
});
|
|
2434
|
+
// Captured for use inside the `ctx` object-literal getters/methods below
|
|
2435
|
+
// (e.g. `get api()`), where `this` rebinds to the literal — an arrow can't
|
|
2436
|
+
// be used for an accessor, so an explicit alias is required.
|
|
2437
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
2434
2438
|
const registry = this;
|
|
2435
2439
|
const rr = this.moleculer.readinessRegistry;
|
|
2436
2440
|
const capHandleCache = new Map();
|
|
@@ -58,12 +58,16 @@ function createAddonSettingsProvider(deps) {
|
|
|
58
58
|
const addon = getAddon(input.addonId);
|
|
59
59
|
if (!addon || typeof addon.getGlobalSettings !== 'function')
|
|
60
60
|
return null;
|
|
61
|
-
|
|
61
|
+
// Pass the REQUESTED nodeId so an addon serving a cluster-central,
|
|
62
|
+
// per-node config (the store is hub-resident) can scope to that node —
|
|
63
|
+
// the hub addon answers for every node, so it must not assume "self".
|
|
64
|
+
const result = await addon.getGlobalSettings(input.overlay, input.cap, input.nodeId);
|
|
62
65
|
return result ? reshapeForOutput(result) : null;
|
|
63
66
|
}
|
|
64
67
|
return forkedGet(input.addonId, 'getGlobalSettings', {
|
|
65
68
|
...(input.overlay ? { overlay: input.overlay } : {}),
|
|
66
69
|
...(input.cap ? { cap: input.cap } : {}),
|
|
70
|
+
...(input.nodeId ? { nodeId: input.nodeId } : {}),
|
|
67
71
|
}, input.nodeId);
|
|
68
72
|
},
|
|
69
73
|
async updateGlobalSettings(input) {
|
|
@@ -72,10 +76,10 @@ function createAddonSettingsProvider(deps) {
|
|
|
72
76
|
if (!addon || typeof addon.updateGlobalSettings !== 'function') {
|
|
73
77
|
throw new Error(`Addon "${input.addonId}" does not implement updateGlobalSettings`);
|
|
74
78
|
}
|
|
75
|
-
await addon.updateGlobalSettings(input.patch);
|
|
79
|
+
await addon.updateGlobalSettings(input.patch, input.nodeId);
|
|
76
80
|
return { success: true };
|
|
77
81
|
}
|
|
78
|
-
return forkedUpdate(input.addonId, 'updateGlobalSettings', { patch: input.patch }, input.nodeId);
|
|
82
|
+
return forkedUpdate(input.addonId, 'updateGlobalSettings', { patch: input.patch, ...(input.nodeId ? { nodeId: input.nodeId } : {}) }, input.nodeId);
|
|
79
83
|
},
|
|
80
84
|
async getDeviceSettings(input) {
|
|
81
85
|
if (gateway.isInProcess(input.addonId, input.nodeId)) {
|
|
@@ -3,8 +3,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.AuthService = void 0;
|
|
4
4
|
const system_1 = require("@camstack/system");
|
|
5
5
|
class AuthService extends system_1.AuthManager {
|
|
6
|
-
constructor(config) {
|
|
7
|
-
super(config);
|
|
8
|
-
}
|
|
9
6
|
}
|
|
10
7
|
exports.AuthService = AuthService;
|
|
@@ -3,8 +3,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ConfigService = void 0;
|
|
4
4
|
const system_1 = require("@camstack/system");
|
|
5
5
|
class ConfigService extends system_1.ConfigManager {
|
|
6
|
-
constructor(configPath) {
|
|
7
|
-
super(configPath);
|
|
8
|
-
}
|
|
9
6
|
}
|
|
10
7
|
exports.ConfigService = ConfigService;
|
|
@@ -3,8 +3,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.FeatureService = void 0;
|
|
4
4
|
const system_1 = require("@camstack/system");
|
|
5
5
|
class FeatureService extends system_1.FeatureManager {
|
|
6
|
-
constructor(configService) {
|
|
7
|
-
super(configService);
|
|
8
|
-
}
|
|
9
6
|
}
|
|
10
7
|
exports.FeatureService = FeatureService;
|
|
@@ -266,18 +266,18 @@ class MoleculerService {
|
|
|
266
266
|
// double-apply is idempotent (same nodeId + same caps → no-op on the second call).
|
|
267
267
|
registry.onChildRegistered((child) => {
|
|
268
268
|
const hubNodeId = this.brokerSafe.nodeID;
|
|
269
|
-
const
|
|
270
|
-
const params = buildChildUdsManifest(
|
|
269
|
+
const childNodeId = `${hubNodeId}/${child.childId}`;
|
|
270
|
+
const params = buildChildUdsManifest(childNodeId, child.childId, child.caps);
|
|
271
271
|
this.onRegisterNode(params);
|
|
272
|
-
logger.info('UDS child registered — manifest applied', { meta: { nodeId } });
|
|
272
|
+
logger.info('UDS child registered — manifest applied', { meta: { nodeId: childNodeId } });
|
|
273
273
|
});
|
|
274
274
|
// E1: cleanup on child disconnect — same effect as `$node.disconnected`
|
|
275
275
|
// for hub-local children. The Moleculer path stays for AGENT nodes.
|
|
276
276
|
registry.onChildGone((childId) => {
|
|
277
277
|
const hubNodeId = this.brokerSafe.nodeID;
|
|
278
|
-
const
|
|
278
|
+
const childNodeId = `${hubNodeId}/${childId}`;
|
|
279
279
|
logger.info('UDS child gone — removing from registry', { meta: { childId } });
|
|
280
|
-
this.removeNodeFromRegistry(
|
|
280
|
+
this.removeNodeFromRegistry(childNodeId);
|
|
281
281
|
});
|
|
282
282
|
// B2: ingest UDS child logs into the hub's LoggingService so they appear
|
|
283
283
|
// in the LogManager / admin-UI log stream alongside broker-forwarded logs.
|
package/dist/main.js
CHANGED
|
@@ -358,10 +358,10 @@ async function bootstrap() {
|
|
|
358
358
|
trpcOptions: {
|
|
359
359
|
router: appRouter,
|
|
360
360
|
createContext: ({ req }) => (0, trpc_context_1.createTrpcContext)(req, authService, addonRegistry),
|
|
361
|
-
onError: ({ path, error, }) => {
|
|
361
|
+
onError: ({ path: trpcPath, error, }) => {
|
|
362
362
|
const trpcLogger = app.get(logging_service_1.LoggingService).createLogger('tRPC');
|
|
363
363
|
trpcLogger.warn('tRPC error', {
|
|
364
|
-
meta: { code: error.code, path:
|
|
364
|
+
meta: { code: error.code, path: trpcPath ?? '?', message: error.message },
|
|
365
365
|
});
|
|
366
366
|
if (error.cause)
|
|
367
367
|
trpcLogger.warn('tRPC error cause', {
|
|
@@ -715,11 +715,11 @@ async function bootstrap() {
|
|
|
715
715
|
}
|
|
716
716
|
}
|
|
717
717
|
const qIdx = request.url.indexOf('?');
|
|
718
|
-
const
|
|
718
|
+
const queryString = qIdx >= 0 ? request.url.slice(qIdx) : '';
|
|
719
719
|
// Forward the FULL sub-path INCLUDING the prefix — the addon's facility
|
|
720
720
|
// multiplexes by prefix, so it strips the prefix itself. (`dpMatch.rest`
|
|
721
721
|
// is only used to pick the endpoint, not to rewrite the path.)
|
|
722
|
-
const upstreamPath = `/${subPath}${
|
|
722
|
+
const upstreamPath = `/${subPath}${queryString}`;
|
|
723
723
|
// Take over the socket — `proxyToUpstream` drives the raw response.
|
|
724
724
|
reply.hijack();
|
|
725
725
|
(0, system_1.proxyToUpstream)({
|
|
@@ -838,10 +838,10 @@ async function bootstrap() {
|
|
|
838
838
|
wss,
|
|
839
839
|
router: appRouter,
|
|
840
840
|
createContext: (opts) => (0, trpc_context_1.createWsTrpcContext)(opts, authService, addonRegistry),
|
|
841
|
-
onError: ({ path, error, }) => {
|
|
841
|
+
onError: ({ path: trpcPath, error, }) => {
|
|
842
842
|
const trpcLogger = app.get(logging_service_1.LoggingService).createLogger('tRPC:ws');
|
|
843
843
|
trpcLogger.warn('tRPC error', {
|
|
844
|
-
meta: { code: error.code, path:
|
|
844
|
+
meta: { code: error.code, path: trpcPath ?? '?', message: error.message },
|
|
845
845
|
});
|
|
846
846
|
if (error.cause)
|
|
847
847
|
trpcLogger.warn('tRPC error cause', {
|
|
@@ -899,8 +899,8 @@ async function bootstrap() {
|
|
|
899
899
|
// no static file serving' warn that left the SPA unserved until next
|
|
900
900
|
// restart.
|
|
901
901
|
try {
|
|
902
|
-
const
|
|
903
|
-
const capRegistry =
|
|
902
|
+
const bootAddonRegistry = app.get(addon_registry_service_1.AddonRegistryService);
|
|
903
|
+
const capRegistry = bootAddonRegistry.getCapabilityRegistry();
|
|
904
904
|
let adminUI = capRegistry?.getSingleton('admin-ui');
|
|
905
905
|
// CAMSTACK_SKIP_ADMIN_UI_WAIT — bypass the 60s poll. Used by the
|
|
906
906
|
// e2e harness, which doesn't need the SPA served and spawns hubs
|