@camstack/server 1.1.19 → 1.1.20
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/core/notifications.router.js +17 -37
- package/dist/api/trpc/generated-cap-mounts.js +10 -1
- package/dist/api/trpc/generated-cap-routers.js +61 -5
- package/dist/core/addon/addon-call-gateway.js +9 -2
- package/dist/core/addon/addon-package.service.js +3 -2
- package/dist/core/addon/addon-registry.service.js +20 -2
- package/dist/core/moleculer/moleculer.service.js +22 -0
- package/package.json +2 -1
|
@@ -4,64 +4,44 @@ exports.createNotificationsRouter = createNotificationsRouter;
|
|
|
4
4
|
/**
|
|
5
5
|
* Notifications router — fixed core API (not a capability).
|
|
6
6
|
*
|
|
7
|
-
* Routes events by category to
|
|
8
|
-
* is a singleton from @camstack/system;
|
|
9
|
-
* by addons via the `notification-output` capability
|
|
7
|
+
* Routes events by category to `(addonId, targetId)` notification targets.
|
|
8
|
+
* The NotificationService is a singleton from @camstack/system; the targets
|
|
9
|
+
* themselves are provided by addons via the `notification-output` capability
|
|
10
|
+
* collection (see the notifiers addon / HA addon). Target management + the
|
|
11
|
+
* test panel live on the cap surface (`api.notificationOutput.*`); this
|
|
12
|
+
* legacy router only exposes category→target routing.
|
|
10
13
|
*/
|
|
11
14
|
const zod_1 = require("zod");
|
|
12
15
|
const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
icon: zod_1.z.string(),
|
|
17
|
-
});
|
|
18
|
-
const SendTestResultSchema = zod_1.z.object({
|
|
19
|
-
success: zod_1.z.boolean(),
|
|
20
|
-
error: zod_1.z.string().optional(),
|
|
16
|
+
const NotificationTargetRefSchema = zod_1.z.object({
|
|
17
|
+
addonId: zod_1.z.string(),
|
|
18
|
+
targetId: zod_1.z.string(),
|
|
21
19
|
});
|
|
22
20
|
function createNotificationsRouter(ns) {
|
|
23
21
|
return (0, trpc_middleware_js_1.trpcRouter)({
|
|
24
|
-
listOutputs: trpc_middleware_js_1.protectedProcedure
|
|
25
|
-
.input(zod_1.z.void())
|
|
26
|
-
.output(zod_1.z.array(NotificationOutputSchema).readonly())
|
|
27
|
-
.query(() => {
|
|
28
|
-
if (!ns)
|
|
29
|
-
return [];
|
|
30
|
-
return ns.getOutputs();
|
|
31
|
-
}),
|
|
32
22
|
getRouting: trpc_middleware_js_1.protectedProcedure
|
|
33
23
|
.input(zod_1.z.void())
|
|
34
|
-
.output(zod_1.z.record(zod_1.z.string(), zod_1.z.array(
|
|
24
|
+
.output(zod_1.z.record(zod_1.z.string(), zod_1.z.array(NotificationTargetRefSchema)))
|
|
35
25
|
.query(() => {
|
|
36
26
|
if (!ns)
|
|
37
27
|
return {};
|
|
38
28
|
const routing = ns.getRouting();
|
|
39
29
|
const result = {};
|
|
40
|
-
for (const [category,
|
|
41
|
-
result[category] = [...
|
|
30
|
+
for (const [category, targets] of routing) {
|
|
31
|
+
result[category] = [...targets];
|
|
42
32
|
}
|
|
43
33
|
return result;
|
|
44
34
|
}),
|
|
45
35
|
setRouting: trpc_middleware_js_1.adminProcedure
|
|
46
|
-
.input(zod_1.z.object({
|
|
36
|
+
.input(zod_1.z.object({
|
|
37
|
+
category: zod_1.z.string(),
|
|
38
|
+
targets: zod_1.z.array(NotificationTargetRefSchema),
|
|
39
|
+
}))
|
|
47
40
|
.output(zod_1.z.void())
|
|
48
41
|
.mutation(({ input }) => {
|
|
49
42
|
if (!ns)
|
|
50
43
|
throw new Error('Notification service unavailable');
|
|
51
|
-
ns.setRouting(input.category, input.
|
|
52
|
-
}),
|
|
53
|
-
sendTest: trpc_middleware_js_1.adminProcedure
|
|
54
|
-
.input(zod_1.z.object({ outputId: zod_1.z.string() }))
|
|
55
|
-
.output(SendTestResultSchema)
|
|
56
|
-
.mutation(async ({ input }) => {
|
|
57
|
-
if (!ns)
|
|
58
|
-
return { success: false, error: 'Notification service unavailable' };
|
|
59
|
-
const output = ns.getOutput(input.outputId);
|
|
60
|
-
if (!output)
|
|
61
|
-
return { success: false, error: `Output "${input.outputId}" not found` };
|
|
62
|
-
if (!output.sendTest)
|
|
63
|
-
return { success: false, error: 'Output does not support test notifications' };
|
|
64
|
-
return output.sendTest();
|
|
44
|
+
ns.setRouting(input.category, input.targets);
|
|
65
45
|
}),
|
|
66
46
|
});
|
|
67
47
|
}
|
|
@@ -271,7 +271,16 @@ function mountAllCaps(services) {
|
|
|
271
271
|
return reg.getProviderByAddonId('notification-output', addonId);
|
|
272
272
|
}
|
|
273
273
|
const entries = reg.getCollectionEntries('notification-output');
|
|
274
|
-
|
|
274
|
+
if (entries.length === 0)
|
|
275
|
+
return null;
|
|
276
|
+
const providers = entries.map(([, p]) => p);
|
|
277
|
+
const first = providers[0];
|
|
278
|
+
return {
|
|
279
|
+
...first,
|
|
280
|
+
listTargetKinds: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listTargetKinds'),
|
|
281
|
+
listTargets: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listTargets'),
|
|
282
|
+
discoverTargets: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'discoverTargets'),
|
|
283
|
+
};
|
|
275
284
|
}, remoteCapProxy),
|
|
276
285
|
notifier: (0, generated_cap_routers_js_1.createCapRouter_notifier)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'notifier'), remoteCapProxy),
|
|
277
286
|
numericSensor: (0, generated_cap_routers_js_1.createCapRouter_numericSensor)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'numeric-sensor'), remoteCapProxy),
|
|
@@ -4469,6 +4469,33 @@ function createCapRouter_nodes(getProvider, _createRemoteProxy) {
|
|
|
4469
4469
|
}
|
|
4470
4470
|
function createCapRouter_notificationOutput(getProvider, createRemoteProxy) {
|
|
4471
4471
|
return (0, trpc_middleware_js_1.trpcRouter)({
|
|
4472
|
+
listTargetKinds: trpc_middleware_js_1.protectedProcedure
|
|
4473
|
+
.input(types_68.notificationOutputCapability.methods.listTargetKinds.input.loose())
|
|
4474
|
+
.output(types_68.notificationOutputCapability.methods.listTargetKinds.output)
|
|
4475
|
+
.query(async ({ input, ctx }) => {
|
|
4476
|
+
const { nodeId, addonId, ...methodInput } = input;
|
|
4477
|
+
const p = resolveProvider('notification-output', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
|
|
4478
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
4479
|
+
return p.listTargetKinds(methodInput);
|
|
4480
|
+
}),
|
|
4481
|
+
listTargets: trpc_middleware_js_1.protectedProcedure
|
|
4482
|
+
.input(types_68.notificationOutputCapability.methods.listTargets.input.loose())
|
|
4483
|
+
.output(types_68.notificationOutputCapability.methods.listTargets.output)
|
|
4484
|
+
.query(async ({ input, ctx }) => {
|
|
4485
|
+
const { nodeId, addonId, ...methodInput } = input;
|
|
4486
|
+
const p = resolveProvider('notification-output', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
|
|
4487
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
4488
|
+
return p.listTargets(methodInput);
|
|
4489
|
+
}),
|
|
4490
|
+
discoverTargets: trpc_middleware_js_1.protectedProcedure
|
|
4491
|
+
.input(types_68.notificationOutputCapability.methods.discoverTargets.input.loose())
|
|
4492
|
+
.output(types_68.notificationOutputCapability.methods.discoverTargets.output)
|
|
4493
|
+
.query(async ({ input, ctx }) => {
|
|
4494
|
+
const { nodeId, addonId, ...methodInput } = input;
|
|
4495
|
+
const p = resolveProvider('notification-output', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
|
|
4496
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
4497
|
+
return p.discoverTargets(methodInput);
|
|
4498
|
+
}),
|
|
4472
4499
|
send: trpc_middleware_js_1.protectedProcedure
|
|
4473
4500
|
.input(types_68.notificationOutputCapability.methods.send.input.loose())
|
|
4474
4501
|
.output(types_68.notificationOutputCapability.methods.send.output)
|
|
@@ -4478,12 +4505,41 @@ function createCapRouter_notificationOutput(getProvider, createRemoteProxy) {
|
|
|
4478
4505
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
4479
4506
|
return p.send(methodInput);
|
|
4480
4507
|
}),
|
|
4481
|
-
|
|
4482
|
-
.input(
|
|
4483
|
-
.output(types_68.notificationOutputCapability.methods.
|
|
4508
|
+
testTarget: trpc_middleware_js_1.protectedProcedure
|
|
4509
|
+
.input(types_68.notificationOutputCapability.methods.testTarget.input.loose())
|
|
4510
|
+
.output(types_68.notificationOutputCapability.methods.testTarget.output)
|
|
4511
|
+
.mutation(async ({ input, ctx }) => {
|
|
4512
|
+
const { nodeId, addonId, ...methodInput } = input;
|
|
4513
|
+
const p = resolveProvider('notification-output', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
|
|
4514
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
4515
|
+
return p.testTarget(methodInput);
|
|
4516
|
+
}),
|
|
4517
|
+
upsertTarget: trpc_middleware_js_1.protectedProcedure
|
|
4518
|
+
.input(types_68.notificationOutputCapability.methods.upsertTarget.input.loose())
|
|
4519
|
+
.output(types_68.notificationOutputCapability.methods.upsertTarget.output)
|
|
4520
|
+
.mutation(async ({ input, ctx }) => {
|
|
4521
|
+
const { nodeId, addonId, ...methodInput } = input;
|
|
4522
|
+
const p = resolveProvider('notification-output', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
|
|
4523
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
4524
|
+
return p.upsertTarget(methodInput);
|
|
4525
|
+
}),
|
|
4526
|
+
deleteTarget: trpc_middleware_js_1.protectedProcedure
|
|
4527
|
+
.input(types_68.notificationOutputCapability.methods.deleteTarget.input.loose())
|
|
4528
|
+
.output(types_68.notificationOutputCapability.methods.deleteTarget.output)
|
|
4484
4529
|
.mutation(async ({ input, ctx }) => {
|
|
4485
|
-
const
|
|
4486
|
-
|
|
4530
|
+
const { nodeId, addonId, ...methodInput } = input;
|
|
4531
|
+
const p = resolveProvider('notification-output', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
|
|
4532
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
4533
|
+
return p.deleteTarget(methodInput);
|
|
4534
|
+
}),
|
|
4535
|
+
setTargetEnabled: trpc_middleware_js_1.protectedProcedure
|
|
4536
|
+
.input(types_68.notificationOutputCapability.methods.setTargetEnabled.input.loose())
|
|
4537
|
+
.output(types_68.notificationOutputCapability.methods.setTargetEnabled.output)
|
|
4538
|
+
.mutation(async ({ input, ctx }) => {
|
|
4539
|
+
const { nodeId, addonId, ...methodInput } = input;
|
|
4540
|
+
const p = resolveProvider('notification-output', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
|
|
4541
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
4542
|
+
return p.setTargetEnabled(methodInput);
|
|
4487
4543
|
}),
|
|
4488
4544
|
});
|
|
4489
4545
|
}
|
|
@@ -18,7 +18,11 @@ class AddonCallGateway {
|
|
|
18
18
|
const onHub = resolved === 'hub' || resolved === this.deps.hubNodeId;
|
|
19
19
|
if (onHub) {
|
|
20
20
|
const childRegistry = this.deps.getChildRegistry();
|
|
21
|
-
|
|
21
|
+
// Resolve to the runner child id: a grouped addon's UDS child is the
|
|
22
|
+
// group name, not the addon id. `isChildKnown(addonId)` would be false
|
|
23
|
+
// for a grouped addon and misclassify it as `in-process`.
|
|
24
|
+
const childId = this.deps.resolveChildId(addonId);
|
|
25
|
+
if (childRegistry !== null && childRegistry.isChildKnown(childId)) {
|
|
22
26
|
return { kind: 'hub-local-child' };
|
|
23
27
|
}
|
|
24
28
|
return { kind: 'in-process' };
|
|
@@ -48,7 +52,10 @@ class AddonCallGateway {
|
|
|
48
52
|
if (childRegistry === null) {
|
|
49
53
|
throw new Error(`AddonCallGateway: child registry unavailable for "${addonId}"`);
|
|
50
54
|
}
|
|
51
|
-
|
|
55
|
+
// Dispatch to the runner CHILD id (group when grouped, else addon id);
|
|
56
|
+
// the payload keeps the real `addonId` so the child fans out to the
|
|
57
|
+
// right co-located addon within the group.
|
|
58
|
+
return childRegistry.callAddonOnChild(this.deps.resolveChildId(addonId), fullInput);
|
|
52
59
|
}
|
|
53
60
|
case 'remote-agent':
|
|
54
61
|
return this.callRemoteAgent(addonId, dest.baseNodeId, fullInput);
|
|
@@ -1674,8 +1674,9 @@ class AddonPackageService {
|
|
|
1674
1674
|
this.notificationService
|
|
1675
1675
|
.notify({
|
|
1676
1676
|
title: 'Package Updated',
|
|
1677
|
-
|
|
1678
|
-
|
|
1677
|
+
body: `${name} updated to v${version}`,
|
|
1678
|
+
format: 'text',
|
|
1679
|
+
priority: 3,
|
|
1679
1680
|
category: 'system',
|
|
1680
1681
|
timestamp: Date.now(),
|
|
1681
1682
|
})
|
|
@@ -359,10 +359,15 @@ class AddonRegistryService {
|
|
|
359
359
|
// forked-vs-in-process is decided by `isChildKnown` in the gateway.
|
|
360
360
|
return entry ? this.broker.nodeID : 'hub';
|
|
361
361
|
},
|
|
362
|
+
resolveChildId: (addonId) => this.childIdForAddon(addonId),
|
|
362
363
|
getChildRegistry: () => this.moleculer.childRegistry,
|
|
363
364
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- moleculer types unresolvable; see `broker` getter docstring
|
|
364
365
|
broker: this.moleculer.broker,
|
|
365
366
|
});
|
|
367
|
+
// Hand the UDS registry a live addonId → runner-id resolver so its
|
|
368
|
+
// active-singleton preference can select a grouped addon (whose child id
|
|
369
|
+
// is its `execution.group`, not its own id). Identity for ungrouped addons.
|
|
370
|
+
this.moleculer.setRunnerIdResolver((addonId) => this.childIdForAddon(addonId));
|
|
366
371
|
const settingsProvider = (0, addon_settings_provider_js_1.createAddonSettingsProvider)({
|
|
367
372
|
getAddon: (addonId) => {
|
|
368
373
|
const entry = this.addonEntries.get(addonId);
|
|
@@ -869,6 +874,19 @@ class AddonRegistryService {
|
|
|
869
874
|
* isn't a cap, but the child is UDS-reachable and has served its endpoints by
|
|
870
875
|
* the time any of its caps register.
|
|
871
876
|
*/
|
|
877
|
+
/**
|
|
878
|
+
* The UDS runner/child id an addon is reachable under. For a solo addon
|
|
879
|
+
* (no `execution.group`) this is the addon id itself; for a grouped addon
|
|
880
|
+
* it is the shared group name (`resolveRunnerId`). The single authority for
|
|
881
|
+
* "which child hosts this addon" on the parent side — every gateway /
|
|
882
|
+
* route-mount / data-plane-mount lookup routes through here so a grouped
|
|
883
|
+
* addon (`execution.group`) is never misclassified as `in-process`.
|
|
884
|
+
* Falls back to the addon id when the declaration is not yet known.
|
|
885
|
+
*/
|
|
886
|
+
childIdForAddon(addonId) {
|
|
887
|
+
const decl = this.addonEntries.get(addonId)?.declaration;
|
|
888
|
+
return decl ? (0, types_1.resolveRunnerId)(decl, addonId) : addonId;
|
|
889
|
+
}
|
|
872
890
|
async mountAddonDataPlanes(addonId) {
|
|
873
891
|
const registry = this.dataPlaneRegistry;
|
|
874
892
|
if (!registry)
|
|
@@ -880,7 +898,7 @@ class AddonRegistryService {
|
|
|
880
898
|
// data-plane yet).
|
|
881
899
|
if (!entry || !this.isForkedAddonEntry(entry))
|
|
882
900
|
return;
|
|
883
|
-
if (childRegistry === null || !childRegistry.isChildKnown(addonId))
|
|
901
|
+
if (childRegistry === null || !childRegistry.isChildKnown(this.childIdForAddon(addonId)))
|
|
884
902
|
return;
|
|
885
903
|
const raw = await this.addonCallGateway.callForked(addonId, { target: 'data-planes' });
|
|
886
904
|
const endpoints = parseDataPlaneEndpoints(raw);
|
|
@@ -2168,7 +2186,7 @@ class AddonRegistryService {
|
|
|
2168
2186
|
const entry = this.addonEntries.get(addonId);
|
|
2169
2187
|
const childRegistry = this.moleculer.childRegistry;
|
|
2170
2188
|
if (entry && this.isForkedAddonEntry(entry)) {
|
|
2171
|
-
if (childRegistry !== null && childRegistry.isChildKnown(addonId)) {
|
|
2189
|
+
if (childRegistry !== null && childRegistry.isChildKnown(this.childIdForAddon(addonId))) {
|
|
2172
2190
|
await this.mountForkedAddonRoutes(addonId, routeProvider, addonRouteRegistry);
|
|
2173
2191
|
return;
|
|
2174
2192
|
}
|
|
@@ -74,6 +74,23 @@ class MoleculerService {
|
|
|
74
74
|
* handler, preventing subscriber leaks on shutdown.
|
|
75
75
|
*/
|
|
76
76
|
udsEventBridgeDispose = null;
|
|
77
|
+
/**
|
|
78
|
+
* addonId → runner/child id resolver, injected late by
|
|
79
|
+
* `AddonRegistryService` (which owns the addon declarations). Lets the UDS
|
|
80
|
+
* `LocalChildRegistry` map the operator's active-singleton preference (an
|
|
81
|
+
* addonId) onto a candidate CHILD id — a grouped addon's child id is its
|
|
82
|
+
* `execution.group`, not its own id. Read live so a manifest change takes
|
|
83
|
+
* effect without rebuilding the registry. Null → identity (addonId ===
|
|
84
|
+
* childId), the correct default for every ungrouped addon.
|
|
85
|
+
*/
|
|
86
|
+
runnerIdResolver = null;
|
|
87
|
+
/**
|
|
88
|
+
* Late-wire the addonId → runner-id resolver used by the UDS singleton
|
|
89
|
+
* preference. Called once by `AddonRegistryService` during boot.
|
|
90
|
+
*/
|
|
91
|
+
setRunnerIdResolver(resolver) {
|
|
92
|
+
this.runnerIdResolver = resolver;
|
|
93
|
+
}
|
|
77
94
|
get childRegistry() {
|
|
78
95
|
return this.localChildRegistry;
|
|
79
96
|
}
|
|
@@ -287,6 +304,11 @@ class MoleculerService {
|
|
|
287
304
|
// registry on every call so a runtime swap takes effect
|
|
288
305
|
// immediately without rebuilding the resolver snapshot.
|
|
289
306
|
getActiveSingletonAddonId: (capName) => this.capabilityService.getRegistry()?.getSingletonAddonId(capName) ?? null,
|
|
307
|
+
// Map the preferred addonId to its runner CHILD id (group when the
|
|
308
|
+
// addon declares `execution.group`, else its own id) so a grouped
|
|
309
|
+
// addon stays selectable as the active singleton. Identity until
|
|
310
|
+
// `AddonRegistryService` injects the resolver.
|
|
311
|
+
resolveChildIdForAddon: (addonId) => this.runnerIdResolver?.(addonId) ?? addonId,
|
|
290
312
|
});
|
|
291
313
|
await registry.start();
|
|
292
314
|
// E1: apply child manifest + cleanup from the UDS lifecycle (hub-local children).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.20",
|
|
4
4
|
"private": false,
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@camstack/addon-admin-ui": "*",
|
|
26
26
|
"@camstack/addon-advanced-notifier": "*",
|
|
27
|
+
"@camstack/addon-notifiers": "*",
|
|
27
28
|
"@camstack/addon-pipeline": "*",
|
|
28
29
|
"@camstack/addon-pipeline-orchestrator": "*",
|
|
29
30
|
"@camstack/addon-post-analysis": "*",
|