@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.
@@ -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 registered outputs. The NotificationService
8
- * is a singleton from @camstack/system; individual outputs are still provided
9
- * by addons via the `notification-output` capability collection.
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 NotificationOutputSchema = zod_1.z.object({
14
- id: zod_1.z.string(),
15
- name: zod_1.z.string(),
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(zod_1.z.string())))
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, outputIds] of routing) {
41
- result[category] = [...outputIds];
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({ category: zod_1.z.string(), outputIds: zod_1.z.array(zod_1.z.string()) }))
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.outputIds);
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
- return entries[0]?.[1] ?? null;
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
- sendTest: trpc_middleware_js_1.protectedProcedure
4482
- .input(zod_1.z.object({ nodeId: zod_1.z.string().optional(), addonId: zod_1.z.string().optional() }).optional())
4483
- .output(types_68.notificationOutputCapability.methods.sendTest.output)
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 p = resolveProvider('notification-output', input?.nodeId, () => getProvider(ctx, input?.addonId), createRemoteProxy);
4486
- return p.sendTest();
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
- if (childRegistry !== null && childRegistry.isChildKnown(addonId)) {
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
- return childRegistry.callAddonOnChild(addonId, fullInput);
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
- message: `${name} updated to v${version}`,
1678
- severity: 'info',
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.19",
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": "*",