@camstack/server 1.2.403 → 1.2.405

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.
@@ -26,6 +26,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.createEventBusProxyRouter = createEventBusProxyRouter;
27
27
  const zod_1 = require("zod");
28
28
  const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
29
+ const principal_visibility_js_1 = require("../trpc/principal-visibility.js");
30
+ const public_event_filter_js_1 = require("./public-event-filter.js");
29
31
  /**
30
32
  * Upper bound on the `getRecent` limit — kept in step with the hub's
31
33
  * `recent[]` ring-buffer size (`MAX_RECENT_EVENTS` in `event-bus-core.ts`).
@@ -111,10 +113,15 @@ function createEventBusProxyRouter(eventBus) {
111
113
  getRecent: trpc_middleware_js_1.protectedProcedure
112
114
  .input(GetRecentInputSchema)
113
115
  .output(zod_1.z.array(RecentEventOutputSchema))
114
- .query(({ input }) => {
115
- return eventBus
116
- .getRecent(input.category !== undefined ? { category: input.category } : undefined, input.limit)
117
- .map(serializeRecentEvent);
116
+ .query(({ input, ctx }) => {
117
+ // This router is on the PUBLIC app router too, not only the worker
118
+ // bridge: the same two projections as `systemEvents.getRecent` apply —
119
+ // owner-only categories are never served (D224) and a device-restricted
120
+ // scoped token reads only its devices (F0.5). A worker reaches this over
121
+ // the mesh context (`isAdmin: true`), so it skips only the SCOPE
122
+ // projection — owner-only categories are stripped for every caller.
123
+ const keep = (0, principal_visibility_js_1.scopedEventVisibility)(ctx.user, ctx.deviceScopeLookup);
124
+ return (0, public_event_filter_js_1.publicRecent)(eventBus, input.category !== undefined ? { category: input.category } : undefined, input.limit, keep).map(serializeRecentEvent);
118
125
  }),
119
126
  });
120
127
  }
@@ -11,6 +11,7 @@ const zod_1 = require("zod");
11
11
  const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
12
12
  const share_view_access_js_1 = require("../trpc/share-view-access.js");
13
13
  const principal_visibility_js_1 = require("../trpc/principal-visibility.js");
14
+ const public_event_filter_js_1 = require("./public-event-filter.js");
14
15
  // The pushed wire shape is `SystemEvent` from @camstack/types — the exact
15
16
  // type `EventBusService.subscribe` hands its handlers. It MUST be a type
16
17
  // exported from @camstack/types (not a local interface): the AppRouter
@@ -27,14 +28,15 @@ function createLiveEventsRouter(eb, ar) {
27
28
  })
28
29
  .optional())
29
30
  .query(({ input, ctx }) => {
30
- const recent = eb.getRecent(input ?? {}, input?.limit ?? 50);
31
31
  // SECURITY (F0.5): a device-restricted scoped token must not read the
32
32
  // whole deployment's event history. `scopedEventVisibility` returns null
33
33
  // for admins and broad category-view operators (no filtering), or a
34
34
  // per-event predicate that keeps only events for devices the caller may
35
35
  // view. (Share-view tokens cannot reach this method — not allowlisted.)
36
+ // An owner-only category (D224) is not history anyone may read. Both
37
+ // projections run BEFORE the limit (`publicRecent`).
36
38
  const keep = (0, principal_visibility_js_1.scopedEventVisibility)(ctx.user, ctx.deviceScopeLookup);
37
- return keep ? recent.filter((evt) => keep(evt)) : recent;
39
+ return (0, public_event_filter_js_1.publicRecent)(eb, input?.category ? { category: input.category } : {}, input?.limit ?? 50, keep);
38
40
  }),
39
41
  onEvent: trpc_middleware_js_1.protectedProcedure
40
42
  .input(zod_1.z.object({ category: zod_1.z.string().optional() }))
@@ -52,20 +54,25 @@ function createLiveEventsRouter(eb, ar) {
52
54
  const filter = {};
53
55
  if (input.category)
54
56
  filter.category = input.category;
57
+ // Every branch refuses an owner-only category first (D224): a
58
+ // subscriber that names no category gets every PUBLIC one.
55
59
  if (shareScope) {
56
60
  const allowedDeviceIds = new Set(shareScope.deviceIds);
57
61
  return eb.subscribe(filter, (evt) => {
58
- if ((0, share_view_access_js_1.liveEventInShareScope)(evt, allowedDeviceIds))
62
+ if ((0, public_event_filter_js_1.isPublicEvent)(evt) && (0, share_view_access_js_1.liveEventInShareScope)(evt, allowedDeviceIds))
59
63
  push(evt);
60
64
  });
61
65
  }
62
66
  if (scopedKeep) {
63
67
  return eb.subscribe(filter, (evt) => {
64
- if (scopedKeep(evt))
68
+ if ((0, public_event_filter_js_1.isPublicEvent)(evt) && scopedKeep(evt))
65
69
  push(evt);
66
70
  });
67
71
  }
68
- return eb.subscribe(filter, push);
72
+ return eb.subscribe(filter, (evt) => {
73
+ if ((0, public_event_filter_js_1.isPublicEvent)(evt))
74
+ push(evt);
75
+ });
69
76
  });
70
77
  }),
71
78
  onDeviceEvent: trpc_middleware_js_1.protectedProcedure
@@ -93,7 +100,10 @@ function createLiveEventsRouter(eb, ar) {
93
100
  throw new Error('Access denied');
94
101
  }
95
102
  }
96
- return eb.subscribe({ source: { type: 'device', id: input.deviceId } }, push);
103
+ return eb.subscribe({ source: { type: 'device', id: input.deviceId } }, (evt) => {
104
+ if ((0, public_event_filter_js_1.isPublicEvent)(evt))
105
+ push(evt);
106
+ });
97
107
  });
98
108
  }),
99
109
  });
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isPublicEvent = isPublicEvent;
4
+ exports.publicRecent = publicRecent;
5
+ /**
6
+ * The one predicate every public event procedure applies before an event
7
+ * leaves the hub (D224, D663): an owner-only category — today the native
8
+ * shadow of a claimed cap — is never served, whatever the caller's role or
9
+ * filter. A subscriber that omits `category` gets every PUBLIC category, not
10
+ * every category.
11
+ *
12
+ * `publicRecent` is the history half: it filters BEFORE the limit, so a
13
+ * client asking for N public events gets N — an owner-only row in the window
14
+ * never costs the caller a slot. The ring is bounded (`MAX_RECENT_EVENTS`), so
15
+ * reading it whole and cutting afterwards is one bounded pass.
16
+ */
17
+ const types_1 = require("@camstack/types");
18
+ function isPublicEvent(evt) {
19
+ return !(0, types_1.isOwnerOnlyEventCategory)(evt.category);
20
+ }
21
+ function publicRecent(source, filter, limit, keep) {
22
+ const visible = source
23
+ .getRecent(filter)
24
+ .filter((evt) => isPublicEvent(evt) && (keep === null || keep(evt)));
25
+ return limit !== undefined ? visible.slice(-limit) : visible;
26
+ }
@@ -12,6 +12,7 @@ exports.createSystemEventsRouter = createSystemEventsRouter;
12
12
  const zod_1 = require("zod");
13
13
  const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
14
14
  const principal_visibility_js_1 = require("../trpc/principal-visibility.js");
15
+ const public_event_filter_js_1 = require("./public-event-filter.js");
15
16
  function serialize(e) {
16
17
  return {
17
18
  id: e.id,
@@ -82,17 +83,18 @@ const SubscribeInputSchema = ScopeFieldsSchema.extend({
82
83
  function createSystemEventsRouter(eb) {
83
84
  return (0, trpc_middleware_js_1.trpcRouter)({
84
85
  getRecent: trpc_middleware_js_1.protectedProcedure.input(GetRecentInputSchema).query(({ input, ctx }) => {
85
- const recent = eb.getRecent({
86
+ // SECURITY (F0.5): filter the history to what a device-restricted scoped
87
+ // token may view. null ⇒ admin / broad category-view operator ⇒ unfiltered.
88
+ // An owner-only category (D224) is not history anyone may read. Both
89
+ // projections run BEFORE the limit, so N asked is N public rows answered.
90
+ const keep = (0, principal_visibility_js_1.scopedEventVisibility)(ctx.user, ctx.deviceScopeLookup);
91
+ return (0, public_event_filter_js_1.publicRecent)(eb, {
86
92
  ...(input.source ? { source: input.source } : {}),
87
93
  ...(input.agentId ? { agentId: input.agentId } : {}),
88
94
  ...(input.addonId ? { addonId: input.addonId } : {}),
89
95
  ...(input.deviceId !== undefined ? { deviceId: input.deviceId } : {}),
90
96
  ...(input.category ? { category: input.category } : {}),
91
- }, input.limit);
92
- // SECURITY (F0.5): filter the history to what a device-restricted scoped
93
- // token may view. null ⇒ admin / broad category-view operator ⇒ unfiltered.
94
- const keep = (0, principal_visibility_js_1.scopedEventVisibility)(ctx.user, ctx.deviceScopeLookup);
95
- return (keep ? recent.filter((evt) => keep(evt)) : recent).map(serialize);
97
+ }, input.limit, keep).map(serialize);
96
98
  }),
97
99
  subscribe: trpc_middleware_js_1.protectedProcedure.input(SubscribeInputSchema).subscription(({ input, ctx }) => {
98
100
  // SECURITY (F0.5): a device-restricted scoped token must not receive every
@@ -106,6 +108,8 @@ function createSystemEventsRouter(eb) {
106
108
  ...(input.deviceId !== undefined ? { deviceId: input.deviceId } : {}),
107
109
  ...(input.category ? { category: input.category } : {}),
108
110
  }, (event) => {
111
+ if (!(0, public_event_filter_js_1.isPublicEvent)(event))
112
+ return;
109
113
  if (!keep || keep(event))
110
114
  push(serialize(event));
111
115
  });
@@ -2053,6 +2053,15 @@ function createCapRouter_coreBlocks(getProvider, createRemoteProxy) {
2053
2053
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2054
2054
  return p.listCompositions(methodInput);
2055
2055
  }),
2056
+ getCustomization: trpc_middleware_js_1.adminProcedure
2057
+ .input(types_34.coreBlocksCapability.methods.getCustomization.input.loose())
2058
+ .output(types_34.coreBlocksCapability.methods.getCustomization.output)
2059
+ .query(async ({ input, ctx }) => {
2060
+ const { nodeId, ...methodInput } = input;
2061
+ const p = resolveProvider('core-blocks', nodeId, () => getProvider(ctx), createRemoteProxy);
2062
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2063
+ return p.getCustomization(methodInput);
2064
+ }),
2056
2065
  validateComposition: trpc_middleware_js_1.adminProcedure
2057
2066
  .input(types_34.coreBlocksCapability.methods.validateComposition.input.loose())
2058
2067
  .output(types_34.coreBlocksCapability.methods.validateComposition.output)
@@ -3737,6 +3746,51 @@ function createCapRouter_deviceState(getProvider, createRemoteProxy) {
3737
3746
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
3738
3747
  return p.setCapSlice(methodInput);
3739
3748
  }),
3749
+ claimFields: trpc_middleware_js_1.adminProcedure
3750
+ .input(types_48.deviceStateCapability.methods.claimFields.input.loose())
3751
+ .output(types_48.deviceStateCapability.methods.claimFields.output)
3752
+ .mutation(async ({ input, ctx }) => {
3753
+ const { nodeId, ...methodInput } = input;
3754
+ const p = resolveProvider('device-state', nodeId, () => getProvider(ctx), createRemoteProxy);
3755
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
3756
+ return p.claimFields(methodInput);
3757
+ }),
3758
+ patchOwnedFields: trpc_middleware_js_1.adminProcedure
3759
+ .input(types_48.deviceStateCapability.methods.patchOwnedFields.input.loose())
3760
+ .output(types_48.deviceStateCapability.methods.patchOwnedFields.output)
3761
+ .mutation(async ({ input, ctx }) => {
3762
+ const { nodeId, ...methodInput } = input;
3763
+ const p = resolveProvider('device-state', nodeId, () => getProvider(ctx), createRemoteProxy);
3764
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
3765
+ return p.patchOwnedFields(methodInput);
3766
+ }),
3767
+ releaseClaim: trpc_middleware_js_1.adminProcedure
3768
+ .input(types_48.deviceStateCapability.methods.releaseClaim.input.loose())
3769
+ .output(types_48.deviceStateCapability.methods.releaseClaim.output)
3770
+ .mutation(async ({ input, ctx }) => {
3771
+ const { nodeId, ...methodInput } = input;
3772
+ const p = resolveProvider('device-state', nodeId, () => getProvider(ctx), createRemoteProxy);
3773
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
3774
+ return p.releaseClaim(methodInput);
3775
+ }),
3776
+ listClaims: trpc_middleware_js_1.adminProcedure
3777
+ .input(types_48.deviceStateCapability.methods.listClaims.input.loose())
3778
+ .output(types_48.deviceStateCapability.methods.listClaims.output)
3779
+ .query(async ({ input, ctx }) => {
3780
+ const { nodeId, ...methodInput } = input;
3781
+ const p = resolveProvider('device-state', nodeId, () => getProvider(ctx), createRemoteProxy);
3782
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
3783
+ return p.listClaims(methodInput);
3784
+ }),
3785
+ getNativeCapSlice: trpc_middleware_js_1.adminProcedure
3786
+ .input(types_48.deviceStateCapability.methods.getNativeCapSlice.input.loose())
3787
+ .output(types_48.deviceStateCapability.methods.getNativeCapSlice.output)
3788
+ .query(async ({ input, ctx }) => {
3789
+ const { nodeId, ...methodInput } = input;
3790
+ const p = resolveProvider('device-state', nodeId, () => getProvider(ctx), createRemoteProxy);
3791
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
3792
+ return p.getNativeCapSlice(methodInput);
3793
+ }),
3740
3794
  });
3741
3795
  }
3742
3796
  function createCapRouter_deviceStatus(getProvider, createRemoteProxy) {
@@ -311,6 +311,12 @@ class MoleculerService {
311
311
  resolveNative: (capName, deviceId) => this.capabilityService
312
312
  .getRegistry()
313
313
  ?.getNativeProvider(capName, deviceId) ?? null,
314
+ // `getStatus` of a CLAIMED cap (D663, S3) — the hub mirror answers a
315
+ // forked child's `battery.getStatus({ deviceId })` (exporters, the
316
+ // notification center) before the resolver would dispatch it to the
317
+ // owning runner, whose `getStatus` is the native slice. The SAME
318
+ // question the registry's resolvers ask, through the same method.
319
+ claimedStatus: (capName, deviceId) => this.resolveClaimedStatus(capName, deviceId),
314
320
  // `nodeIdMode: 'data'` signal: for these caps (`addon-settings`,
315
321
  // `addons`, `nodes`, `pipeline-orchestrator`) an inline `args.nodeId`
316
322
  // is provider DATA — the hub singleton dispatches internally — never a
@@ -397,6 +403,10 @@ class MoleculerService {
397
403
  // onUnownedCall (a pin to ANOTHER node still bypasses the sibling).
398
404
  ownNodeId: nodeId,
399
405
  onUnownedCall,
406
+ // `getStatus` of a CLAIMED cap is answered by the hub mirror BEFORE the
407
+ // sibling short-circuit can dispatch it to the owning child (D663, S3)
408
+ // — the same resolver the handler above and `callCapabilityOnNode` use.
409
+ claimedStatus: (capName, deviceId) => this.resolveClaimedStatus(capName, deviceId),
400
410
  // A collection ARRAY method must reach `onUnownedCall` (which fans it
401
411
  // across every provider) instead of being short-circuited to ONE local
402
412
  // sibling. Same predicate the aggregate itself uses, so the two agree
@@ -719,6 +729,17 @@ class MoleculerService {
719
729
  registerCoreCapService(service) {
720
730
  this.brokerSafe.createService(service);
721
731
  }
732
+ /**
733
+ * The hub's claim authority (D663, S3): `CapabilityRegistry.resolveClaimedStatus`,
734
+ * which resolves the device's claims first. Every entry that routes a
735
+ * device-scoped `getStatus` by name asks it through `answerClaimedGetStatus`.
736
+ */
737
+ resolveClaimedStatus(capName, deviceId) {
738
+ const registry = this.capabilityService.getRegistry();
739
+ return registry !== null
740
+ ? registry.resolveClaimedStatus(capName, deviceId)
741
+ : Promise.resolve(system_1.UNCLAIMED);
742
+ }
722
743
  /**
723
744
  * Call a capability method on a specific node.
724
745
  *
@@ -737,6 +758,14 @@ class MoleculerService {
737
758
  * have run before the resolver was constructed).
738
759
  */
739
760
  async callCapabilityOnNode(nodeId, capabilityName, methodName, params) {
761
+ // `getStatus` of a CLAIMED cap — the hub mirror answers (D663, S3). This is
762
+ // the third entry that resolves a cap call by name (the hub's own tRPC with
763
+ // a `nodeId` pin — the generated DeviceProxy always sends one — and every
764
+ // hub in-process addon's `ctx.api` through `deviceCapResolverLink`); a pin
765
+ // does not bypass the claim authority. Same shared function as the other two.
766
+ const claimed = await (0, system_1.answerClaimedGetStatus)((capName, deviceId) => this.resolveClaimedStatus(capName, deviceId), { capName: capabilityName, method: methodName, args: params });
767
+ if (claimed !== null)
768
+ return claimed.slice;
740
769
  const resolver = this.resolver;
741
770
  if (resolver !== null) {
742
771
  // Extract deviceId from params so device-scoped native caps (ptz, motion-zones, …)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.403",
3
+ "version": "1.2.405",
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.280",
41
- "@camstack/addon-agent-ui": "1.2.152",
40
+ "@camstack/addon-admin-ui": "1.2.282",
41
+ "@camstack/addon-agent-ui": "1.2.154",
42
42
  "@camstack/addon-auth": "1.2.123",
43
43
  "@camstack/addon-decoder-nodeav": "1.2.115",
44
44
  "@camstack/addon-notifiers": "1.2.121",
45
45
  "@camstack/addon-pipeline": "1.2.304",
46
46
  "@camstack/addon-pipeline-orchestrator": "1.2.261",
47
- "@camstack/addon-post-analysis": "1.2.292",
47
+ "@camstack/addon-post-analysis": "1.2.293",
48
48
  "@camstack/sdk": "1.2.118",
49
- "@camstack/system": "1.2.313",
50
- "@camstack/types": "1.2.267",
51
- "@camstack/ui-library": "1.2.210",
49
+ "@camstack/system": "1.2.314",
50
+ "@camstack/types": "1.2.268",
51
+ "@camstack/ui-library": "1.2.212",
52
52
  "@fastify/compress": "^9.0.0",
53
53
  "@fastify/cookie": "^11.0.2",
54
54
  "@fastify/cors": "^11.2.0",