@camstack/server 1.2.9 → 1.2.10

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.
@@ -41,6 +41,7 @@ exports.computeTopology = computeTopology;
41
41
  exports.createNodeRootPackageLookup = createNodeRootPackageLookup;
42
42
  exports.buildNodesProvider = buildNodesProvider;
43
43
  exports.buildIntegrationsProvider = buildIntegrationsProvider;
44
+ exports.dispatchCustomAction = dispatchCustomAction;
44
45
  exports.buildAddonsProvider = buildAddonsProvider;
45
46
  /* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access -- pre-existing lint debt across this 800-line provider-factory module. The flagged sites delegate into Moleculer/EventBus/IntegrationRegistry surfaces typed as `unknown` to break circular dependencies; runtime contracts are validated by the cap-mount-helper layer above. Tracked separately. */
46
47
  /**
@@ -972,6 +973,39 @@ function ensureCustomActionAuth(ctx, level) {
972
973
  return;
973
974
  }
974
975
  }
976
+ /**
977
+ * Resolve + dispatch a single addon custom action. This is the LIVE dispatch
978
+ * used by the `addons.custom` cap-provider handler below — factored out so it
979
+ * can be unit-tested directly against the real code path.
980
+ *
981
+ * Responsibilities (in order):
982
+ * 1. Resolve `(addonId, action)` against the process `CustomActionRegistry`.
983
+ * 2. Enforce the action's declared per-action auth (`ensureCustomActionAuth`).
984
+ * NOTE: the OUTER cap mount is `protected`, so unauthenticated callers are
985
+ * already rejected before this runs; the generic cap-router auth matrix
986
+ * covers that mount-level gate.
987
+ * 3. Validate input against the action's Zod schema.
988
+ * 4. Forward the authenticated caller only when the action opts in with
989
+ * `caller: 'required'` (derived server-side from the principal, never
990
+ * trusted from input).
991
+ * 5. Validate the addon's output — crash-early on a misbehaving addon.
992
+ */
993
+ async function dispatchCustomAction(registry, ctx, input) {
994
+ const entry = registry.resolve(input.addonId, input.action);
995
+ if (!entry) {
996
+ throw new server_1.TRPCError({
997
+ code: 'NOT_FOUND',
998
+ message: `addon '${input.addonId}' has no custom action '${input.action}'`,
999
+ });
1000
+ }
1001
+ ensureCustomActionAuth(ctx, entry.spec.auth);
1002
+ const parsedInput = entry.spec.input.parse(input.input);
1003
+ const caller = entry.spec.caller === 'required' && ctx.user
1004
+ ? { userId: ctx.user.id, isAdmin: ctx.user.isAdmin }
1005
+ : undefined;
1006
+ const result = await entry.handler(parsedInput, caller);
1007
+ return entry.spec.output.parse(result);
1008
+ }
975
1009
  /** A node id that refers to the hub itself (top-level or a hub group runner). */
976
1010
  function isHubNode(nodeId) {
977
1011
  return nodeId === 'hub' || nodeId.startsWith('hub/');
@@ -1163,20 +1197,7 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
1163
1197
  getJob: async (input) => lifecycleRunner.getJob(input.jobId),
1164
1198
  listJobs: async (input) => lifecycleRunner.listJobs({ activeOnly: input.activeOnly }),
1165
1199
  cancelJob: async (input) => lifecycleRunner.cancelJob(input.jobId),
1166
- custom: async (input) => {
1167
- const registry = ar.getCustomActionRegistry();
1168
- const entry = registry.resolve(input.addonId, input.action);
1169
- if (!entry) {
1170
- throw new server_1.TRPCError({
1171
- code: 'NOT_FOUND',
1172
- message: `addon '${input.addonId}' has no custom action '${input.action}'`,
1173
- });
1174
- }
1175
- ensureCustomActionAuth(ctx, entry.spec.auth);
1176
- const parsedInput = entry.spec.input.parse(input.input);
1177
- const result = await entry.handler(parsedInput);
1178
- return entry.spec.output.parse(result);
1179
- },
1200
+ custom: async (input) => dispatchCustomAction(ar.getCustomActionRegistry(), ctx, input),
1180
1201
  onAddonLogs: (input, push) => {
1181
1202
  const unsubscribe = ls.subscribe({ tags: { addonId: input.addonId }, level: input.level }, (entry) => {
1182
1203
  push({
@@ -2,7 +2,7 @@
2
2
  // AUTO-GENERATED by scripts/generate-cap-mounts.ts — DO NOT EDIT
3
3
  // Re-run: npx tsx scripts/generate-cap-mounts.ts
4
4
  //
5
- // Mounted: 140 Skipped (legacy): 3
5
+ // Mounted: 139 Skipped (legacy): 3
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.LEGACY_SHAPE_SKIP = void 0;
8
8
  exports.mountAllCaps = mountAllCaps;
@@ -74,8 +74,6 @@ function mountAllCaps(services) {
74
74
  }, remoteCapProxy),
75
75
  addons: (0, generated_cap_routers_js_1.createCapRouter_addons)((_ctx) => reg?.getSingleton('addons') ?? null, remoteCapProxy),
76
76
  adminUi: (0, generated_cap_routers_js_1.createCapRouter_adminUi)((_ctx) => reg?.getSingleton('admin-ui') ?? null, remoteCapProxy),
77
- advancedNotifier: (0, generated_cap_routers_js_1.createCapRouter_advancedNotifier)((_ctx) => reg?.getSingleton('advanced-notifier') ??
78
- null, remoteCapProxy),
79
77
  airQualitySensor: (0, generated_cap_routers_js_1.createCapRouter_airQualitySensor)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'air-quality-sensor'), remoteCapProxy),
80
78
  alarmPanel: (0, generated_cap_routers_js_1.createCapRouter_alarmPanel)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'alarm-panel'), remoteCapProxy),
81
79
  alerts: (0, generated_cap_routers_js_1.createCapRouter_alerts)((_ctx) => reg?.getSingleton('alerts') ?? null, remoteCapProxy),