openvibers/openvibe.live Commits
openvibe-contracts v0.32.0: live.lineage.resolve is written inline in the guard, so the CI contract check enforces it (6 capabilities now); the lineage resolver test requires lineage.resolution@1 and validates every answer; live.stream.started/ended and live.release.deployed payloads are validated against their new contracts (and the started event's channel link is /@<username>). Stale comments saying these were not in the registry are gone
Changes
@@@ -25,7 +25,7 @@"multer": "^1.4.5-lts.1","node-media-server": "^2.7.1","onnxruntime-node": "^1.20.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.2",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0","sanitize-html": "^2.17.7",@@@ -2814,9 +2814,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.30.2",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.2",-- "integrity": "sha512-R01NgRY5V/GEYWkWUyxfl1MifSQb4dfJy11VS9RP+wsWedh2Crr9/IL7yGSn1PytwTKKL+tQl/r4ppGfH3TwLg==",++ "version": "0.32.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0",++ "integrity": "sha512-ntN58SIG03YDxbhsiIzOM5GRH19jRMHZUYWI3xU2VwovExagF5DdwApLVfezsu84UTJ+V2Jfn+LWDc+Oz8ab+g==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -43,7 +43,7 @@"multer": "^1.4.5-lts.1","node-media-server": "^2.7.1","onnxruntime-node": "^1.20.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.2",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0","sanitize-html": "^2.17.7",@@@ -15,8 +15,8 @@* first, at most 40), deployed_at, notes_url }** Visibility is `internal`: the commit subjects are public in chat and on /updates already, but-- * the consumer (the chat notice) decides what is shown. No contract in openvibe-contracts v0.30.1-- * defines this type yet; it is listed as a contract addition for the `live.*` namespace Live owns.++ * the consumer (the chat notice) decides what is shown. Contract: live.release.deployed@1++ * (openvibe-contracts 0.32.0).*/const EVENT_TYPE = 'live.release.deployed';const NOTES_URL = 'https://openvibe.live/updates';@@@ -10,23 +10,14 @@** Every well-formed request is answered 200 with a lineage.resolution@1, resolved or unresolved* (a conflict, a display name alone, Media down); a malformed one is 400 lineage.invalid_request.-- *-- * The capability id is passed as a constant, not a string literal inside the guard call:-- * live.lineage.resolve is registered in openvibe-contracts 0.32.0, Live still pins 0.30.2, and-- * `openvibe-contracts-check` in CI fails on a literal capability id the pinned registry does not-- * define (it scans comments too). guard() checks the grant itself while the id is unknown to the-- * registry and switches to capabilities.check() once the pin moves. When Live pins 0.32.0+, inline-- * the id so the CI check enforces it.*/const express = require('express');const { http } = require('openvibe-contracts');const { guard } = require('../net/service-guard');const resolver = require('./resolver');--const CAPABILITY = 'live.lineage.resolve';--const router = express.Router();--router.use(guard(CAPABILITY));++router.use(guard('live.lineage.resolve'));async function answer(req, res, raw, flat) {const { input, error } = resolver.normalizeRequest(raw, { flat });@@@ -7,11 +7,10 @@* today the ones OpenVibe.Chat calls (/internal/chat-context/*, /internal/chat-effects/*). The* token is verified with the same Network key Live verifies user JWTs with.*-- * Chat's capabilities (live.chat_context.read, live.chat_effects.write, live.chat_mirror.write)-- * are proposals not yet in the openvibe-contracts registry, whose requireCapability() refuses-- * unknown ids; until they are registered the grant is checked with capabilities.grants() and the-- * guard switches to capabilities.check() by itself once the registry knows the id. Like-- * X-Internal-Key routes, these are loopback-only: anything that came through nginx is refused.++ * The capability ids are registered in openvibe-contracts (checked with capabilities.check()); an++ * id the pinned registry does not know yet is checked as a plain grant with capabilities.grants()++ * until the pin moves. Like X-Internal-Key routes, these are loopback-only: anything that came++ * through nginx is refused.*/const { serviceAuth, capabilities, http } = require('openvibe-contracts');const { getNetworkPublicKey, getNetworkIssuer } = require('../auth/auth');@@@ -72,9 +72,10 @@ const mediaServer = http.createServer((req, res) => {send(404, { error: 'no route' });});--// Every contract answer is checked against lineage.resolution@1 when the installed contracts know it.++// Every contract answer is checked against lineage.resolution@1 (openvibe-contracts 0.32.0+, which Live pins).const RESOLUTION = 'lineage.resolution@1';const contractKnown = (() => { try { contracts.resolve(RESOLUTION); return true; } catch { return false; } })();++assert.ok(contractKnown, `the pinned openvibe-contracts defines ${RESOLUTION}`);let validated = 0;function checkContract(out, label) {if (!contractKnown) return out;@@@ -53,6 +53,7 @@ const stub = http.createServer((req, res) => {const sample = releaseEvents.envelopeFor({ head, previous: null, commits: [{ hash: head, short: head.slice(0, 7), subject: 'x', date: '2026-09-23T00:00:00Z' }] });const full = { ...sample, event_id: 'evt_01JAB2C3D4E5F6G7H8J9K0MNPQ', version: 1, source: 'live', timestamp: new Date().toISOString() };contracts.assertValid('events.event-envelope@1', full);++ contracts.assertValid('live.release.deployed@1', full.payload);assert.strictEqual(sample.payload.release, head.slice(0, 7));assert.throws(() => releaseEvents.envelopeFor({ head: 'nope' }), /full commit sha/);@@@ -75,6 +75,8 @@ const stub = http.createServer((req, res) => {assert.strictEqual(ev.payload.channel.username, 'streamer');assert.match(ev.payload.started_at, /^\d{4}-\d\d-\d\dT/);assert.match(ev.event_id, /^evt_[0-9A-HJKMNP-TV-Z]{26}$/);++ assert.strictEqual(ev.payload.channel.url, 'https://openvibe.live/@streamer', 'the channel link is the channel page');++ require('openvibe-contracts').assertValid('live.stream.started@1', ev.payload);// Ending a stream that is already ended emits nothing; ending a live one emits ended.db.endStream(id);@@@ -84,6 +86,7 @@ const stub = http.createServer((req, res) => {assert.strictEqual(published[1].event_type, 'live.stream.ended');assert.strictEqual(published[1].subject.revision, 2);assert.ok('duration_seconds' in published[1].payload);++ require('openvibe-contracts').assertValid('live.stream.ended@1', published[1].payload);// A rolled-back go-live leaves no event.assert.throws(() => d.transaction(() => { db.createStream({ user_id: 501, title: 'rolled back' }); throw new Error('abort'); })());
@@ -25,7 +25,7 @@
@@ -25,7 +25,7 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"node-media-server": "^2.7.1",
"node-media-server": "^2.7.1",
"onnxruntime-node": "^1.20.0",
"onnxruntime-node": "^1.20.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.2",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
"sanitize-html": "^2.17.7",
"sanitize-html": "^2.17.7",
@@ -2814,9 +2814,9 @@
@@ -2814,9 +2814,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.30.2",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.2",
- "integrity": "sha512-R01NgRY5V/GEYWkWUyxfl1MifSQb4dfJy11VS9RP+wsWedh2Crr9/IL7yGSn1PytwTKKL+tQl/r4ppGfH3TwLg==",
+ "version": "0.32.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0",
+ "integrity": "sha512-ntN58SIG03YDxbhsiIzOM5GRH19jRMHZUYWI3xU2VwovExagF5DdwApLVfezsu84UTJ+V2Jfn+LWDc+Oz8ab+g==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -43,7 +43,7 @@
@@ -43,7 +43,7 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"node-media-server": "^2.7.1",
"node-media-server": "^2.7.1",
"onnxruntime-node": "^1.20.0",
"onnxruntime-node": "^1.20.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.2",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
"sanitize-html": "^2.17.7",
"sanitize-html": "^2.17.7",
@@ -15,8 +15,8 @@
@@ -15,8 +15,8 @@
* first, at most 40), deployed_at, notes_url }
* first, at most 40), deployed_at, notes_url }
*
*
* Visibility is `internal`: the commit subjects are public in chat and on /updates already, but
* Visibility is `internal`: the commit subjects are public in chat and on /updates already, but
- * the consumer (the chat notice) decides what is shown. No contract in openvibe-contracts v0.30.1
- * defines this type yet; it is listed as a contract addition for the `live.*` namespace Live owns.
+ * the consumer (the chat notice) decides what is shown. Contract: live.release.deployed@1
+ * (openvibe-contracts 0.32.0).
*/
*/
const EVENT_TYPE = 'live.release.deployed';
const EVENT_TYPE = 'live.release.deployed';
const NOTES_URL = 'https://openvibe.live/updates';
const NOTES_URL = 'https://openvibe.live/updates';
@@ -10,23 +10,14 @@
@@ -10,23 +10,14 @@
*
*
* Every well-formed request is answered 200 with a lineage.resolution@1, resolved or unresolved
* Every well-formed request is answered 200 with a lineage.resolution@1, resolved or unresolved
* (a conflict, a display name alone, Media down); a malformed one is 400 lineage.invalid_request.
* (a conflict, a display name alone, Media down); a malformed one is 400 lineage.invalid_request.
- *
- * The capability id is passed as a constant, not a string literal inside the guard call:
- * live.lineage.resolve is registered in openvibe-contracts 0.32.0, Live still pins 0.30.2, and
- * `openvibe-contracts-check` in CI fails on a literal capability id the pinned registry does not
- * define (it scans comments too). guard() checks the grant itself while the id is unknown to the
- * registry and switches to capabilities.check() once the pin moves. When Live pins 0.32.0+, inline
- * the id so the CI check enforces it.
*/
*/
const express = require('express');
const express = require('express');
const { http } = require('openvibe-contracts');
const { http } = require('openvibe-contracts');
const { guard } = require('../net/service-guard');
const { guard } = require('../net/service-guard');
const resolver = require('./resolver');
const resolver = require('./resolver');
-const CAPABILITY = 'live.lineage.resolve';
-
const router = express.Router();
const router = express.Router();
-router.use(guard(CAPABILITY));
+router.use(guard('live.lineage.resolve'));
async function answer(req, res, raw, flat) {
async function answer(req, res, raw, flat) {
const { input, error } = resolver.normalizeRequest(raw, { flat });
const { input, error } = resolver.normalizeRequest(raw, { flat });
@@ -7,11 +7,10 @@
@@ -7,11 +7,10 @@
* today the ones OpenVibe.Chat calls (/internal/chat-context/*, /internal/chat-effects/*). The
* today the ones OpenVibe.Chat calls (/internal/chat-context/*, /internal/chat-effects/*). The
* token is verified with the same Network key Live verifies user JWTs with.
* token is verified with the same Network key Live verifies user JWTs with.
*
*
- * Chat's capabilities (live.chat_context.read, live.chat_effects.write, live.chat_mirror.write)
- * are proposals not yet in the openvibe-contracts registry, whose requireCapability() refuses
- * unknown ids; until they are registered the grant is checked with capabilities.grants() and the
- * guard switches to capabilities.check() by itself once the registry knows the id. Like
- * X-Internal-Key routes, these are loopback-only: anything that came through nginx is refused.
+ * The capability ids are registered in openvibe-contracts (checked with capabilities.check()); an
+ * id the pinned registry does not know yet is checked as a plain grant with capabilities.grants()
+ * until the pin moves. Like X-Internal-Key routes, these are loopback-only: anything that came
+ * through nginx is refused.
*/
*/
const { serviceAuth, capabilities, http } = require('openvibe-contracts');
const { serviceAuth, capabilities, http } = require('openvibe-contracts');
const { getNetworkPublicKey, getNetworkIssuer } = require('../auth/auth');
const { getNetworkPublicKey, getNetworkIssuer } = require('../auth/auth');
@@ -72,9 +72,10 @@ const mediaServer = http.createServer((req, res) => {
@@ -72,9 +72,10 @@ const mediaServer = http.createServer((req, res) => {
send(404, { error: 'no route' });
send(404, { error: 'no route' });
});
});
-// Every contract answer is checked against lineage.resolution@1 when the installed contracts know it.
+// Every contract answer is checked against lineage.resolution@1 (openvibe-contracts 0.32.0+, which Live pins).
const RESOLUTION = 'lineage.resolution@1';
const RESOLUTION = 'lineage.resolution@1';
const contractKnown = (() => { try { contracts.resolve(RESOLUTION); return true; } catch { return false; } })();
const contractKnown = (() => { try { contracts.resolve(RESOLUTION); return true; } catch { return false; } })();
+assert.ok(contractKnown, `the pinned openvibe-contracts defines ${RESOLUTION}`);
let validated = 0;
let validated = 0;
function checkContract(out, label) {
function checkContract(out, label) {
if (!contractKnown) return out;
if (!contractKnown) return out;
@@ -53,6 +53,7 @@ const stub = http.createServer((req, res) => {
@@ -53,6 +53,7 @@ const stub = http.createServer((req, res) => {
const sample = releaseEvents.envelopeFor({ head, previous: null, commits: [{ hash: head, short: head.slice(0, 7), subject: 'x', date: '2026-09-23T00:00:00Z' }] });
const sample = releaseEvents.envelopeFor({ head, previous: null, commits: [{ hash: head, short: head.slice(0, 7), subject: 'x', date: '2026-09-23T00:00:00Z' }] });
const full = { ...sample, event_id: 'evt_01JAB2C3D4E5F6G7H8J9K0MNPQ', version: 1, source: 'live', timestamp: new Date().toISOString() };
const full = { ...sample, event_id: 'evt_01JAB2C3D4E5F6G7H8J9K0MNPQ', version: 1, source: 'live', timestamp: new Date().toISOString() };
contracts.assertValid('events.event-envelope@1', full);
contracts.assertValid('events.event-envelope@1', full);
+ contracts.assertValid('live.release.deployed@1', full.payload);
assert.strictEqual(sample.payload.release, head.slice(0, 7));
assert.strictEqual(sample.payload.release, head.slice(0, 7));
assert.throws(() => releaseEvents.envelopeFor({ head: 'nope' }), /full commit sha/);
assert.throws(() => releaseEvents.envelopeFor({ head: 'nope' }), /full commit sha/);
@@ -75,6 +75,8 @@ const stub = http.createServer((req, res) => {
@@ -75,6 +75,8 @@ const stub = http.createServer((req, res) => {
assert.strictEqual(ev.payload.channel.username, 'streamer');
assert.strictEqual(ev.payload.channel.username, 'streamer');
assert.match(ev.payload.started_at, /^\d{4}-\d\d-\d\dT/);
assert.match(ev.payload.started_at, /^\d{4}-\d\d-\d\dT/);
assert.match(ev.event_id, /^evt_[0-9A-HJKMNP-TV-Z]{26}$/);
assert.match(ev.event_id, /^evt_[0-9A-HJKMNP-TV-Z]{26}$/);
+ assert.strictEqual(ev.payload.channel.url, 'https://openvibe.live/@streamer', 'the channel link is the channel page');
+ require('openvibe-contracts').assertValid('live.stream.started@1', ev.payload);
// Ending a stream that is already ended emits nothing; ending a live one emits ended.
// Ending a stream that is already ended emits nothing; ending a live one emits ended.
db.endStream(id);
db.endStream(id);
@@ -84,6 +86,7 @@ const stub = http.createServer((req, res) => {
@@ -84,6 +86,7 @@ const stub = http.createServer((req, res) => {
assert.strictEqual(published[1].event_type, 'live.stream.ended');
assert.strictEqual(published[1].event_type, 'live.stream.ended');
assert.strictEqual(published[1].subject.revision, 2);
assert.strictEqual(published[1].subject.revision, 2);
assert.ok('duration_seconds' in published[1].payload);
assert.ok('duration_seconds' in published[1].payload);
+ require('openvibe-contracts').assertValid('live.stream.ended@1', published[1].payload);
// A rolled-back go-live leaves no event.
// A rolled-back go-live leaves no event.
assert.throws(() => d.transaction(() => { db.createStream({ user_id: 501, title: 'rolled back' }); throw new Error('abort'); })());
assert.throws(() => d.transaction(() => { db.createStream({ user_id: 501, title: 'rolled back' }); throw new Error('abort'); })());