@camstack/server 1.2.119 → 1.2.120
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.
|
|
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
|
|
225
|
-
//
|
|
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
|
-
|
|
233
|
-
? deviceScopeCovers(s, meta.access,
|
|
234
|
-
: exports.DEVICE_ENUMERATION_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
|
|
182
|
-
// overview/links and the merged events feed)
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
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.
|
|
3
|
+
"version": "1.2.120",
|
|
4
4
|
"private": false,
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@camstack/sdk": "1.2.19",
|
|
45
45
|
"@camstack/shm-ring": "1.1.16",
|
|
46
46
|
"@camstack/system": "1.2.96",
|
|
47
|
-
"@camstack/types": "1.2.
|
|
47
|
+
"@camstack/types": "1.2.79",
|
|
48
48
|
"@camstack/ui-library": "1.2.54",
|
|
49
49
|
"@fastify/compress": "^9.0.0",
|
|
50
50
|
"@fastify/cookie": "^11.0.2",
|