openvibers/openvibe.network Commits

openvibe-contracts v0.126.0 (was v0.122.1); OpenRestream may bind Live slots (live.openre.slot.bind) (#110)

OP OpenVibers committed 9 Oct 2026 · 95f8cf7f37 · 9 files, +36 −29

* openvibe-contracts v0.126.0 (was v0.122.1)

Opened by scripts/pin-bump.js in OpenVibers/OpenVibe.Shared after the v0.126.0 release.

* Network: OpenRestream may bind Live slots to its streams (live.openre.slot.bind, OpenVibe Live on by default)

Contracts 0.126.0 (this pin) declares live.openre.slot.bind; svc:openre is granted it for openvibe.live, so a person's
new stream on openre.stream gets a slot on their Live channel (OpenRestream#28, Live#65). test/principals.test.js:
OpenRestream is the only holder.

* Network tests follow contracts 0.125.0: News, Reviews, Trade, Coupons and OpenRestream are live

- registry-exposure: they are live (34 live, 4 internal); Tips is the example of an internal service whose domain
  still serves a placeholder; the open sites list gains news, reviews, trade, coupons and stream.
- frame: the cold-start nav order includes them; Tips and VIP are the only planned sites left.
- events-consumer: a Trade alert now links the instrument (https://openvibe.trade/i/AAPL), as Trade has a public origin.

Changes

ddiff --git a/README.md b/README.md
iindex 6310a0fb..0092af81 100644
---- a/README.md
++++ b/README.md
@@@ -772,7 +772,7 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md). The rules the code keeps:
Same as the parent [OpenVibe](../LICENSE) project.
<!-- versions:start -->
--- openvibe-contracts: v0.122.1
++- openvibe-contracts: v0.126.0
- openvibe-sdk: v0.35.1
- openvibe-shared: v2.20.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex 7c03764b..34a0128f 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -6,7 +6,7 @@
"runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"port": 4000,
"domain": "openvibe.network (with auth., api., admin., my. and themes.openvibe.network in its manifest)",
-- "contracts": "openvibe-contracts v0.122.1",
++ "contracts": "openvibe-contracts v0.126.0",
"sdk": "openvibe-sdk v0.35.1",
"shared": "openvibe-shared v2.20.0",
"capabilities": [
ddiff --git a/package-lock.json b/package-lock.json
iindex 72ea1bc0..9a2de525 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -21,7 +21,7 @@
"iovalkey": "^0.4.0",
"jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.122.1",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.126.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.1",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.20.0",
"pg": "^8.23.0",
@@@ -1556,9 +1556,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.122.1",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.122.1",
-- "integrity": "sha512-+edJcURx0E76QWdgjr42fl0OgQxdWl7FA1OuWpopDKfO4ndidvf+GEz5skt1upSN4JLecLWrpMMyXJN27GjZsQ==",
++ "version": "0.126.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.126.0",
++ "integrity": "sha512-M/Vu5aMHEMtPI29xYaM03ZMbm2bfmyX2NVhEHNsjgw6dt/buW2g9tPE015VuBjO3j8LpkzsjWc2QXIJiajTs+w==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex b8927a0c..ae1064ef 100644
---- a/package.json
++++ b/package.json
@@@ -30,7 +30,7 @@
"iovalkey": "^0.4.0",
"jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.122.1",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.126.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.1",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.20.0",
"pg": "^8.23.0",
ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex 65a3a5a0..14d2cb7c 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -122,6 +122,9 @@ const DEFAULT_GRANTS = [
// session/output events (Live consumes openre.session.* by webhook).
...['openre.stream.read', 'openre.stream.write', 'openre.key.rotate', 'openre.session.read'].map(c => ['live', c, 'openvibe.openre', []]),
['openre', 'events.event.publish', 'openvibe.events', []],
++ // OpenVibe Live on by default (contracts 0.126.0): OpenRestream asks Live for a slot on the person's channel bound to
++ // their OpenRestream stream, so its sessions show on openvibe.live through Live's mirror.
++ ['openre', 'live.openre.slot.bind', 'openvibe.live', []],
// Wave 16: Wiki publishes events, attaches Community discussion, cites Sources items, reads its Media.
['wiki', 'events.event.publish', 'openvibe.events', []],
['wiki', 'community.comment.write', 'openvibe.community', []],
ddiff --git a/test/events-consumer.test.js b/test/events-consumer.test.js
iindex 3051d62c..8f6d84bd 100644
---- a/test/events-consumer.test.js
++++ b/test/events-consumer.test.js
@@@ -157,7 +157,7 @@ const rows = async (userId) => await db.prepare('SELECT * FROM notifications WHE
assert.strictEqual(bobs[0].title, 'Trade alert: AAPL');
assert.strictEqual(bobs[0].priority, 'high');
assert.strictEqual(bobs[0].message, 'Apple Inc. — close is 201.50 USD (above 200). Information only — not investment advice; no trading here.');
-- assert.strictEqual(bobs[0].url, null);
++ assert.strictEqual(bobs[0].url, 'https://openvibe.trade/i/AAPL', 'Trade serves openvibe.trade since contracts 0.125.0, so the alert links the instrument');
r = await post(tradeEvent({ payload: { ...tradeEvent().payload, trigger: { kind: 'document', id: 'doc_1', form_type: '8-K', title: 'Current report' } } }));
assert.strictEqual(r.body.outcome, 'notified');
assert.match((await rows(8))[1].message, /New 8-K: Current report\./);
ddiff --git a/test/frame.test.js b/test/frame.test.js
iindex d082f3ac..6a5b9992 100644
---- a/test/frame.test.js
++++ b/test/frame.test.js
@@@ -14,12 +14,12 @@ assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');
assert.equal(siteForHost('evil.example'), null);
assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the registry\'s featured list says so)');
let p = svc.payloadFor('openvibe.media');
--assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'deals', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory'], 'cold start follows the base order');
++assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'news', 'reviews', 'trade', 'host', 'deals', 'coupons', 'stream', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory'], 'cold start follows the base order');
assert.strictEqual(p.nav.find(n => n.id === 'space').url, 'https://openvibe.space/', 'Space keeps its own home (the forum returned to Community, 0.118.0)');
assert.strictEqual(p.nav.find(n => n.id === 'community').url, 'https://openvibe.community/', 'Community: the forum, pastes and Pulse');
assert.equal(p.footer.legal.dmca, 'https://openvibe.media/dmca', 'legal links stay on the site\'s own domain');
assert.ok(!p.footer.discover.some(l => l.url === 'https://openvibe.media/'), 'a site never recommends itself');
--assert.ok(p.soon.length >= 7, 'every planned site still listed as soon (deals and media-hub launched 2026-10-08: news, reviews, tips, vip, trade, coupons, stream)');
++assert.deepStrictEqual(p.soon.map(n => n.id || n), ['tips', 'vip'], 'Tips and VIP are the planned sites left (news, reviews, trade, coupons and stream launched 2026-10-09)');
// Real use reorders: community gets the history, so it rises above the cold-start order.
// user_history's foreign key needs its users (PostgreSQL enforces the constraint SQLite only declared).
ddiff --git a/test/principals.test.js b/test/principals.test.js
iindex 973b0524..2df75a45 100644
---- a/test/principals.test.js
++++ b/test/principals.test.js
@@@ -108,6 +108,9 @@ const server = http.createServer(app);
assert.strictEqual(t.status, 400, 'Live does not grant itself its own resolver');
const holders = (await 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']);
++ // OpenVibe Live on by default (contracts 0.126.0): only OpenRestream binds Live slots to its streams.
++ const binders = (await db.prepare("SELECT client_id FROM principal_grants WHERE capability = 'live.openre.slot.bind' AND audience = 'openvibe.live' AND revoked_at IS NULL ORDER BY client_id").all()).map(x => x.client_id);
++ assert.deepStrictEqual(binders, ['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 network.account.deletion.confirm network.account.export.contribute', 'Media resolves object owners to subjects and takes part in account export and deletion (ADR-033), nothing else here');
ddiff --git a/test/registry-exposure.test.js b/test/registry-exposure.test.js
iindex e0ddd1d9..aa88ae62 100644
---- a/test/registry-exposure.test.js
++++ b/test/registry-exposure.test.js
@@@ -17,8 +17,8 @@ const { SITES } = require('../server/frame/sites');
const PINNED = {
network: 'live', live: 'live', tools: 'live', media: 'live', games: 'live', community: 'live', events: 'live',
billing: 'live', codes: 'live', blog: 'live', wiki: 'live', sites: 'live',
-- news: 'internal', reviews: 'internal', deals: 'live', coupons: 'internal', trade: 'internal', host: 'live',
-- tips: 'internal', vip: 'internal', openre: 'internal', search: 'live', sources: 'internal', chat: 'live', 'media-hub': 'live', space: 'live', // a home and redirects since the forum returned to Community (contracts 0.118.0)
++ news: 'live', reviews: 'live', deals: 'live', coupons: 'live', trade: 'live', host: 'live', // News, Reviews, Coupons, Trade live since contracts 0.125.0
++ tips: 'internal', vip: 'internal', openre: 'live', search: 'live', sources: 'internal', chat: 'live', 'media-hub': 'live', space: 'live', // a home and redirects since the forum returned to Community (contracts 0.118.0)
ai: 'live', // the public home and developer-app API at ai.openvibe.services since 2026-10-07 (contracts 0.110.0)
// openvibe-contracts v0.86-v0.90: Bot runs (its own exposure row); Zone is a charter only.
bot: 'live', zone: 'placeholder',
@@@ -33,7 +33,7 @@ const PINNED = {
sdk: 'library', shared: 'library', contracts: 'library', publishing: 'library',
examples: 'repository',
};
--const PLACEHOLDER_DOMAINS = ['news', 'reviews', 'coupons', 'trade', 'tips', 'vip', 'openre']; // deals: live since contracts 0.118.0
++const PLACEHOLDER_DOMAINS = ['tips', 'vip']; // deals: live since contracts 0.118.0; news, reviews, coupons, trade, openre since 0.125.0
function fakeService(routes) {
const srv = http.createServer((req, res) => {
@@@ -77,20 +77,20 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
const d = await json('/.well-known/openvibe');
const dBy = Object.fromEntries(d.services.map(s => [s.id, s]));
-- assert.strictEqual(dBy.news.state, 'internal');
-- assert.strictEqual(dBy.news.origin, null, 'clients never route to a placeholder page');
-- assert.strictEqual(dBy.news.planned_origin, 'https://openvibe.news');
++ assert.strictEqual(dBy.tips.state, 'internal');
++ assert.strictEqual(dBy.tips.origin, null, 'clients never route to a placeholder page');
++ assert.strictEqual(dBy.tips.planned_origin, 'https://openvibe.tips');
assert.strictEqual(dBy.live.origin, 'https://openvibe.live');
assert.strictEqual(dBy.live.state, 'live');
for (const s of d.services) assert.strictEqual(s.state, PINNED[s.id]);
let r = await json('/api/v1/registry/services');
const by = Object.fromEntries(r.services.map(s => [s.id, s]));
-- assert.strictEqual(by.news.exposure.state, 'internal');
-- assert.strictEqual(by.news.exposure.public_site, 'placeholder');
-- assert.strictEqual(by.news.runtime.status, 'up');
-- assert.strictEqual(by.news.runtime.scope, 'loopback');
-- assert.match(by.news.runtime.reason, /loopback only/);
++ assert.strictEqual(by.tips.exposure.state, 'internal');
++ assert.strictEqual(by.tips.exposure.public_site, 'placeholder');
++ assert.strictEqual(by.tips.runtime.status, 'up');
++ assert.strictEqual(by.tips.runtime.scope, 'loopback');
++ assert.match(by.tips.runtime.reason, /loopback only/);
assert.strictEqual(by.deals.exposure.state, 'live', 'Deals serves openvibe.deals since contracts 0.118.0');
assert.strictEqual(by.ai.runtime.status, 'up', 'AI is polled on loopback although its pinned manifest says placeholder');
assert.strictEqual(by.ai.runtime.basis, 'ready');
@@@ -102,21 +102,22 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
assert.strictEqual(by.live.runtime.scope, 'public');
r = await json('/api/v1/registry/services?state=internal');
assert.deepStrictEqual(r.services.map(s => s.id).sort(), Object.keys(PINNED).filter(k => PINNED[k] === 'internal').sort());
-- r = await json('/api/v1/registry/domains/openvibe.coupons');
++ r = await json('/api/v1/registry/domains/openvibe.tips');
assert.strictEqual(r.service.exposure.state, 'internal');
const st = await json('/api/v1/status');
const sBy = Object.fromEntries(st.services.map(s => [s.id, s]));
-- assert.strictEqual(sBy.news.label, 'up (loopback only)');
-- assert.strictEqual(sBy.news.origin, null);
-- assert.strictEqual(sBy.news.planned_origin, 'https://openvibe.news');
++ assert.strictEqual(sBy.tips.label, 'up (loopback only)');
++ assert.strictEqual(sBy.tips.origin, null);
++ assert.strictEqual(sBy.tips.planned_origin, 'https://openvibe.tips');
assert.strictEqual(sBy.live.label, 'up');
assert.strictEqual(sBy.live.origin, 'https://openvibe.live');
assert.strictEqual(sBy.shared.label, `not running (library, released v${require('openvibe-shared/package.json').version})`);
// Contracts 0.83.0 added two placeholder services (media-hub, space), v0.86-v0.90 Bot (live) and two more (zone, run),
// v0.95.0 Services; Space and AI went live in v0.110.0; Food, Help and Work in v0.116.0; Quest, Rent and Watch in v0.117.0;
// Inventory in v0.122.0; none is retired
-- assert.deepStrictEqual(st.exposure_summary, { live: 29, internal: 9, library: 4, repository: 1, placeholder: 1, retired: 0 });
++ // News, Reviews, Coupons, Trade and OpenRestream in v0.125.0
++ assert.deepStrictEqual(st.exposure_summary, { live: 34, internal: 4, library: 4, repository: 1, placeholder: 1, retired: 0 });
// The page: no row for a service whose domain serves a placeholder shows a bare "Up".
const html = await fetch(base + '/status').then(x => x.text());
@@@ -126,18 +127,18 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
assert.ok(row.includes('Up · loopback only'), `${id} row says loopback only`);
assert.ok(row.includes('internal (loopback only, no public site yet)'), `${id} row names its exposure`);
}
-- assert.ok(/<tr id="svc-news">[\s\S]*?openvibe\.news: not this service yet/.test(html));
++ assert.ok(/<tr id="svc-tips">[\s\S]*?openvibe\.tips: not this service yet/.test(html));
assert.ok(/<tr id="svc-live">[\s\S]*?>Up<\/span>/.test(html), 'a public service still reads Up');
assert.ok(/<tr id="svc-ai">[\s\S]*?>Up<\/span>/.test(html), 'AI serves its own public origin since contracts 0.110.0');
// 3. The Frame: the nav lists only sites whose domain serves the service; the rest are soon with a reason.
const open = SITES.filter(s => s.status === 'open').map(s => s.id);
-- assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'deals', 'space', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory']); // by site position; bot: a site since openvibe-contracts 0.106.0; host: live since 0.107.0; space: live since 0.110.0 (the forum, 2026-10-07)
++ assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'news', 'reviews', 'trade', 'host', 'deals', 'coupons', 'stream', 'space', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory']); // news, reviews, trade, coupons and stream (OpenRestream): open since 0.125.0; // by site position; bot: a site since openvibe-contracts 0.106.0; host: live since 0.107.0; space: live since 0.110.0 (the forum, 2026-10-07)
for (const s of SITES) {
assert.ok(s.service && PINNED[s.service], `${s.id} names its service`);
assert.strictEqual(s.status === 'open', PINNED[s.service] === 'live', `${s.id} nav status follows exposure`);
}
-- for (const id of ['news', 'reviews', 'coupons', 'trade', 'tips', 'vip', 'stream']) {
++ for (const id of ['tips', 'vip']) {
const s = SITES.find(x => x.id === id);
assert.strictEqual(s.status, 'soon'); assert.strictEqual(s.state, 'internal');
}