openvibers/openvibe.community Commits

Staff powers from the contracts staff map (D05, ADR-022): a browser viewer is paste staff with staff.moderation.pastes and discussion staff with staff.moderation.discussions, from the role or the staff_caps Network issues (issued claims win); discussionStaff reads the discussion capability. Same answers for every role today. test/staff-map.test.js

OP OpenVibers committed 24 Sept 2026 · 0a540eade9 · 3 files, +37 −3


Changes

ddiff --git a/server/identity/capabilities.js b/server/identity/capabilities.js
iindex b64d76e..12271d4 100644
---- a/server/identity/capabilities.js
++++ b/server/identity/capabilities.js
@@@ -35,12 +35,12 @@ function serviceHas(viewer, capabilityId) {
}
/**
-- * Staff for discussions (comments, forum, relay admin): an admin/global_mod browser, or a
++ * Staff for discussions (comments, forum, relay admin): a browser holding staff.moderation.discussions, or a
* service that vouches for a staff person with X-OV-Staff: 1 and holds community.comment.moderate.
*/
function discussionStaff(viewer) {
if (!viewer) return false;
-- if (viewer.kind === 'user') return !!viewer.staff;
++ if (viewer.kind === 'user') return !!(viewer.discussionStaff !== undefined ? viewer.discussionStaff : viewer.staff);
return viewer.kind === 'service' && !!viewer.vouchesStaff && serviceHas(viewer, 'community.comment.moderate');
}
ddiff --git a/server/identity/viewer.js b/server/identity/viewer.js
iindex ac5a149..7c4a6bc 100644
---- a/server/identity/viewer.js
++++ b/server/identity/viewer.js
@@@ -26,6 +26,8 @@ const { extractToken, claimsToUser, decodeJwtPayload } = require('../auth/routes
const { checkCapability } = require('./capabilities');
const { ids, capabilities, serviceAuth, http } = contracts;
++const { staff: staffMap } = require('openvibe-contracts');
++// The roles that hold staff moderation today (kept for callers; gates ask staffMap.can instead).
const STAFF_ROLES = new Set(['admin', 'global_mod']);
const PRINCIPAL_SUB = /^(svc|app|mod):/;
const AUDIENCE = 'openvibe.community';
@@@ -111,7 +113,10 @@ function createViewerResolver({ auth, config, network }) {
if (subject && network) { try { network.rememberClaims(subject, claims); } catch { /* display cache only */ } }
const user = claimsToUser(claims);
if (subject && !user.subject_id) user.subject_id = subject;
-- return { kind: 'user', subject, staff: STAFF_ROLES.has(claims.role), origin: 'user', user, token };
++ // Staff powers come from the contracts staff map (ADR-022): the role, or Network's issued staff_caps.
++ const staff = staffMap.can(claims, 'staff.moderation.pastes');
++ const discussionStaff = staffMap.can(claims, 'staff.moderation.discussions');
++ return { kind: 'user', subject, staff, discussionStaff, origin: 'user', user, token };
}
/**
ddiff --git a/test/staff-map.test.js b/test/staff-map.test.js
nnew file mode 100644
iindex 0000000..6c70fdb
---- /dev/null
++++ b/test/staff-map.test.js
@@@ -0,0 +1,29 @@
++'use strict';
++/** Community's staff powers come from the contracts staff map: the role, or the staff_caps Network issues. */
++const assert = require('assert');
++const { ids } = require('openvibe-contracts');
++const { boot, check, done } = require('./helpers/app');
++
++(async () => {
++ const t = await boot({ authority: 'community' });
++ const net = t.network;
++ const { createViewerResolver } = require('../server/identity/viewer');
++ const viewers = createViewerResolver({ auth: t.app.locals.auth, config: require('../server/config'), network: null });
++ const resolve = (token) => viewers.resolve({ headers: { authorization: `Bearer ${token}` }, get: () => undefined, cookies: {} });
++ const tok = (claims) => net.sign({ id: Math.floor(Math.random() * 1e6), subject_id: ids.newId('user'), username: 'x', display_name: 'X', ...claims });
++
++ await check('a global moderator is paste and discussion staff', async () => {
++ const v = await resolve(tok({ role: 'global_mod' }));
++ assert.deepStrictEqual([v.staff, v.discussionStaff], [true, true]);
++ });
++ await check('an ordinary user is neither', async () => {
++ const v = await resolve(tok({ role: 'user' }));
++ assert.deepStrictEqual([v.staff, v.discussionStaff], [false, false]);
++ });
++ await check('issued staff_caps win over the role: discussions only', async () => {
++ const v = await resolve(tok({ role: 'user', staff_caps: ['staff.moderation.discussions'] }));
++ assert.deepStrictEqual([v.staff, v.discussionStaff], [false, true]);
++ assert.strictEqual(require('../server/identity/capabilities').discussionStaff(v), true);
++ });
++ done();
++})();