@camstack/server 1.2.618 → 1.2.620
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 +5 -1
- package/dist/api/trpc/generated-cap-routers.js +16 -1
- 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/package.json +14 -14
|
@@ -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)
|
|
@@ -6233,7 +6233,7 @@ function createCapRouter_motionTrigger(getProvider, createRemoteProxy) {
|
|
|
6233
6233
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
6234
6234
|
return p.getStatus(methodInput);
|
|
6235
6235
|
}),
|
|
6236
|
-
setMotionTrigger: trpc_middleware_js_1.
|
|
6236
|
+
setMotionTrigger: trpc_middleware_js_1.protectedProcedure
|
|
6237
6237
|
.input(types_84.motionTriggerCapability.methods.setMotionTrigger.input.loose())
|
|
6238
6238
|
.output(types_84.motionTriggerCapability.methods.setMotionTrigger.output)
|
|
6239
6239
|
.mutation(async ({ input, ctx }) => {
|
|
@@ -8853,6 +8853,14 @@ function createCapRouter_pipelineOrchestrator(getProvider, _createRemoteProxy) {
|
|
|
8853
8853
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
8854
8854
|
return p.getNodeInferenceDevices(input);
|
|
8855
8855
|
}),
|
|
8856
|
+
forgetInferenceDevice: trpc_middleware_js_1.adminProcedure
|
|
8857
|
+
.input(types_103.pipelineOrchestratorCapability.methods.forgetInferenceDevice.input.loose())
|
|
8858
|
+
.output(types_103.pipelineOrchestratorCapability.methods.forgetInferenceDevice.output)
|
|
8859
|
+
.mutation(async ({ input, ctx }) => {
|
|
8860
|
+
const p = requireCapProvider('pipeline-orchestrator', () => getProvider(ctx));
|
|
8861
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
8862
|
+
return p.forgetInferenceDevice(input);
|
|
8863
|
+
}),
|
|
8856
8864
|
resetNodePipelineDefaults: trpc_middleware_js_1.adminProcedure
|
|
8857
8865
|
.input(types_103.pipelineOrchestratorCapability.methods.resetNodePipelineDefaults.input.loose())
|
|
8858
8866
|
.output(types_103.pipelineOrchestratorCapability.methods.resetNodePipelineDefaults.output)
|
|
@@ -9446,6 +9454,13 @@ function createCapRouter_platformProbe(getProvider, createRemoteProxy) {
|
|
|
9446
9454
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
9447
9455
|
return p.resolveHwAccel(methodInput);
|
|
9448
9456
|
}),
|
|
9457
|
+
rescanAccelerators: trpc_middleware_js_1.adminProcedure
|
|
9458
|
+
.input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
|
|
9459
|
+
.output(types_106.platformProbeCapability.methods.rescanAccelerators.output)
|
|
9460
|
+
.mutation(async ({ input, ctx }) => {
|
|
9461
|
+
const p = resolveProvider('platform-probe', input?.nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
9462
|
+
return p.rescanAccelerators();
|
|
9463
|
+
}),
|
|
9449
9464
|
});
|
|
9450
9465
|
}
|
|
9451
9466
|
function createCapRouter_powerMeter(getProvider, createRemoteProxy) {
|
|
@@ -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 });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.620",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"files": [
|
|
@@ -38,19 +38,19 @@
|
|
|
38
38
|
]
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@camstack/addon-admin-ui": "1.2.
|
|
42
|
-
"@camstack/addon-agent-ui": "1.2.
|
|
43
|
-
"@camstack/addon-auth": "1.2.
|
|
44
|
-
"@camstack/addon-decoder-nodeav": "1.2.
|
|
45
|
-
"@camstack/addon-notifiers": "1.2.
|
|
46
|
-
"@camstack/addon-pipeline": "1.2.
|
|
47
|
-
"@camstack/addon-pipeline-orchestrator": "1.2.
|
|
48
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
49
|
-
"@camstack/develop": "0.1.
|
|
50
|
-
"@camstack/sdk": "1.2.
|
|
51
|
-
"@camstack/system": "1.2.
|
|
52
|
-
"@camstack/types": "1.2.
|
|
53
|
-
"@camstack/ui-library": "1.2.
|
|
41
|
+
"@camstack/addon-admin-ui": "1.2.457",
|
|
42
|
+
"@camstack/addon-agent-ui": "1.2.261",
|
|
43
|
+
"@camstack/addon-auth": "1.2.166",
|
|
44
|
+
"@camstack/addon-decoder-nodeav": "1.2.141",
|
|
45
|
+
"@camstack/addon-notifiers": "1.2.619",
|
|
46
|
+
"@camstack/addon-pipeline": "1.2.621",
|
|
47
|
+
"@camstack/addon-pipeline-orchestrator": "1.2.622",
|
|
48
|
+
"@camstack/addon-post-analysis": "1.2.627",
|
|
49
|
+
"@camstack/develop": "0.1.31",
|
|
50
|
+
"@camstack/sdk": "1.2.151",
|
|
51
|
+
"@camstack/system": "1.2.471",
|
|
52
|
+
"@camstack/types": "1.2.403",
|
|
53
|
+
"@camstack/ui-library": "1.2.318",
|
|
54
54
|
"@fastify/compress": "^9.0.0",
|
|
55
55
|
"@fastify/cookie": "^11.0.2",
|
|
56
56
|
"@fastify/cors": "^11.2.0",
|