openvibers/openvibe.network Commits

Principals: openre, media and community hold live.lineage.resolve for audience openvibe.live (roadmap §10.5/§15.10, D20-R1), so OpenRe, Media and Community's Pulse can ask Live's canonical channel/owner resolver (GET|POST /internal/lineage/resolve, token only) instead of keeping their own channel mappings; production had no grant for it. Seeded at boot like every default grant. test/principals.test.js: each of the three gets a token for openvibe.live carrying it, and nobody else holds it

OP OpenVibers committed 24 Sept 2026 · 80a841cd81 · 2 files, +17 −0


Changes

ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex c9503299..b0dc8bfe 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -130,6 +130,10 @@ const DEFAULT_GRANTS = [
['live', 'community.paste.create', 'openvibe.community', []],
['live', 'community.paste.write', 'openvibe.community', []],
['live', 'community.paste.moderate', 'openvibe.community', []],
++ // One canonical channel/owner resolver (roadmap §10.5/§15.10, D20-R1): OpenRe, Media and Community (Pulse)
++ // resolve channels, streams, VODs and clips through Live's /internal/lineage/resolve instead of their own
++ // channel mappings.
++ ...['openre', 'media', 'community'].map(c => [c, 'live.lineage.resolve', 'openvibe.live', []]),
// Wave 5 remainder: Live comments on its own entities, publishes stream/VOD items to Pulse, and
// hides a thread when it takes the entity down.
['live', 'community.comment.write', 'openvibe.community', []],
ddiff --git a/test/principals.test.js b/test/principals.test.js
iindex ba15e92b..b265c772 100644
---- a/test/principals.test.js
++++ b/test/principals.test.js
@@@ -67,6 +67,19 @@ const server = http.createServer(app);
db.prepare("UPDATE oauth_clients SET client_secret = 'openre-secret' WHERE client_id = 'openre'").run();
t = await token({ client_id: 'openre', client_secret: 'openre-secret', audience: 'openvibe.network' });
assert.strictEqual(t.status, 400); assert.strictEqual(t.body.error, 'invalid_scope', 'a client with no grants gets no token');
++ // The canonical channel/owner resolver on Live (D20-R1): OpenRe, Media and Community hold it, nobody else.
++ db.prepare("UPDATE oauth_clients SET client_secret = 'community-secret' WHERE client_id = 'community'").run();
++ for (const [client, secret] of [['openre', 'openre-secret'], ['media', 'media-secret'], ['community', 'community-secret']]) {
++ t = await token({ client_id: client, client_secret: secret, audience: 'openvibe.live' });
++ assert.strictEqual(t.status, 200, `${client}: ${JSON.stringify(t.body)}`);
++ assert.ok(t.body.scope.split(' ').includes('live.lineage.resolve'), `${client} may resolve lineage on Live`);
++ const c = serviceAuth.verifyServiceToken(t.body.access_token, { publicKey: keys.publicKey, issuer: ISSUER, audience: 'openvibe.live' });
++ assert.ok(c.ok && c.claims.sub === `svc:${client}` && c.claims.aud.includes('openvibe.live'));
++ }
++ t = await token({ client_id: 'live', client_secret: 'live-secret', audience: 'openvibe.live', scope: 'live.lineage.resolve' });
++ assert.strictEqual(t.status, 400, 'Live does not grant itself its own resolver');
++ const holders = db.prepare("SELECT client_id FROM principal_grants WHERE capability = 'live.lineage.resolve' AND audience = 'openvibe.live' AND revoked_at IS NULL ORDER BY client_id").all().map(x => x.client_id);
++ assert.deepStrictEqual(holders, ['community', 'media', 'openre']);
t = await token({ client_id: 'media', client_secret: 'media-secret', audience: 'openvibe.network' });
assert.strictEqual(t.status, 200, JSON.stringify(t.body));
assert.strictEqual(t.body.scope, 'identity.subject.resolve', 'Media resolves object owners to subjects, and nothing else here');