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
Changes
@@@ -49,3 +49,9 @@ Live, already holds `identity.subject.resolve` and `network.coins.credit` on `opadding 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`) |@@@ -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"},@@@ -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 });@@@ -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.nnew file mode 100644@@@ -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); });
@@ -49,3 +49,9 @@ Live, already holds `identity.subject.resolve` and `network.coins.credit` on `op
@@ -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
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
`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.
(`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`) |
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
"scripts": {
"scripts": {
"start": "node server/index.js",
"start": "node server/index.js",
"dev": "NODE_ENV=development 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",
"reset-db": "node server/reset-db.js",
"grant-admin": "node server/grant-admin.js"
"grant-admin": "node server/grant-admin.js"
},
},
@@ -215,24 +215,9 @@ function createAdminRoutes(db, notificationService, emailService, requireAuth) {
@@ -215,24 +215,9 @@ function createAdminRoutes(db, notificationService, emailService, requireAuth) {
return refreshServiceByKey(req, key);
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);
router.use(requireAuth, requireAdmin);
@@ -582,7 +567,6 @@ function createAdminRoutes(db, notificationService, emailService, requireAuth) {
@@ -582,7 +567,6 @@ function createAdminRoutes(db, notificationService, emailService, requireAuth) {
'grant_admin',
'grant_admin',
JSON.stringify({ targetId: user.id, targetUsername: user.username, targetEmail: user.email }),
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' } });
res.json({ ok: true, user: { id: user.id, username: user.username, email: user.email, role: 'admin' } });
} catch (err) {
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
res.status(500).json({ ok: false, error: err.message });
@@ -740,8 +724,6 @@ function createAdminRoutes(db, notificationService, emailService, requireAuth) {
@@ -740,8 +724,6 @@ function createAdminRoutes(db, notificationService, emailService, requireAuth) {
db.prepare('INSERT INTO audit_log (user_id, action, details) VALUES (?, ?, ?)').run(
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 })
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 });
res.json({ ok: true });
} catch (err) {
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
res.status(500).json({ ok: false, error: err.message });
@@ -676,9 +676,10 @@ function initDb(dbPath) {
@@ -676,9 +676,10 @@ function initDb(dbPath) {
}
}
// Roles are Network's own data. Network never opens another service's database (ADR-007):
// 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 ───────────────────────────────────
// ── Helper: getSetting ───────────────────────────────────
// Provider secrets (server/secrets.js) come from their environment variable when it is set.
// Provider secrets (server/secrets.js) come from their environment variable when it is set.
new file mode 100644
new file mode 100644
@@ -0,0 +1,80 @@
@@ -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); });