openvibers/openvibe.network Commits

Retire the key-only role push to Live (roadmap WS-B task 2 step 5; register C-55, Live's route C-54): an admin role change no longer POSTs Live's /internal/user-role with the internal key

OP OpenVibers committed 26 Sept 2026 · 60455b3490 · 5 files, +94 −25

The users trigger already records every role change in the admin request's own transaction and server/identity/profile-events.js relays it as network.user.updated with changed ['role'] (Contracts 0.43.0), which Live applies at POST /internal/network-events, downgrades included. pushRoleToStreamer and its two call sites (PUT /api/admin/users/:id/role, POST /api/admin/users/grant-admin) are removed; the comment in server/db/database.js now names the event.

test/admin-role-events.test.js (in npm test) drives both admin routes over HTTP: promote, demote and grant-admin each produce one valid network.user.updated for that subject with changed ['role'] and the new role, and no outbound call goes to /internal/user-role. docs/retirement.md gains a "Removed key calls" table recording it

Changes

ddiff --git a/docs/retirement.md b/docs/retirement.md
iindex 79eb758d..2a2fad1c 100644
---- a/docs/retirement.md
++++ b/docs/retirement.md
@@@ -49,3 +49,9 @@ Live, already holds `identity.subject.resolve` and `network.coins.credit` on `op
adding the five paths to `TOKEN_PATHS` in its `server/net/network-principal.js` and sending those calls through
`headersFor(path)`. The capability names are the closest existing ones in openvibe-contracts 0.33; narrower ones
(`network.registry.read`, `network.coins.read`, an identity legacy-map write) are a contracts change.
++
++## Removed key calls
++
++| Call | Removed | Instead |
++| --- | --- | --- |
++| Network → Live `POST /internal/user-role` (register C-55; Live's route, C-54, went in the same change) | 2026-09-26, WS-B task 2 step 5 | `network.user.updated`: the `users` trigger records a role change in the admin request's own transaction and `server/identity/profile-events.js` relays it; Live applies it, downgrades too (`test/admin-role-events.test.js`) |
ddiff --git a/package.json b/package.json
iindex 5096d0ef..f9973844 100644
---- a/package.json
++++ b/package.json
@@@ -6,7 +6,7 @@
"scripts": {
"start": "node server/index.js",
"dev": "NODE_ENV=development node server/index.js",
-- "test": "node test/registry-bootstrap.test.js && node test/nginx-generator-live.test.js && node test/admin-refresh.test.js && node test/history-sso.test.js && node test/sso-check.test.js && node test/fedcm.test.js && node test/domains.test.js && node test/frame.test.js && node test/github-integration.test.js && node test/revocation.test.js && node test/usernames.test.js && node test/avatar.test.js && node test/roadmap-baseline.test.js && node test/identity-subjects.test.js && node test/principals.test.js && node test/modules.test.js && node test/module-events.test.js && node test/registry-ecosystem.test.js && node test/registry-exposure.test.js && node test/registry-manifest-fixture.test.js && node test/staff-claims.test.js && node test/profile-events.test.js && node test/grants-admin.test.js && node test/events-ops.test.js && node test/deploy-drift.test.js && node test/registry-topics.test.js && node test/service-principal.test.js && node test/oauth-pkce.test.js && node test/developer-projects.test.js && node test/developer-defaults.test.js && node test/public-cors.test.js && node test/observability.test.js && node test/status.test.js && node test/events-consumer.test.js && node test/security-session.test.js && node test/security-redirects.test.js && node test/analytics-privacy.test.js && node test/data-ownership.test.js && node test/unknown-paths.test.js && node test/oidc-discovery.test.js && node test/refresh-tokens.test.js && node test/provider-secrets.test.js && node test/first-party-origins.test.js && node test/updates.test.js && node test/static-headers.test.js && node test/library-tags.test.js && node test/moderation-audit.test.js && node test/blocks.test.js && node test/site-config.test.js && node test/perf-budget.test.js && node test/shutdown.test.js && node test/loyalty-policy.test.js && node test/themes-review.test.js && node test/page-checks.test.js && node test/operator-checklist.test.js && node test/release-health.test.js",
++ "test": "node test/registry-bootstrap.test.js && node test/nginx-generator-live.test.js && node test/admin-refresh.test.js && node test/history-sso.test.js && node test/sso-check.test.js && node test/fedcm.test.js && node test/domains.test.js && node test/frame.test.js && node test/github-integration.test.js && node test/revocation.test.js && node test/usernames.test.js && node test/avatar.test.js && node test/roadmap-baseline.test.js && node test/identity-subjects.test.js && node test/principals.test.js && node test/modules.test.js && node test/module-events.test.js && node test/registry-ecosystem.test.js && node test/registry-exposure.test.js && node test/registry-manifest-fixture.test.js && node test/staff-claims.test.js && node test/profile-events.test.js && node test/admin-role-events.test.js && node test/grants-admin.test.js && node test/events-ops.test.js && node test/deploy-drift.test.js && node test/registry-topics.test.js && node test/service-principal.test.js && node test/oauth-pkce.test.js && node test/developer-projects.test.js && node test/developer-defaults.test.js && node test/public-cors.test.js && node test/observability.test.js && node test/status.test.js && node test/events-consumer.test.js && node test/security-session.test.js && node test/security-redirects.test.js && node test/analytics-privacy.test.js && node test/data-ownership.test.js && node test/unknown-paths.test.js && node test/oidc-discovery.test.js && node test/refresh-tokens.test.js && node test/provider-secrets.test.js && node test/first-party-origins.test.js && node test/updates.test.js && node test/static-headers.test.js && node test/library-tags.test.js && node test/moderation-audit.test.js && node test/blocks.test.js && node test/site-config.test.js && node test/perf-budget.test.js && node test/shutdown.test.js && node test/loyalty-policy.test.js && node test/themes-review.test.js && node test/page-checks.test.js && node test/operator-checklist.test.js && node test/release-health.test.js",
"reset-db": "node server/reset-db.js",
"grant-admin": "node server/grant-admin.js"
},
ddiff --git a/server/admin/routes.js b/server/admin/routes.js
iindex 57da5aaf..e2f84dbb 100644
---- a/server/admin/routes.js
++++ b/server/admin/routes.js
@@@ -215,24 +215,9 @@ function createAdminRoutes(db, notificationService, emailService, requireAuth) {
return refreshServiceByKey(req, key);
}
-- // Push an authoritative role change to OpenVibe.Live so it takes effect
-- // immediately (chat Staff badge, moderation powers) instead of waiting on the
-- // user's next SSO token. Fire-and-forget — never blocks the admin response.
-- function pushRoleToStreamer(user, role) {
-- try {
-- const base = (process.env.OV_LIVE_INTERNAL_URL || 'http://127.0.0.1:3000').replace(/\/$/, '');
-- const key = process.env.INTERNAL_API_KEY || process.env.OV_INTERNAL_KEY;
-- if (!key || !user) return;
-- fetch(`${base}/internal/user-role`, {
-- method: 'POST',
-- headers: { 'Content-Type': 'application/json', 'X-Internal-Key': key },
-- body: JSON.stringify({ openvibenetwork_id: user.id, username: user.username, role }),
-- }).then(r => { if (!r.ok) console.warn('[AdminRole] streamer role push HTTP', r.status); })
-- .catch(e => console.warn('[AdminRole] streamer role push failed:', e.message));
-- } catch (e) {
-- console.warn('[AdminRole] streamer role push error:', e.message);
-- }
-- }
++ // A role change reaches Live (and every other consumer) as network.user.updated: the users trigger records
++ // it in this request's transaction and server/identity/profile-events.js relays it (WS-B task 2). The
++ // key-only push to Live's /internal/user-role that used to follow it is retired (register C-54/C-55).
router.use(requireAuth, requireAdmin);
@@@ -582,7 +567,6 @@ function createAdminRoutes(db, notificationService, emailService, requireAuth) {
'grant_admin',
JSON.stringify({ targetId: user.id, targetUsername: user.username, targetEmail: user.email }),
);
-- pushRoleToStreamer(user, 'admin');
res.json({ ok: true, user: { id: user.id, username: user.username, email: user.email, role: 'admin' } });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
@@@ -740,8 +724,6 @@ function createAdminRoutes(db, notificationService, emailService, requireAuth) {
db.prepare('INSERT INTO audit_log (user_id, action, details) VALUES (?, ?, ?)').run(
req.user.id, 'user_role_change', JSON.stringify({ targetId: req.params.id, role })
);
-- const changed = db.prepare('SELECT id, username FROM users WHERE id = ?').get(req.params.id);
-- if (changed) pushRoleToStreamer(changed, role);
res.json({ ok: true });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
ddiff --git a/server/db/database.js b/server/db/database.js
iindex e213d367..615c6a20 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -676,9 +676,10 @@ function initDb(dbPath) {
}
// Roles are Network's own data. Network never opens another service's database (ADR-007):
-- // role changes made here reach Live through POST /internal/user-role (admin/routes.js), and
-- // Live never downgrades from a stale SSO token. The boot-time read of Live's database that
-- // used to live here was removed (compatibility register C-58).
++ // role changes made here reach Live as network.user.updated (server/identity/profile-events.js;
++ // the key-only POST /internal/user-role push was retired, register C-54/C-55), and Live never
++ // downgrades from a stale SSO token. The boot-time read of Live's database that used to live
++ // here was removed (compatibility register C-58).
// ── Helper: getSetting ───────────────────────────────────
// Provider secrets (server/secrets.js) come from their environment variable when it is set.
ddiff --git a/test/admin-role-events.test.js b/test/admin-role-events.test.js
nnew file mode 100644
iindex 00000000..95d5920a
---- /dev/null
++++ b/test/admin-role-events.test.js
@@@ -0,0 +1,80 @@
++'use strict';
++// WS-B task 2 step 5: an admin role change reaches Live (and every consumer) as network.user.updated with
++// `changed` naming role (the users trigger, server/identity/profile-events.js), not as a key-only push to
++// Live's retired POST /internal/user-role. Both admin paths are covered: PUT /users/:id/role (up and down)
++// and the owner's POST /users/grant-admin.
++const assert = require('assert');
++const fs = require('fs');
++const http = require('http');
++const os = require('os');
++const path = require('path');
++const express = require('express');
++const { validate } = require('openvibe-contracts');
++const { initDb } = require('../server/db/database');
++
++const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-admin-role-events-'));
++const log = console.log; console.log = () => {};
++const db = initDb(path.join(dir, 'network.db'));
++console.log = log;
++const profile = require('../server/identity/profile-events');
++profile.ensureSchema(db);
++profile.drain(db);
++db.prepare('DELETE FROM network_event_outbox').run();
++
++delete process.env.OWNER_USERNAME; // the default owner, 'goosely'
++const OWNER = 'usr_01JAB2C3D4E5F6G7H8J9K0MNPA';
++const MIA = 'usr_01JAB2C3D4E5F6G7H8J9K0MNPB';
++db.prepare("INSERT INTO users (id, username, password_hash, subject_id, role) VALUES (1, 'goosely', 'x', ?, 'admin')").run(OWNER);
++db.prepare("INSERT INTO users (id, username, password_hash, subject_id, role) VALUES (2, 'mia', 'x', ?, 'user')").run(MIA);
++profile.drain(db);
++db.prepare('DELETE FROM network_event_outbox').run();
++const events = () => db.prepare('SELECT envelope FROM network_event_outbox ORDER BY id').all().map((r) => JSON.parse(r.envelope))
++ .filter((e) => e.event_type === 'network.user.updated');
++
++// Any outbound call the admin routes make is recorded; none may go to Live's retired route.
++const outbound = [];
++const realFetch = global.fetch;
++global.fetch = (url, opts) => { outbound.push(String(url)); return Promise.resolve(new Response('{}', { status: 200 })); };
++
++const createAdminRoutes = require('../server/admin/routes');
++const owner = db.prepare('SELECT * FROM users WHERE id = 1').get();
++const app = express();
++app.use(express.json());
++app.use('/api/admin', createAdminRoutes(db, {}, {}, (req, res, next) => { req.user = owner; next(); }));
++const server = http.createServer(app);
++
++(async () => {
++ await new Promise((r) => server.listen(0, '127.0.0.1', r));
++ const base = `http://127.0.0.1:${server.address().port}/api/admin`;
++ const call = (method, p, body) => realFetch(base + p, { method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
++ .then(async (r) => ({ status: r.status, body: await r.json().catch(() => ({})) }));
++ const roleEvent = (n, role) => {
++ assert.strictEqual(profile.drain(db), 1, 'one event for the change');
++ const ev = events();
++ assert.strictEqual(ev.length, n);
++ const e = ev[n - 1];
++ assert.ok(validate('events.event-envelope@1', e).valid);
++ assert.ok(validate('network.user.updated@1', e.payload).valid);
++ assert.deepStrictEqual([e.subject.id, e.payload.role, e.payload.changed], [MIA, role, ['role']]);
++ };
++ try {
++ let r = await call('PUT', '/users/2/role', { role: 'global_mod' });
++ assert.strictEqual(r.status, 200, JSON.stringify(r.body));
++ roleEvent(1, 'global_mod');
++ r = await call('PUT', '/users/2/role', { role: 'user' });
++ assert.strictEqual(r.status, 200);
++ roleEvent(2, 'user'); // a downgrade too: Live applies it from the event
++ r = await call('POST', '/users/grant-admin', { id: 2 });
++ assert.strictEqual(r.status, 200, JSON.stringify(r.body));
++ roleEvent(3, 'admin');
++ assert.deepStrictEqual(outbound.filter((u) => /user-role/.test(u)), [], 'no push to Live\'s retired /internal/user-role');
++ const src = fs.readFileSync(path.join(__dirname, '../server/admin/routes.js'), 'utf8');
++ assert.ok(!/pushRoleToStreamer|fetch\([^)]*user-role/.test(src), 'the push is gone from the admin routes');
++ console.log('admin role events: all checks passed');
++ } finally {
++ global.fetch = realFetch;
++ server.close();
++ db.close();
++ fs.rmSync(dir, { recursive: true, force: true });
++ }
++})().catch((e) => { console.error(e); process.exit(1); });