@camstack/server 1.2.119 → 1.2.121

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.
@@ -5,7 +5,6 @@ exports.isBroadEventViewer = isBroadEventViewer;
5
5
  exports.filterViewableDeviceIds = filterViewableDeviceIds;
6
6
  exports.hasUnrestrictedDeviceView = hasUnrestrictedDeviceView;
7
7
  exports.projectListAllForPrincipal = projectListAllForPrincipal;
8
- exports.projectBatchForPrincipal = projectBatchForPrincipal;
9
8
  exports.scopedEventVisibility = scopedEventVisibility;
10
9
  /**
11
10
  * Non-admin principal device visibility — the shared answer to "which cameras
@@ -133,39 +132,6 @@ function projectListAllForPrincipal(data, isAdmin, scopes, fleet) {
133
132
  const viewable = (0, types_1.resolveViewableDeviceIds)(scopes, fleet, 'view');
134
133
  return (0, share_view_access_js_1.projectListAllForDeviceIds)(data, viewable);
135
134
  }
136
- /**
137
- * Cut a DEVICE_BATCH_METHODS response to the devices the caller may view.
138
- *
139
- * Those methods answer "for THESE devices" where the request named the set
140
- * in an array (`deviceIds`, `targets[].deviceId`) — a shape the per-device
141
- * matcher cannot gate, so the answer is filtered here instead. Every one of
142
- * their rows carries a `deviceId` (snapshot overview/links rows, batched
143
- * event-kind rows) or nests the rows one level down (`listRecentTracks` →
144
- * `{ tracks, nextCursor }`, tracks carry `deviceId`). A row without a
145
- * readable `deviceId` is DROPPED — fail closed, same as the event filter.
146
- *
147
- * A caller with unrestricted device reach gets the payload untouched.
148
- */
149
- function projectBatchForPrincipal(data, isAdmin, scopes, fleet) {
150
- if (hasUnrestrictedDeviceView(isAdmin, scopes))
151
- return data;
152
- const viewable = (0, types_1.resolveViewableDeviceIds)(scopes, fleet, 'view');
153
- const rowVisible = (row) => {
154
- if (row === null || typeof row !== 'object')
155
- return false;
156
- const id = row['deviceId'];
157
- return typeof id === 'number' && viewable.has(id);
158
- };
159
- if (Array.isArray(data))
160
- return data.filter(rowVisible);
161
- if (data !== null && typeof data === 'object') {
162
- const tracks = data['tracks'];
163
- if (Array.isArray(tracks)) {
164
- return { ...data, tracks: tracks.filter(rowVisible) };
165
- }
166
- }
167
- return data;
168
- }
169
135
  /**
170
136
  * Build the event-stream predicate for a NON-share principal (F0.5).
171
137
  *
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEVICE_BATCH_METHODS = exports.DEVICE_ENUMERATION_METHODS = void 0;
3
+ exports.DEVICE_ENUMERATION_METHODS = void 0;
4
4
  exports.checkScopeAccess = checkScopeAccess;
5
5
  /**
6
6
  * Pure scope-access matcher.
@@ -102,31 +102,6 @@ function describeSelector(scope) {
102
102
  * `scoped-list-projection.spec.ts`.
103
103
  */
104
104
  exports.DEVICE_ENUMERATION_METHODS = new Set(['deviceManager.listAll']);
105
- /**
106
- * Device-scope methods that name their devices in BATCHES — an array of
107
- * deviceIds, or a positional targets array — instead of the single
108
- * top-level `deviceId` the `DeviceProxy` contract gives every other
109
- * device-scope method. `extractDeviceId` finds nothing in their input, so
110
- * without this set a device-granted caller could never reach them at all
111
- * (live regression, 2026-08-15: a non-admin viewer with `device:[all]`
112
- * received no snapshots and no events — `snapshot.getSnapshotLinks`,
113
- * `snapshot.getSnapshotOverview`, `listRecentTracks` and
114
- * `listEventKindsBatch` all failed closed).
115
- *
116
- * Same safety contract as {@link DEVICE_ENUMERATION_METHODS}, enforced by
117
- * the same middleware block: the RESPONSE is cut to the caller's viewable
118
- * set (`projectBatchForPrincipal`) before it leaves the server, so a
119
- * one-camera token can ask about the whole fleet and still only receive its
120
- * own rows. Adding a path here WITHOUT a projector in
121
- * `projectBatchForPrincipal` hands out the unfiltered answer — the pairing
122
- * is asserted by `scoped-list-projection.spec.ts`.
123
- */
124
- exports.DEVICE_BATCH_METHODS = new Set([
125
- 'snapshot.getSnapshotOverview',
126
- 'snapshot.getSnapshotLinks',
127
- 'pipelineAnalytics.listRecentTracks',
128
- 'pipelineAnalytics.listEventKindsBatch',
129
- ]);
130
105
  /**
131
106
  * Every deviceId a request NAMES, per the codegen'd
132
107
  * {@link METHOD_DEVICE_SELECTORS}. Absent / null / malformed values are
@@ -150,6 +125,15 @@ function referencedDeviceIds(path, input) {
150
125
  if (!Array.isArray(raw))
151
126
  continue;
152
127
  for (const item of raw) {
128
+ if (field.form === 'object-array') {
129
+ // `targets[].deviceId` & co. — one nesting level, by design.
130
+ if (item !== null && typeof item === 'object') {
131
+ const nested = Reflect.get(item, field.itemField ?? 'deviceId');
132
+ if (typeof nested === 'number' && Number.isFinite(nested))
133
+ out.push(nested);
134
+ }
135
+ continue;
136
+ }
153
137
  if (typeof item === 'number' && Number.isFinite(item))
154
138
  out.push(item);
155
139
  }
@@ -206,7 +190,6 @@ function checkScopeAccess(scopes, path, input, lookup) {
206
190
  return { ok: false, reason: `Unknown method '${path}' — codegen drift` };
207
191
  }
208
192
  const deviceId = meta.capScope === 'device' ? extractDeviceId(input) : null;
209
- const candidates = deviceId !== null ? candidateMetas(deviceId, lookup) : [];
210
193
  for (const s of scopes) {
211
194
  let targetMatches = false;
212
195
  switch (s.type) {
@@ -219,20 +202,28 @@ function checkScopeAccess(scopes, path, input, lookup) {
219
202
  case 'addon':
220
203
  targetMatches = meta.addonId !== null && s.target === meta.addonId;
221
204
  break;
222
- case 'device':
205
+ case 'device': {
223
206
  // A device grant matches either:
224
- // • a request naming a device, when the v3 selector covers it (or an
225
- // ancestor accessory inheritance, gated by `includeLinked`); or
207
+ // • a request NAMING devices in ANY shape the selector codegen
208
+ // recorded (top-level `deviceId`, `deviceIds[]`, or one-level
209
+ // nested `targets[].deviceId`) — when the v3 selector covers
210
+ // EVERY referenced device (or an ancestor, when the grant
211
+ // inherits). A single uncovered reference fails the call closed:
212
+ // the viewer's batches are built from its already-projected
213
+ // camera list, so a legit client never trips this, and a
214
+ // one-camera token cannot launder the fleet through an array.
226
215
  // • a DEVICE ENUMERATION method, which names no device precisely
227
216
  // because it asks "which are mine". Those are safe to reach ONLY
228
217
  // because the RESPONSE is cut to the caller's set (see
229
218
  // `DEVICE_ENUMERATION_METHODS`); without that projection this
230
219
  // branch would hand the whole fleet to a one-camera token.
220
+ const refs = meta.capScope === 'device' ? referencedDeviceIds(path, input) : [];
231
221
  targetMatches =
232
- deviceId !== null
233
- ? deviceScopeCovers(s, meta.access, candidates)
234
- : exports.DEVICE_ENUMERATION_METHODS.has(path) || exports.DEVICE_BATCH_METHODS.has(path);
222
+ refs.length > 0
223
+ ? refs.every((id) => deviceScopeCovers(s, meta.access, candidateMetas(id, lookup)))
224
+ : exports.DEVICE_ENUMERATION_METHODS.has(path);
235
225
  break;
226
+ }
236
227
  }
237
228
  if (!targetMatches)
238
229
  continue;
@@ -178,17 +178,11 @@ exports.protectedProcedure = t.procedure.use(async ({ ctx, next, path, getRawInp
178
178
  data: redactConfigSecrets ? redactConfigSecrets(projected) : projected,
179
179
  };
180
180
  }
181
- // Batch device methods (`deviceIds[]` / `targets[]` inputs — the snapshot
182
- // overview/links and the merged events feed) are reachable on a device
183
- // grant precisely because their answer is cut to the caller's viewable
184
- // set here. Reachability without this projection would be the leak.
185
- if (scope_access_js_1.DEVICE_BATCH_METHODS.has(path)) {
186
- const projected = (0, principal_visibility_js_1.projectBatchForPrincipal)(out.data, ctx.user.isAdmin, ctx.user.scopes ?? [], ctx.deviceFleet?.() ?? []);
187
- return {
188
- ...out,
189
- data: redactConfigSecrets ? redactConfigSecrets(projected) : projected,
190
- };
191
- }
181
+ // Batch device methods (`deviceIds[]` / `targets[].deviceId` inputs — the
182
+ // snapshot overview/links and the merged events feed) need no response
183
+ // projection: the matcher already required EVERY device the request names
184
+ // to be covered (see `referencedDeviceIds`), so the answer can only
185
+ // contain rows the caller is entitled to.
192
186
  if (redactConfigSecrets) {
193
187
  return { ...out, data: redactConfigSecrets(out.data) };
194
188
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.119",
3
+ "version": "1.2.121",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",
@@ -33,19 +33,19 @@
33
33
  ]
34
34
  },
35
35
  "dependencies": {
36
- "@camstack/addon-admin-ui": "1.2.64",
37
- "@camstack/addon-agent-ui": "1.2.17",
38
- "@camstack/addon-auth": "1.2.19",
39
- "@camstack/addon-decoder-nodeav": "1.2.16",
40
- "@camstack/addon-notifiers": "1.2.21",
41
- "@camstack/addon-pipeline": "1.2.85",
42
- "@camstack/addon-pipeline-orchestrator": "1.2.62",
43
- "@camstack/addon-post-analysis": "1.2.80",
44
- "@camstack/sdk": "1.2.19",
45
- "@camstack/shm-ring": "1.1.16",
46
- "@camstack/system": "1.2.96",
47
- "@camstack/types": "1.2.78",
48
- "@camstack/ui-library": "1.2.54",
36
+ "@camstack/addon-admin-ui": "1.2.65",
37
+ "@camstack/addon-agent-ui": "1.2.18",
38
+ "@camstack/addon-auth": "1.2.20",
39
+ "@camstack/addon-decoder-nodeav": "1.2.17",
40
+ "@camstack/addon-notifiers": "1.2.22",
41
+ "@camstack/addon-pipeline": "1.2.86",
42
+ "@camstack/addon-pipeline-orchestrator": "1.2.63",
43
+ "@camstack/addon-post-analysis": "1.2.81",
44
+ "@camstack/sdk": "1.2.20",
45
+ "@camstack/shm-ring": "1.1.17",
46
+ "@camstack/system": "1.2.97",
47
+ "@camstack/types": "1.2.80",
48
+ "@camstack/ui-library": "1.2.55",
49
49
  "@fastify/compress": "^9.0.0",
50
50
  "@fastify/cookie": "^11.0.2",
51
51
  "@fastify/cors": "^11.2.0",