@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.
- package/dist/api/core/auth.router.js +8 -2
- package/dist/api/trpc/generated-cap-routers.js +15 -65
- package/dist/api/trpc/principal-authorization.js +18 -1
- package/dist/api/trpc/principal-visibility.js +12 -3
- package/dist/api/trpc/scope-access.js +43 -3
- package/dist/api/trpc/trpc.middleware.js +5 -2
- package/dist/auth/addon-route-auth.js +2 -1
- package/dist/auth/scope-escalation.js +31 -3
- package/dist/core/addon/addon-registry.service.js +1 -1
- package/package.json +10 -9
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
42
|
-
"@camstack/addon-agent-ui": "1.2.
|
|
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.
|
|
47
|
-
"@camstack/addon-pipeline-orchestrator": "1.2.
|
|
48
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
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.
|
|
52
|
-
"@camstack/types": "1.2.
|
|
53
|
-
"@camstack/ui-library": "1.2.
|
|
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",
|