openvibers/openvibe.trade Commits

Staff powers from the contracts staff map (WS-D, ADR-022; Contracts 0.38.0): editors are staff.editorial.manage (admin, as before) or TRADE_EDITORS (the product's own role), not a role comparison. test/staff-map.test.js fails on raw role checks

OP OpenVibers committed 24 Sept 2026 · d8250750c7 · 6 files, +36 −10


Changes

ddiff --git a/.env.example b/.env.example
iindex 2ec5942..e3c06bc 100644
---- a/.env.example
++++ b/.env.example
@@@ -31,7 +31,7 @@ OV_OAUTH_CLIENT_SECRET=
#TRADE_FORM_SECRET=
# Editors (create instruments, write context, review AI drafts): Network subjects, comma-separated.
--# Network admins are editors too.
++# Staff with staff.editorial.manage (Network admins) are editors too; this list is the product's own role.
TRADE_EDITORS=
# OpenVibe.Sources — the trade category (filings, market data feeds), read by cursor with the
ddiff --git a/package-lock.json b/package-lock.json
iindex 38069a3..ef339ed 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -15,7 +15,7 @@
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.38.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v0.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.12.0"
@@@ -1015,9 +1015,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.33.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
-- "integrity": "sha512-7q8NUhcPxgb8lxrVdC3pFbiPdjzliJM1mMP/1G5a2PmOr2tJVAXwYa8+a8YgCJvYeMSeMHWijLkIEiZ9IhllOg==",
++ "version": "0.38.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.38.0",
++ "integrity": "sha512-nHYCcKM9N4TJS+BlHOax9xwt4jU87qhXsLZ2gnuK4nOHpGlkLk2OI49PCQVvfHPe+V7bzBK7L4MKjFcTNv8ORA==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 79fb568..a3bcd88 100644
---- a/package.json
++++ b/package.json
@@@ -20,7 +20,7 @@
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.38.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v0.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.12.0"
ddiff --git a/server/auth/viewer.js b/server/auth/viewer.js
iindex b823002..29e50dc 100644
---- a/server/auth/viewer.js
++++ b/server/auth/viewer.js
@@@ -6,7 +6,7 @@
* { kind: 'anonymous', subject: null, editor: false }
* { kind: 'user', subject: 'usr_…'|null, editor, user, token }
* A browser with the Network user JWT (ov_token cookie or Bearer). The subject comes from the
-- * token's subject_id claim. Editors: role admin, or a subject in TRADE_EDITORS.
++ * token's subject_id claim. Editors: staff with staff.editorial.manage (the contracts staff map), or a subject in TRADE_EDITORS.
* X-OV-* headers are ignored for browsers.
* { kind: 'service', service: 'svc:ai', claims, subject: 'usr_…'|null, origin: 'user'|'ai' }
* A first-party service with a Network client-credentials token for audience openvibe.trade.
@@@ -23,7 +23,7 @@ const contracts = require('openvibe-contracts');
const { extractToken, claimsToUser, decodeJwtPayload } = require('./sso');
const { checkCapability } = require('./capabilities');
--const { ids, serviceAuth, http } = contracts;
++const { ids, serviceAuth, http, staff: staffMap } = contracts;
const PRINCIPAL_SUB = /^(svc|app|mod):/;
const AUDIENCE = 'openvibe.trade';
@@@ -69,7 +69,7 @@ function createViewerResolver({ auth, config }) {
const claims = await auth.verify(token);
if (!claims || (typeof claims.sub === 'string' && PRINCIPAL_SUB.test(claims.sub))) return null;
const subject = ids.isSubjectId('user', claims.subject_id) ? claims.subject_id : null;
-- const editor = claims.role === 'admin' || Boolean(subject && editors.has(subject));
++ const editor = staffMap.can(claims, 'staff.editorial.manage') || Boolean(subject && editors.has(subject));
return { kind: 'user', subject, editor, origin: 'user', user: claimsToUser(claims), token };
}
ddiff --git a/server/config.js b/server/config.js
iindex 8499e0e..e8c8255 100644
---- a/server/config.js
++++ b/server/config.js
@@@ -45,7 +45,7 @@ function load(env = process.env) {
formSecret: env.TRADE_FORM_SECRET || '',
// Editors: Network subjects (usr_…) who may create instruments, write context and review
-- // AI drafts. Network admins are editors too.
++ // AI drafts (a product role). Staff with staff.editorial.manage are editors too.
editors: list(env.TRADE_EDITORS),
// OpenVibe.Sources: the trade category (filings, market data feeds). Read with a service
ddiff --git a/test/staff-map.test.js b/test/staff-map.test.js
nnew file mode 100644
iindex 0000000..a1a4174
---- /dev/null
++++ b/test/staff-map.test.js
@@@ -0,0 +1,26 @@
++'use strict';
++// Staff powers come from the openvibe-contracts staff map (ADR-022): staff.editorial.manage makes a person an editor (TRADE_EDITORS names more).
++// No server file compares a person's role by hand.
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const { staff } = require('openvibe-contracts');
++
++const CAPS = ["staff.editorial.manage"];
++for (const [claims, want] of [[{"role": "user"}, [false]], [{"role": "global_mod"}, [false]], [{"role": "admin"}, [true]], [{"role": "admin", "is_owner": true}, [true]]]) {
++ CAPS.forEach((cap, i) => assert.strictEqual(staff.can(claims, cap), want[i], `${JSON.stringify(claims)} ${cap}`));
++}
++
++const offenders = [];
++(function walk(dir) {
++ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
++ const f = path.join(dir, e.name);
++ if (e.isDirectory()) { if (e.name !== 'node_modules') walk(f); continue; }
++ if (!/\.(js|ts)$/.test(e.name)) continue;
++ fs.readFileSync(f, 'utf8').split('\n').forEach((line, i) => {
++ if (/STAFF_ROLES|claims\.role\s*[!=]==?|\.(has|includes)\(\s*(claims|u|user|p)\.role\b|\brole\s*[!=]==?\s*['"](admin|global_mod|moderator)['"]/.test(line)) offenders.push(`${path.relative(path.join(__dirname, '..'), f)}:${i + 1}`);
++ });
++ }
++})(path.join(__dirname, '..', "server"));
++assert.deepStrictEqual(offenders, [], 'raw role checks; ask staff.can(claims, \'staff.…\')');
++console.log('staff map: all checks passed');