@camstack/server 1.2.619 → 1.2.621

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.
@@ -224,6 +224,10 @@ const ScopeGrantSummarySchema = zod_1.z.object({
224
224
  */
225
225
  const EffectiveScopeSchema = zod_1.z.object({
226
226
  isAdmin: zod_1.z.boolean(),
227
+ /** `admin`, `viewer` (D921), or `scoped` — a legacy grant set not yet converted. */
228
+ role: zod_1.z.enum(['admin', 'viewer', 'scoped']),
229
+ /** The viewer features granted (D921); empty for every other role. */
230
+ features: zod_1.z.array(types_1.ViewerFeatureIdSchema).readonly(),
227
231
  allDevicesViewable: zod_1.z.boolean(),
228
232
  viewableDeviceCount: zod_1.z.number().int(),
229
233
  device: zod_1.z.object({
@@ -935,7 +939,7 @@ function createAuthRouter(auth, registry, moleculer = null, handoffCodes = new h
935
939
  */
936
940
  previewScope: trpc_middleware_js_1.adminProcedure
937
941
  .input(zod_1.z.object({
938
- scopes: zod_1.z.array(types_1.TokenScopeSchema),
942
+ scopes: zod_1.z.array(types_1.UserScopeSchema),
939
943
  isAdmin: zod_1.z.boolean().default(false),
940
944
  }))
941
945
  .output(EffectiveScopeSchema)
@@ -971,8 +975,10 @@ function createAuthRouter(auth, registry, moleculer = null, handoffCodes = new h
971
975
  username: ctx.user.username,
972
976
  password: input.currentPassword,
973
977
  });
978
+ // A stable code (D863): the change-password page names this case
979
+ // instead of showing whatever text arrived.
974
980
  if (!ok)
975
- throw new Error('Current password is incorrect');
981
+ throw (0, types_2.userError)('auth.password-incorrect', undefined, 'Password is incorrect');
976
982
  // Hub-internal: the password changes AND every other session of the
977
983
  // user ends (this one stays signed in).
978
984
  const internal = registry === null ? null : (0, hub_sessions_js_1.userManagementInternal)(registry);
@@ -8302,15 +8302,6 @@ function createCapRouter_pipelineExecutor(getProvider, createRemoteProxy) {
8302
8302
  const p = resolveProvider('pipeline-executor', input?.nodeId, () => getProvider(ctx), createRemoteProxy);
8303
8303
  return p.getSelectedEngine();
8304
8304
  }),
8305
- getDefaultSteps: trpc_middleware_js_1.protectedProcedure
8306
- .input(types_102.pipelineExecutorCapability.methods.getDefaultSteps.input.loose())
8307
- .output(types_102.pipelineExecutorCapability.methods.getDefaultSteps.output)
8308
- .query(async ({ input, ctx }) => {
8309
- const { nodeId, ...methodInput } = input;
8310
- const p = resolveProvider('pipeline-executor', nodeId, () => getProvider(ctx), createRemoteProxy);
8311
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
8312
- return p.getDefaultSteps(methodInput);
8313
- }),
8314
8305
  getEngineProvisioning: trpc_middleware_js_1.protectedProcedure
8315
8306
  .input(types_102.pipelineExecutorCapability.methods.getEngineProvisioning.input.loose())
8316
8307
  .output(types_102.pipelineExecutorCapability.methods.getEngineProvisioning.output)
@@ -8336,24 +8327,6 @@ function createCapRouter_pipelineExecutor(getProvider, createRemoteProxy) {
8336
8327
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
8337
8328
  return p.setVideoPipelineSteps(methodInput);
8338
8329
  }),
8339
- clearDeviceOverrides: trpc_middleware_js_1.adminProcedure
8340
- .input(types_102.pipelineExecutorCapability.methods.clearDeviceOverrides.input.loose())
8341
- .output(types_102.pipelineExecutorCapability.methods.clearDeviceOverrides.output)
8342
- .mutation(async ({ input, ctx }) => {
8343
- const { nodeId, ...methodInput } = input;
8344
- const p = resolveProvider('pipeline-executor', nodeId, () => getProvider(ctx), createRemoteProxy);
8345
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
8346
- return p.clearDeviceOverrides(methodInput);
8347
- }),
8348
- clearModelBoundDeviceOverrides: trpc_middleware_js_1.adminProcedure
8349
- .input(types_102.pipelineExecutorCapability.methods.clearModelBoundDeviceOverrides.input.loose())
8350
- .output(types_102.pipelineExecutorCapability.methods.clearModelBoundDeviceOverrides.output)
8351
- .mutation(async ({ input, ctx }) => {
8352
- const { nodeId, ...methodInput } = input;
8353
- const p = resolveProvider('pipeline-executor', nodeId, () => getProvider(ctx), createRemoteProxy);
8354
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
8355
- return p.clearModelBoundDeviceOverrides(methodInput);
8356
- }),
8357
8330
  getInferenceDeviceHealth: trpc_middleware_js_1.protectedProcedure
8358
8331
  .input(types_102.pipelineExecutorCapability.methods.getInferenceDeviceHealth.input.loose())
8359
8332
  .output(types_102.pipelineExecutorCapability.methods.getInferenceDeviceHealth.output)
@@ -8379,20 +8352,6 @@ function createCapRouter_pipelineExecutor(getProvider, createRemoteProxy) {
8379
8352
  const p = resolveProvider('pipeline-executor', input?.nodeId, () => getProvider(ctx), createRemoteProxy);
8380
8353
  return p.getSchema();
8381
8354
  }),
8382
- getGlobalSteps: trpc_middleware_js_1.protectedProcedure
8383
- .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
8384
- .output(types_102.pipelineExecutorCapability.methods.getGlobalSteps.output)
8385
- .query(async ({ input, ctx }) => {
8386
- const p = resolveProvider('pipeline-executor', input?.nodeId, () => getProvider(ctx), createRemoteProxy);
8387
- return p.getGlobalSteps();
8388
- }),
8389
- getGlobalPipelineConfig: trpc_middleware_js_1.protectedProcedure
8390
- .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
8391
- .output(types_102.pipelineExecutorCapability.methods.getGlobalPipelineConfig.output)
8392
- .query(async ({ input, ctx }) => {
8393
- const p = resolveProvider('pipeline-executor', input?.nodeId, () => getProvider(ctx), createRemoteProxy);
8394
- return p.getGlobalPipelineConfig();
8395
- }),
8396
8355
  getOrchestratorConfigSchema: trpc_middleware_js_1.protectedProcedure
8397
8356
  .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
8398
8357
  .output(types_102.pipelineExecutorCapability.methods.getOrchestratorConfigSchema.output)
@@ -8845,6 +8804,14 @@ function createCapRouter_pipelineOrchestrator(getProvider, _createRemoteProxy) {
8845
8804
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
8846
8805
  return p.setAgentInferenceDevices(input);
8847
8806
  }),
8807
+ setNodeStepSettings: trpc_middleware_js_1.adminProcedure
8808
+ .input(types_103.pipelineOrchestratorCapability.methods.setNodeStepSettings.input.loose())
8809
+ .output(types_103.pipelineOrchestratorCapability.methods.setNodeStepSettings.output)
8810
+ .mutation(async ({ input, ctx }) => {
8811
+ const p = requireCapProvider('pipeline-orchestrator', () => getProvider(ctx));
8812
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
8813
+ return p.setNodeStepSettings(input);
8814
+ }),
8848
8815
  getNodeInferenceDevices: trpc_middleware_js_1.protectedProcedure
8849
8816
  .input(types_103.pipelineOrchestratorCapability.methods.getNodeInferenceDevices.input.loose())
8850
8817
  .output(types_103.pipelineOrchestratorCapability.methods.getNodeInferenceDevices.output)
@@ -9013,22 +8980,6 @@ function createCapRouter_pipelineOrchestrator(getProvider, _createRemoteProxy) {
9013
8980
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
9014
8981
  return p.setCameraDetectionSource(input);
9015
8982
  }),
9016
- getCameraStepOverrides: trpc_middleware_js_1.protectedProcedure
9017
- .input(types_103.pipelineOrchestratorCapability.methods.getCameraStepOverrides.input.loose())
9018
- .output(types_103.pipelineOrchestratorCapability.methods.getCameraStepOverrides.output)
9019
- .query(async ({ input, ctx }) => {
9020
- const p = requireCapProvider('pipeline-orchestrator', () => getProvider(ctx));
9021
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
9022
- return p.getCameraStepOverrides(input);
9023
- }),
9024
- setCameraStepOverride: trpc_middleware_js_1.adminProcedure
9025
- .input(types_103.pipelineOrchestratorCapability.methods.setCameraStepOverride.input.loose())
9026
- .output(types_103.pipelineOrchestratorCapability.methods.setCameraStepOverride.output)
9027
- .mutation(async ({ input, ctx }) => {
9028
- const p = requireCapProvider('pipeline-orchestrator', () => getProvider(ctx));
9029
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
9030
- return p.setCameraStepOverride(input);
9031
- }),
9032
8983
  setCameraStepSettings: trpc_middleware_js_1.adminProcedure
9033
8984
  .input(types_103.pipelineOrchestratorCapability.methods.setCameraStepSettings.input.loose())
9034
8985
  .output(types_103.pipelineOrchestratorCapability.methods.setCameraStepSettings.output)
@@ -9037,14 +8988,6 @@ function createCapRouter_pipelineOrchestrator(getProvider, _createRemoteProxy) {
9037
8988
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
9038
8989
  return p.setCameraStepSettings(input);
9039
8990
  }),
9040
- setCameraPipelineForAgent: trpc_middleware_js_1.adminProcedure
9041
- .input(types_103.pipelineOrchestratorCapability.methods.setCameraPipelineForAgent.input.loose())
9042
- .output(types_103.pipelineOrchestratorCapability.methods.setCameraPipelineForAgent.output)
9043
- .mutation(async ({ input, ctx }) => {
9044
- const p = requireCapProvider('pipeline-orchestrator', () => getProvider(ctx));
9045
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
9046
- return p.setCameraPipelineForAgent(input);
9047
- }),
9048
8991
  resolvePipeline: trpc_middleware_js_1.protectedProcedure
9049
8992
  .input(types_103.pipelineOrchestratorCapability.methods.resolvePipeline.input.loose())
9050
8993
  .output(types_103.pipelineOrchestratorCapability.methods.resolvePipeline.output)
@@ -12197,6 +12140,13 @@ function createCapRouter_training(getProvider, createRemoteProxy) {
12197
12140
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
12198
12141
  return p.getJobResult(methodInput);
12199
12142
  }),
12143
+ listCanaryTargets: trpc_middleware_js_1.adminProcedure
12144
+ .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
12145
+ .output(types_138.trainingCapability.methods.listCanaryTargets.output)
12146
+ .query(async ({ input, ctx }) => {
12147
+ const p = resolveProvider('training', input?.nodeId, () => getProvider(ctx), createRemoteProxy);
12148
+ return p.listCanaryTargets();
12149
+ }),
12200
12150
  registerResult: trpc_middleware_js_1.adminProcedure
12201
12151
  .input(types_138.trainingCapability.methods.registerResult.input.loose())
12202
12152
  .output(types_138.trainingCapability.methods.registerResult.output)
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.authorizeProcedure = authorizeProcedure;
4
+ exports.viewerPassesAdminTier = viewerPassesAdminTier;
4
5
  exports.authorizeAddonRoute = authorizeAddonRoute;
5
6
  /**
6
7
  * The ONE scope matcher (D892): every authorization decision about a verified
@@ -43,6 +44,22 @@ function authorizeProcedure(scopes, path, rawInput, lookup) {
43
44
  }
44
45
  return { ok: true, mode: 'scoped', scopes: capability };
45
46
  }
47
+ /**
48
+ * May a VIEWER (D921) pass the admin tier of the cap method at `path`? Only
49
+ * when the method's role is a feature the viewer was granted (or explicitly
50
+ * `viewer`). `adminProcedure` asks this AFTER `protectedProcedure` ran the
51
+ * matcher, which already required the role and every named device; a
52
+ * hand-written admin route has no cap entry and is never passed.
53
+ */
54
+ function viewerPassesAdminTier(scopes, path) {
55
+ const viewer = (0, types_1.viewerScopeOf)((0, types_1.capabilityScopesOf)(scopes));
56
+ if (viewer === null)
57
+ return false;
58
+ const meta = system_1.METHOD_ACCESS_MAP[path];
59
+ if (meta === undefined || meta.role === 'admin')
60
+ return false;
61
+ return (0, types_1.viewerHoldsRole)(viewer, meta.role);
62
+ }
46
63
  /** May a principal holding `scopes` reach `/addon/<addonId>/…` with this verb? */
47
64
  function authorizeAddonRoute(scopes, target) {
48
65
  const grant = (0, types_1.shareViewGrantOf)(scopes);
@@ -70,7 +87,7 @@ function authorizeAddonRoute(scopes, target) {
70
87
  }
71
88
  const access = (0, types_1.methodAccessForHttpMethod)(target.method);
72
89
  if ((0, types_1.scopesAllowAddonRoutes)(scopes, access) ||
73
- (0, types_1.scopesAllowAddon)((0, types_1.capabilityScopesOf)(scopes), target.addonId, access)) {
90
+ (0, types_1.scopesAllowAddon)((0, types_1.capabilityScopesOf)(scopes).flatMap((s) => (s.type === 'viewer' ? [] : [s])), target.addonId, access)) {
74
91
  return { ok: true, shareHeaders: null };
75
92
  }
76
93
  return {
@@ -69,6 +69,11 @@ function callerCanViewDevice(scopes, deviceId, lookup) {
69
69
  * security both point the same way here — broad grant, broad stream.
70
70
  */
71
71
  function isBroadEventViewer(scopes) {
72
+ // A viewer (D921) sees the firehose only with every device admitted; any
73
+ // other row it carries never counts.
74
+ const viewer = (0, types_1.viewerScopeOf)(scopes);
75
+ if (viewer !== null)
76
+ return viewer.allow === '*' && viewer.block.length === 0;
72
77
  return scopes.some((s) => s.type === 'category' && s.access.includes('view'));
73
78
  }
74
79
  /**
@@ -107,9 +112,13 @@ function eventDeviceId(evt) {
107
112
  function hasUnrestrictedDeviceView(isAdmin, scopes) {
108
113
  if (isAdmin)
109
114
  return true;
110
- return scopes.some((s) => s.access.includes('view') &&
111
- ((s.type === 'category' && s.target === 'device') ||
112
- (s.type === 'device' && s.selector.kind === 'all')));
115
+ // A viewer (D921) is unrestricted only with every device allowed and none
116
+ // blocked; the rest of its set never counts.
117
+ const viewer = (0, types_1.viewerScopeOf)(scopes);
118
+ if (viewer !== null)
119
+ return viewer.allow === '*' && viewer.block.length === 0;
120
+ return scopes.some((s) => (s.type === 'category' && s.target === 'device' && s.access.includes('view')) ||
121
+ (s.type === 'device' && s.selector.kind === 'all' && s.access.includes('view')));
113
122
  }
114
123
  /**
115
124
  * Cut a `deviceManager.listAll` payload to the devices this principal may VIEW
@@ -151,9 +151,8 @@ function referencedDeviceIds(path, input) {
151
151
  * selector is `all`. Such a caller is unaffected by the device-reference gate.
152
152
  */
153
153
  function hasUnrestrictedDeviceReach(scopes, access) {
154
- return scopes.some((s) => s.access.includes(access) &&
155
- ((s.type === 'category' && s.target === 'device') ||
156
- (s.type === 'device' && s.selector.kind === 'all')));
154
+ return scopes.some((s) => (s.type === 'category' && s.target === 'device' && s.access.includes(access)) ||
155
+ (s.type === 'device' && s.selector.kind === 'all' && s.access.includes(access)));
157
156
  }
158
157
  /**
159
158
  * The device-reference gate for SYSTEM-scope cap methods (F1 #3).
@@ -189,7 +188,43 @@ function deviceRefsSatisfied(scopes, path, input, access, lookup) {
189
188
  }
190
189
  return { ok: true };
191
190
  }
191
+ /**
192
+ * The viewer role's matcher (D921). The method's ROLE decides what a viewer
193
+ * may call (`viewer` baseline, `feature:<id>` when granted, never `admin`),
194
+ * and every device the request names must be admitted by the viewer's
195
+ * allow/block lists — through itself or an ancestor, with unknown parentage
196
+ * failing closed (`viewerAdmitsDevice`). A method that names no device is
197
+ * cross-device by nature (an enumeration, a gallery): its answer is cut where
198
+ * one exists (`DEVICE_ENUMERATION_METHODS`, the event firehose filter).
199
+ */
200
+ function checkViewerAccess(viewer, path, input, lookup) {
201
+ const meta = system_1.METHOD_ACCESS_MAP[path];
202
+ if (!meta)
203
+ return { ok: false, reason: `Unknown method '${path}' — codegen drift` };
204
+ if (!(0, types_1.viewerHoldsRole)(viewer, meta.role)) {
205
+ const feature = (0, types_1.featureOfRole)(meta.role);
206
+ return {
207
+ ok: false,
208
+ reason: feature !== null
209
+ ? `'${path}' needs the '${feature}' feature, which this viewer does not have`
210
+ : `'${path}' is for administrators — a viewer cannot call it`,
211
+ };
212
+ }
213
+ const ancestors = lookup === undefined
214
+ ? undefined
215
+ : (id) => lookup.getType(id) === null ? null : lookup.getAncestors(id);
216
+ for (const deviceId of referencedDeviceIds(path, input)) {
217
+ if (!(0, types_1.viewerAdmitsDevice)(viewer, deviceId, ancestors)) {
218
+ return { ok: false, reason: `Device ${deviceId} is outside this viewer's devices` };
219
+ }
220
+ }
221
+ return { ok: true, access: meta.access };
222
+ }
192
223
  function checkScopeAccess(scopes, path, input, lookup) {
224
+ // A viewer is decided by its own rules, alone (D921).
225
+ const viewer = (0, types_1.viewerScopeOf)(scopes);
226
+ if (viewer !== null)
227
+ return checkViewerAccess(viewer, path, input, lookup);
193
228
  const meta = system_1.METHOD_ACCESS_MAP[path];
194
229
  if (!meta) {
195
230
  return { ok: false, reason: `Unknown method '${path}' — codegen drift` };
@@ -229,6 +264,9 @@ function checkScopeAccess(scopes, path, input, lookup) {
229
264
  : exports.DEVICE_ENUMERATION_METHODS.has(path);
230
265
  break;
231
266
  }
267
+ case 'viewer':
268
+ // Unreachable: a viewer row is decided above, alone.
269
+ continue;
232
270
  }
233
271
  if (!targetMatches)
234
272
  continue;
@@ -252,6 +290,8 @@ function checkScopeAccess(scopes, path, input, lookup) {
252
290
  ok: false,
253
291
  reason: `No scope grants ${meta.access} on '${meta.capName}' (${meta.capScope}-scope cap${deviceId !== null ? `, device=${deviceId}` : ''}). Have: ${scopes
254
292
  .map((s) => {
293
+ if (s.type === 'viewer')
294
+ return 'viewer';
255
295
  const target = s.type === 'device' ? `[${describeSelector(s)}]` : s.target;
256
296
  return `${s.type}:${target}[${s.access.join(',')}]`;
257
297
  })
@@ -176,8 +176,11 @@ exports.protectedProcedure = localized.use(async ({ ctx, next, path, getRawInput
176
176
  * `protectedProcedure`. Useful on hand-written routes whose admin-only
177
177
  * nature should be obvious to a code reader.
178
178
  */
179
- exports.adminProcedure = exports.protectedProcedure.use(({ ctx, next }) => {
180
- if (!ctx.user.isAdmin) {
179
+ exports.adminProcedure = exports.protectedProcedure.use(({ ctx, next, path }) => {
180
+ // A viewer passes the admin tier only on a method of a feature it was
181
+ // granted (D921); the matcher in `protectedProcedure` already checked the
182
+ // feature and every device the call names.
183
+ if (!ctx.user.isAdmin && !(0, principal_authorization_js_1.viewerPassesAdminTier)(ctx.user.scopes ?? [], path)) {
181
184
  throw (0, user_error_js_1.trpcUserError)('FORBIDDEN', 'auth.admin-required', undefined, 'Admin required');
182
185
  }
183
186
  return next({ ctx });
@@ -37,7 +37,8 @@ function addonRouteCaller(principal) {
37
37
  scopedToken: {
38
38
  id: principal.credentialId,
39
39
  userId: principal.subjectId,
40
- scopes: [...(0, types_1.capabilityScopesOf)(scoped)],
40
+ // Capability rows only: an addon reads grants, never a viewer row (D921).
41
+ scopes: (0, types_1.capabilityScopesOf)(scoped).flatMap((s) => (s.type === 'viewer' ? [] : [s])),
41
42
  },
42
43
  }),
43
44
  };
@@ -19,6 +19,10 @@ function accessCovered(asked, held) {
19
19
  }
20
20
  /** The grant without its access list, as a comparable key. */
21
21
  function grantKey(scope) {
22
+ // A viewer row (D921) is compared WHOLE: its lists and features are the
23
+ // grant, there is no access flavour to cut.
24
+ if (scope.type === 'viewer')
25
+ return JSON.stringify(scope);
22
26
  if (scope.type === 'device') {
23
27
  return JSON.stringify({
24
28
  type: scope.type,
@@ -37,6 +41,8 @@ function describe(scope) {
37
41
  return `addon-routes[${scope.access.join(',')}]`;
38
42
  if (scope.type === 'preferences-self')
39
43
  return 'preferences-self';
44
+ if (scope.type === 'viewer')
45
+ return 'viewer';
40
46
  if (scope.type === 'device')
41
47
  return `device[${scope.access.join(',')}]`;
42
48
  return `${scope.type}:${scope.target}[${scope.access.join(',')}]`;
@@ -55,7 +61,10 @@ function scopesWithin(requested, held, lookup) {
55
61
  return { ok: false, reason: 'a share-view grant names a device you cannot view' };
56
62
  }
57
63
  if (scope.grant.talk === true && !canTalkOn(heldCapability, scope.grant.deviceIds, lookup)) {
58
- return { ok: false, reason: 'a share-view grant asks talk-back on a device you cannot talk on' };
64
+ return {
65
+ ok: false,
66
+ reason: 'a share-view grant asks talk-back on a device you cannot talk on',
67
+ };
59
68
  }
60
69
  continue;
61
70
  }
@@ -72,7 +81,18 @@ function scopesWithin(requested, held, lookup) {
72
81
  };
73
82
  }
74
83
  const key = grantKey(scope);
75
- const covered = heldCapability.some((h) => grantKey(h) === key && accessCovered(scope.access, h.access));
84
+ if (scope.type === 'viewer') {
85
+ // Only the SAME viewer grant the caller holds — a viewer mints a token
86
+ // for itself, never a wider (or differently shaped) one.
87
+ if (!heldCapability.some((h) => grantKey(h) === key)) {
88
+ return {
89
+ ok: false,
90
+ reason: `you do not hold ${describe(scope)} with these devices and features`,
91
+ };
92
+ }
93
+ continue;
94
+ }
95
+ const covered = heldCapability.some((h) => h.type !== 'viewer' && grantKey(h) === key && accessCovered(scope.access, h.access));
76
96
  if (!covered)
77
97
  return { ok: false, reason: `you do not hold ${describe(scope)}` };
78
98
  }
@@ -124,7 +144,15 @@ function intersectScopes(token, held, lookup) {
124
144
  continue;
125
145
  }
126
146
  const key = grantKey(scope);
127
- const heldAccess = new Set(held.filter((h) => grantKey(h) === key).flatMap((h) => [...h.access]));
147
+ if (scope.type === 'viewer') {
148
+ // Kept only while the subject still holds the identical viewer grant:
149
+ // an admin who edits a viewer's devices or features ends that viewer's
150
+ // tokens rather than letting them keep (or gain) another reach.
151
+ if (held.some((h) => grantKey(h) === key))
152
+ out.push(scope);
153
+ continue;
154
+ }
155
+ const heldAccess = new Set(held.flatMap((h) => (h.type !== 'viewer' && grantKey(h) === key ? [...h.access] : [])));
128
156
  const access = scope.access.filter((a) => heldAccess.has(a));
129
157
  if (access.length > 0)
130
158
  out.push({ ...scope, access });
@@ -3103,7 +3103,7 @@ class AddonRegistryService {
3103
3103
  devices: deviceManagerApi,
3104
3104
  cluster: { broker: (0, system_1.adaptBrokerToCluster)(this.moleculer.broker) },
3105
3105
  streamProbe: kernelStreamProbe,
3106
- hwaccel: (0, system_1.createKernelHwAccel)(),
3106
+ hwaccel: (0, system_1.createKernelHwAccel)({ logger }),
3107
3107
  capabilityRegistry: this.capabilityRegistry,
3108
3108
  // Per-addon storage-location declarations across every installed
3109
3109
  // addon — surfaced from the kernel's AddonLoader so the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.619",
3
+ "version": "1.2.621",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "files": [
@@ -38,19 +38,20 @@
38
38
  ]
39
39
  },
40
40
  "dependencies": {
41
- "@camstack/addon-admin-ui": "1.2.456",
42
- "@camstack/addon-agent-ui": "1.2.261",
41
+ "@camstack/addon-admin-ui": "1.2.458",
42
+ "@camstack/addon-agent-ui": "1.2.262",
43
43
  "@camstack/addon-auth": "1.2.166",
44
+ "@camstack/addon-benchmark": "1.2.262",
44
45
  "@camstack/addon-decoder-nodeav": "1.2.141",
45
46
  "@camstack/addon-notifiers": "1.2.619",
46
- "@camstack/addon-pipeline": "1.2.620",
47
- "@camstack/addon-pipeline-orchestrator": "1.2.622",
48
- "@camstack/addon-post-analysis": "1.2.626",
47
+ "@camstack/addon-pipeline": "1.2.622",
48
+ "@camstack/addon-pipeline-orchestrator": "1.2.623",
49
+ "@camstack/addon-post-analysis": "1.2.628",
49
50
  "@camstack/develop": "0.1.31",
50
51
  "@camstack/sdk": "1.2.151",
51
- "@camstack/system": "1.2.470",
52
- "@camstack/types": "1.2.402",
53
- "@camstack/ui-library": "1.2.318",
52
+ "@camstack/system": "1.2.472",
53
+ "@camstack/types": "1.2.404",
54
+ "@camstack/ui-library": "1.2.319",
54
55
  "@fastify/compress": "^9.0.0",
55
56
  "@fastify/cookie": "^11.0.2",
56
57
  "@fastify/cors": "^11.2.0",