openvibers/openvibe.network Commits
openvibe-shared v1.5.0 (Track R release manifest), openvibe-contracts v0.32.0, openvibe-sdk v0.5.0
Changes
@@@ -350,9 +350,14 @@ Google-style account management supporting up to 5 accounts:Otherwise it is 200, with `status: "degraded"` when an optional check has failed.- `GET /metrics`: Prometheus text for direct loopback callers only. Any proxied request gets a 404,and `deploy/nginx` blocks the path too. Besides the HTTP golden signals by route template, it-- exports process metrics and `release_info`. Network's own counters are++ exports process metrics, `release_info` and `release_client_updates_total{outcome,reason}` (what++ open tabs report to `POST /release-metrics`). Network's own counters are`network_tokens_issued_total{grant_type}`, `network_token_failures_total{grant_type,error}` and`network_principal_token_failures_total{code}`.++- `GET /release.json` is Network's release manifest (ADR-016, `registry.release-manifest@1`, from++ `openvibe-shared/release`'s `release.mount`): the deployed commit, the library versions and, since++ openvibe-contracts 0.32.0, the 1.1.0 fields. No components are declared, so every release still++ prompts open tabs to reload.- `GET /status` is the operator page: server-rendered, no JavaScript needed, `noindex`.`GET /api/v1/status` returns the same data as JSON. Each OpenVibe service shows as up, degraded,down, not running (placeholder or no runtime) or unknown, with its release, boot time and@@@ -371,9 +376,10 @@ Google-style account management supporting up to 5 accounts:consumer pattern and the payload contract; filters `service`, `producer`, `consumer`, `prefix`),`/topics/:topic`, `/releases` (each running service's `/release.json` from the same loopback poll,with its installed openvibe-contracts/sdk/shared versions and drift against the libraries' current-- releases), `/health` and `/search?q=`. Live and Media list no `eventsProduced` in openvibe-contracts-- 0.30.1; the registry adds what their code publishes (`server/registry/topics.js` OBSERVED, marked-- `observed` on the service), and Network's consumed topics come from its own Events consumer.++ releases), `/health` and `/search?q=`. A type a service's code publishes before its manifest lists++ it is added from `server/registry/topics.js` OBSERVED (marked `observed` on the service; empty since++ openvibe-contracts 0.32.0 lists Live's, Media's and Network's), and Network's consumed topics come++ from its own Events consumer.Payload schemas are served at their `$id` (`/contracts/events/payloads/<type>.v1.json`).- `node scripts/contracts-drift.js` warns about services that pin an older openvibe-contracts thanthe latest tag, or a tag that was never published (`--dir ~/OpenVibers` for local checkouts,@@@ -20,9 +20,9 @@"helmet": "^8.1.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.30.1",-- "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-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.5.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.0","uuid": "^11.1.0","web-push": "^3.6.7"}@@@ -1465,9 +1465,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.30.1",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.1",-- "integrity": "sha512-JAOna8MA/+4iABEZ5qnzGjW27z9UgAHDF1Pj4VE+R3tb1UVyRUVZX06yuE+BggTdhZPGZRq5mFFIyPNHp8N3GA==",++ "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",@@@ -1481,18 +1481,18 @@}},"node_modules/openvibe-sdk": {-- "version": "0.4.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",-- "integrity": "sha512-Hyz55YOMIs0xwCQ1NXsN0sUYhlbKXAX3uWY2fPCdEEL18FXUYVhK8wlG/5JMMypFHnjDiSd9VHzBNG12hWsmkQ==",++ "version": "0.5.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",++ "integrity": "sha512-QVJkR5iazQX0ZwIrDp2A88ZeYYBSXEmSzhpgURPUSnzezIM3jz0HBhVKowyhkvg2aBO61XNf77eb4zJGkMjZiw==","license": "MIT","engines": {"node": ">=20"}},"node_modules/openvibe-shared": {-- "version": "1.4.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",-- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",++ "version": "1.5.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.0",++ "integrity": "sha512-cehnscuYMkJwuCYGcI6wPSswbUn0bhTyGAaD5feNCoO//2e9gyC7cgjkEcIOtM8DQF3yTuUNyXnOwckIeM4fUg==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -23,9 +23,9 @@"helmet": "^8.1.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.30.1",-- "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-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.5.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.0","uuid": "^11.1.0","web-push": "^3.6.7"}@@@ -37,14 +37,12 @@ const moduleEvents = require('./module-events');const SUBJECT_RE = /^(usr|gst)_[0-9A-HJKMNP-TV-Z]{26}$/;/**-- * Namespaces whose owner moved before openvibe-contracts caught up. The manifest of chat.preferences-- * (0.30.x) still names live and says it "moves to OpenVibe.Chat in Wave 6"; Chat has served Live's chat-- * since the Wave 6 cutover (2026-09-23). The handoff applies only while the installed manifest still-- * names `from`: once Contracts publishes owner `to` it is a no-op, and the entry can be deleted.++ * Namespaces whose owner moved before openvibe-contracts caught up: `{ [namespace]: { from, to } }`.++ * A handoff applies only while the installed manifest still names `from`: once Contracts publishes++ * owner `to` it is a no-op, and the entry can be deleted. None is pending: chat.preferences, the last++ * one (live → chat, the Wave 6 cutover), is owned by chat in the contracts since openvibe-contracts 0.32.0.*/--const OWNER_HANDOFFS = Object.freeze({-- 'chat.preferences': Object.freeze({ from: 'live', to: 'chat' }),--});++const OWNER_HANDOFFS = Object.freeze({});/** The service that owns a namespace now (the manifest's owner unless a handoff moved it). */function ownerOf(namespace) {@@@ -22,7 +22,7 @@ const DEFAULT_GRANTS = [['live', 'network.coins.debit', SELF_AUDIENCE, ['live']],['live', 'network.notifications.push', SELF_AUDIENCE, ['live']],// User modules: each service reads and writes the namespaces it owns (openvibe-contracts manifests/namespaces).-- // chat.preferences moved to Chat with the Wave 6 cutover (modules.js OWNER_HANDOFFS); Live keeps reading it.++ // chat.preferences moved to Chat with the Wave 6 cutover (its owner in the contracts); Live keeps reading it.['live', 'network.modules.read', SELF_AUDIENCE, ['chat.preferences', 'chat.tts_defaults', 'live.profile']],['live', 'network.modules.write', SELF_AUDIENCE, ['chat.tts_defaults', 'live.profile']],['chat', 'network.modules.read', SELF_AUDIENCE, ['chat.preferences']],@@@ -681,7 +681,9 @@ function serveShared(req, res, next) {app.use('/shared/v1', serveShared);app.use('/shared', serveShared);--app.get('/release.json', release.handler);++// GET /release.json, and POST /release-metrics: open tabs' update outcomes into /metrics++// (release_client_updates_total, openvibe-shared 1.5.0).++release.mount(app, { registry: observability.registry });// Avatar servingconst avatarDir = path.resolve(config.avatars.path);@@@ -6,10 +6,10 @@* service manifests (eventsProduced / eventsConsumed) and the event payload contracts* (contracts/events/payloads/<type>.v<N>.json), plus two things Network itself knows:*-- * OBSERVED types a service publishes today that its manifest (openvibe-contracts 0.30.1) does not-- * list yet. Live and Media declare no eventsProduced there, although both relay these-- * through their outboxes to OpenVibe.Events. Each row names the code that emits it, so-- * the row can be deleted once the manifest lists the type.++ * OBSERVED types a service publishes today that its manifest does not list yet, as++ * `{ [service]: { produced: [types], source: 'the code that emits them' } }`. A row is++ * deleted once the manifest lists the type. Empty since openvibe-contracts 0.32.0: the++ * Live, Media and Network manifests list every type they publish.* Network's own consumer the topics server/notifications/events-consumer.js subscribes to (its* TOPICS), so the registry cannot disagree with what Network actually consumes.*@@@ -18,20 +18,7 @@*/const contracts = require('openvibe-contracts');--const OBSERVED = {-- live: {-- produced: ['live.stream.started', 'live.stream.ended'],-- source: 'OpenVibe.Live server/events/stream-events.js (event_outbox, visibility public, priority important)',-- },-- media: {-- produced: ['media.vod.ready', 'media.vod.failed', 'media.clip.ready', 'media.clip.failed', 'media.object.uploaded', 'media.storage.alert', 'media.storage.recovered'],-- source: 'OpenVibe.Media server/events.js TYPES (event_outbox, visibility internal)',-- },-- network: {-- produced: ['network.module.updated'],-- source: 'OpenVibe.Network server/identity/module-events.js (network_event_outbox, visibility internal)',-- },--};++const OBSERVED = {};const SEGMENT = /^[a-z0-9_]+$/;function patternRe(p) {@@@ -2,7 +2,7 @@// User modules, Wave 1 item 24 (server/identity/modules.js, module-events.js):// - every change emits network.module.updated in the same transaction (network_event_outbox), with the// owner, namespace, changed keys and revision, never a private value; revisions survive deletes--// - chat.preferences is owned by Chat (handoff over the 0.30.x manifest); Live's old write grant narrows++// - chat.preferences is owned by Chat (the contracts' owner since 0.32.0); Live's old write grant narrows// - owner-service DELETE; account removal and merge (onSubjectRemoved/onSubjectMerged) with events, and// triggers that refuse deleting or re-keying an account while module rows remain; guest -> account link// - onOwnerRemoved: a retired owner makes its namespace read-only; delete-after-retention sweeps@@@ -80,8 +80,7 @@ const valid = (e) => { const v = contracts.validate('events.event-envelope@1', econst chat = { authorization: `Bearer ${chatTok.access_token}` };const live = { authorization: `Bearer ${(await svc('live')).access_token}` };-- // ── Handoff: chat.preferences belongs to Chat while the manifest still says live ──-- assert.strictEqual(contracts.modules.get('chat.preferences').owner, 'live', 'Contracts moved the owner: delete the OWNER_HANDOFFS entry and this line');++ // ── Ownership: chat.preferences belongs to Chat ──assert.strictEqual(modulesLib.ownerOf('chat.preferences'), 'chat');assert.strictEqual(modulesLib.ownerOf('chat.tts_defaults'), 'live');let r = await call('PUT', `/internal/modules/chat.preferences/${ANN}`, { headers: live, body: { data: { timestamps: true } } });@@@ -44,6 +44,9 @@ const ready = observability.createNetworkReadiness({discordService: { isReady: () => discordReady, _getSetting: () => 'configured-token' },});app.get('/api/ready', ready.handler);++// The release manifest as server/index.js mounts it: GET /release.json, POST /release-metrics into /metrics.++const release = require('openvibe-shared/release').createRelease({ service: 'network', root: path.join(__dirname, '..') });++release.mount(app, { registry: observability.registry });const server = http.createServer(app);(async () => {@@@ -72,8 +75,20 @@ const server = http.createServer(app);r = await fetch(`${base}/internal/coins/credit`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: 'Bearer not-a-token' }, body: '{}' });assert.strictEqual(r.status, 401);++ // ── Release manifest (ADR-016) and open tabs' update reports ──++ const rel = await get('/release.json');++ assert.strictEqual(rel.status, 200);++ const manifest = JSON.parse(rel.text);++ assert.deepStrictEqual(require('openvibe-contracts').validate('registry.release-manifest@1', manifest).errors, []);++ assert.strictEqual(release.validate().valid, true);++ assert.strictEqual(manifest.metrics_url, '/release-metrics');++ r = await fetch(`${base}/release-metrics`, { method: 'POST', headers: { 'content-type': 'text/plain' }, body: JSON.stringify({ counts: { applied: { style: 2 }, reloaded: { nonsense: 1 } } }) });++ assert.strictEqual(r.status, 204);++const m = await get('/metrics');assert.strictEqual(m.status, 200);++ assert.ok(m.text.includes('release_client_updates_total{outcome="applied",reason="style"} 2\n'), m.text);++ assert.ok(m.text.includes('release_client_updates_total{outcome="reloaded",reason="other"} 1\n'), 'unknown reasons never become labels');assert.ok(m.text.includes('network_tokens_issued_total{grant_type="client_credentials"} 2\n'), m.text);assert.ok(m.text.includes('network_token_failures_total{grant_type="client_credentials",error="invalid_client"} 1\n'));assert.ok(/network_token_failures_total\{grant_type="refresh_token",error="[a-z_]+"\} 1\n/.test(m.text));@@@ -35,14 +35,17 @@ const rel = (contractsVersion, packages) => [200, { service: 'x', release: 'abcdassert.strictEqual(driftOf('0.28.0', 'v0.30.1'), 'behind');assert.strictEqual(driftOf('0.30.1', 'v0.30.1'), 'current');-- // 2. topics: every produced type, payload contract and consumer; Live and Media from Network's observation.++ // 2. topics: every produced type, payload contract and consumer; Live and Media from their manifests.const built = buildTopics();const by = Object.fromEntries(built.topics.map(t => [t.topic, t]));const produced = new Set(contracts.services.manifests.flatMap(m => m.eventsProduced || []));for (const t of produced) assert.ok(by[t], `${t} is listed`);for (const c of contracts.catalog.filter(c => c.schema.startsWith('events/payloads/'))) assert.strictEqual(by[c.id].payload_contract.id, `${c.id}@1`);-- for (const id of ['live', 'media']) for (const t of OBSERVED[id].produced) {-- assert.deepStrictEqual(by[t].producers, [{ service: id, declared: (contracts.services.get(id).eventsProduced || []).includes(t) ? 'manifest' : 'observed' }]);++ for (const id of ['live', 'media']) for (const t of contracts.services.get(id).eventsProduced) {++ assert.deepStrictEqual(by[t].producers, [{ service: id, declared: 'manifest' }]);++ }++ for (const [id, o] of Object.entries(OBSERVED)) for (const t of o.produced) {++ assert.ok(!(contracts.services.get(id).eventsProduced || []).includes(t), `${id}'s manifest lists ${t}: delete it from OBSERVED`);}for (const t of TOPICS) assert.ok(by[t].consumers.some(c => c.service === 'network'), `Network consumes ${t}`);assert.ok(by['wiki.index_document.upserted'].consumers.some(c => c.service === 'search' && c.pattern === '*.index_document.upserted'), 'a consumer pattern is listed on each topic it matches');@@@ -65,7 +68,7 @@ const rel = (contractsVersion, packages) => [200, { service: 'x', release: 'abcdlet r = await get('/api/v1/registry/topics');assert.ok(r.body.topics.length >= produced.size, 'no longer empty');r = await get('/api/v1/registry/topics?producer=live');-- assert.deepStrictEqual(r.body.topics.map(t => t.topic).sort(), ['live.stream.ended', 'live.stream.started']);++ assert.deepStrictEqual(r.body.topics.map(t => t.topic).sort(), ['live.release.deployed', 'live.stream.ended', 'live.stream.started']);r = await get('/api/v1/registry/topics?consumer=network');assert.deepStrictEqual(r.body.topics.map(t => t.topic).sort(), [...TOPICS].sort());r = await get('/api/v1/registry/topics?prefix=media.vod.');
@@ -350,9 +350,14 @@ Google-style account management supporting up to 5 accounts:
@@ -350,9 +350,14 @@ Google-style account management supporting up to 5 accounts:
Otherwise it is 200, with `status: "degraded"` when an optional check has failed.
Otherwise it is 200, with `status: "degraded"` when an optional check has failed.
- `GET /metrics`: Prometheus text for direct loopback callers only. Any proxied request gets a 404,
- `GET /metrics`: Prometheus text for direct loopback callers only. Any proxied request gets a 404,
and `deploy/nginx` blocks the path too. Besides the HTTP golden signals by route template, it
and `deploy/nginx` blocks the path too. Besides the HTTP golden signals by route template, it
- exports process metrics and `release_info`. Network's own counters are
+ exports process metrics, `release_info` and `release_client_updates_total{outcome,reason}` (what
+ open tabs report to `POST /release-metrics`). Network's own counters are
`network_tokens_issued_total{grant_type}`, `network_token_failures_total{grant_type,error}` and
`network_tokens_issued_total{grant_type}`, `network_token_failures_total{grant_type,error}` and
`network_principal_token_failures_total{code}`.
`network_principal_token_failures_total{code}`.
+- `GET /release.json` is Network's release manifest (ADR-016, `registry.release-manifest@1`, from
+ `openvibe-shared/release`'s `release.mount`): the deployed commit, the library versions and, since
+ openvibe-contracts 0.32.0, the 1.1.0 fields. No components are declared, so every release still
+ prompts open tabs to reload.
- `GET /status` is the operator page: server-rendered, no JavaScript needed, `noindex`.
- `GET /status` is the operator page: server-rendered, no JavaScript needed, `noindex`.
`GET /api/v1/status` returns the same data as JSON. Each OpenVibe service shows as up, degraded,
`GET /api/v1/status` returns the same data as JSON. Each OpenVibe service shows as up, degraded,
down, not running (placeholder or no runtime) or unknown, with its release, boot time and
down, not running (placeholder or no runtime) or unknown, with its release, boot time and
@@ -371,9 +376,10 @@ Google-style account management supporting up to 5 accounts:
@@ -371,9 +376,10 @@ Google-style account management supporting up to 5 accounts:
consumer pattern and the payload contract; filters `service`, `producer`, `consumer`, `prefix`),
consumer pattern and the payload contract; filters `service`, `producer`, `consumer`, `prefix`),
`/topics/:topic`, `/releases` (each running service's `/release.json` from the same loopback poll,
`/topics/:topic`, `/releases` (each running service's `/release.json` from the same loopback poll,
with its installed openvibe-contracts/sdk/shared versions and drift against the libraries' current
with its installed openvibe-contracts/sdk/shared versions and drift against the libraries' current
- releases), `/health` and `/search?q=`. Live and Media list no `eventsProduced` in openvibe-contracts
- 0.30.1; the registry adds what their code publishes (`server/registry/topics.js` OBSERVED, marked
- `observed` on the service), and Network's consumed topics come from its own Events consumer.
+ releases), `/health` and `/search?q=`. A type a service's code publishes before its manifest lists
+ it is added from `server/registry/topics.js` OBSERVED (marked `observed` on the service; empty since
+ openvibe-contracts 0.32.0 lists Live's, Media's and Network's), and Network's consumed topics come
+ from its own Events consumer.
Payload schemas are served at their `$id` (`/contracts/events/payloads/<type>.v1.json`).
Payload schemas are served at their `$id` (`/contracts/events/payloads/<type>.v1.json`).
- `node scripts/contracts-drift.js` warns about services that pin an older openvibe-contracts than
- `node scripts/contracts-drift.js` warns about services that pin an older openvibe-contracts than
the latest tag, or a tag that was never published (`--dir ~/OpenVibers` for local checkouts,
the latest tag, or a tag that was never published (`--dir ~/OpenVibers` for local checkouts,
@@ -20,9 +20,9 @@
@@ -20,9 +20,9 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.1",
- "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-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.5.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.0",
"uuid": "^11.1.0",
"uuid": "^11.1.0",
"web-push": "^3.6.7"
"web-push": "^3.6.7"
}
}
@@ -1465,9 +1465,9 @@
@@ -1465,9 +1465,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.30.1",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.1",
- "integrity": "sha512-JAOna8MA/+4iABEZ5qnzGjW27z9UgAHDF1Pj4VE+R3tb1UVyRUVZX06yuE+BggTdhZPGZRq5mFFIyPNHp8N3GA==",
+ "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",
@@ -1481,18 +1481,18 @@
@@ -1481,18 +1481,18 @@
}
}
},
},
"node_modules/openvibe-sdk": {
"node_modules/openvibe-sdk": {
- "version": "0.4.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
- "integrity": "sha512-Hyz55YOMIs0xwCQ1NXsN0sUYhlbKXAX3uWY2fPCdEEL18FXUYVhK8wlG/5JMMypFHnjDiSd9VHzBNG12hWsmkQ==",
+ "version": "0.5.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
+ "integrity": "sha512-QVJkR5iazQX0ZwIrDp2A88ZeYYBSXEmSzhpgURPUSnzezIM3jz0HBhVKowyhkvg2aBO61XNf77eb4zJGkMjZiw==",
"license": "MIT",
"license": "MIT",
"engines": {
"engines": {
"node": ">=20"
"node": ">=20"
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "1.4.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",
+ "version": "1.5.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.0",
+ "integrity": "sha512-cehnscuYMkJwuCYGcI6wPSswbUn0bhTyGAaD5feNCoO//2e9gyC7cgjkEcIOtM8DQF3yTuUNyXnOwckIeM4fUg==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -23,9 +23,9 @@
@@ -23,9 +23,9 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.1",
- "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-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.5.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.0",
"uuid": "^11.1.0",
"uuid": "^11.1.0",
"web-push": "^3.6.7"
"web-push": "^3.6.7"
}
}
@@ -37,14 +37,12 @@ const moduleEvents = require('./module-events');
@@ -37,14 +37,12 @@ const moduleEvents = require('./module-events');
const SUBJECT_RE = /^(usr|gst)_[0-9A-HJKMNP-TV-Z]{26}$/;
const SUBJECT_RE = /^(usr|gst)_[0-9A-HJKMNP-TV-Z]{26}$/;
/**
/**
- * Namespaces whose owner moved before openvibe-contracts caught up. The manifest of chat.preferences
- * (0.30.x) still names live and says it "moves to OpenVibe.Chat in Wave 6"; Chat has served Live's chat
- * since the Wave 6 cutover (2026-09-23). The handoff applies only while the installed manifest still
- * names `from`: once Contracts publishes owner `to` it is a no-op, and the entry can be deleted.
+ * Namespaces whose owner moved before openvibe-contracts caught up: `{ [namespace]: { from, to } }`.
+ * A handoff applies only while the installed manifest still names `from`: once Contracts publishes
+ * owner `to` it is a no-op, and the entry can be deleted. None is pending: chat.preferences, the last
+ * one (live → chat, the Wave 6 cutover), is owned by chat in the contracts since openvibe-contracts 0.32.0.
*/
*/
-const OWNER_HANDOFFS = Object.freeze({
- 'chat.preferences': Object.freeze({ from: 'live', to: 'chat' }),
-});
+const OWNER_HANDOFFS = Object.freeze({});
/** The service that owns a namespace now (the manifest's owner unless a handoff moved it). */
/** The service that owns a namespace now (the manifest's owner unless a handoff moved it). */
function ownerOf(namespace) {
function ownerOf(namespace) {
@@ -22,7 +22,7 @@ const DEFAULT_GRANTS = [
@@ -22,7 +22,7 @@ const DEFAULT_GRANTS = [
['live', 'network.coins.debit', SELF_AUDIENCE, ['live']],
['live', 'network.coins.debit', SELF_AUDIENCE, ['live']],
['live', 'network.notifications.push', SELF_AUDIENCE, ['live']],
['live', 'network.notifications.push', SELF_AUDIENCE, ['live']],
// User modules: each service reads and writes the namespaces it owns (openvibe-contracts manifests/namespaces).
// User modules: each service reads and writes the namespaces it owns (openvibe-contracts manifests/namespaces).
- // chat.preferences moved to Chat with the Wave 6 cutover (modules.js OWNER_HANDOFFS); Live keeps reading it.
+ // chat.preferences moved to Chat with the Wave 6 cutover (its owner in the contracts); Live keeps reading it.
['live', 'network.modules.read', SELF_AUDIENCE, ['chat.preferences', 'chat.tts_defaults', 'live.profile']],
['live', 'network.modules.read', SELF_AUDIENCE, ['chat.preferences', 'chat.tts_defaults', 'live.profile']],
['live', 'network.modules.write', SELF_AUDIENCE, ['chat.tts_defaults', 'live.profile']],
['live', 'network.modules.write', SELF_AUDIENCE, ['chat.tts_defaults', 'live.profile']],
['chat', 'network.modules.read', SELF_AUDIENCE, ['chat.preferences']],
['chat', 'network.modules.read', SELF_AUDIENCE, ['chat.preferences']],
@@ -681,7 +681,9 @@ function serveShared(req, res, next) {
@@ -681,7 +681,9 @@ function serveShared(req, res, next) {
app.use('/shared/v1', serveShared);
app.use('/shared/v1', serveShared);
app.use('/shared', serveShared);
app.use('/shared', serveShared);
-app.get('/release.json', release.handler);
+// GET /release.json, and POST /release-metrics: open tabs' update outcomes into /metrics
+// (release_client_updates_total, openvibe-shared 1.5.0).
+release.mount(app, { registry: observability.registry });
// Avatar serving
// Avatar serving
const avatarDir = path.resolve(config.avatars.path);
const avatarDir = path.resolve(config.avatars.path);
@@ -6,10 +6,10 @@
@@ -6,10 +6,10 @@
* service manifests (eventsProduced / eventsConsumed) and the event payload contracts
* service manifests (eventsProduced / eventsConsumed) and the event payload contracts
* (contracts/events/payloads/<type>.v<N>.json), plus two things Network itself knows:
* (contracts/events/payloads/<type>.v<N>.json), plus two things Network itself knows:
*
*
- * OBSERVED types a service publishes today that its manifest (openvibe-contracts 0.30.1) does not
- * list yet. Live and Media declare no eventsProduced there, although both relay these
- * through their outboxes to OpenVibe.Events. Each row names the code that emits it, so
- * the row can be deleted once the manifest lists the type.
+ * OBSERVED types a service publishes today that its manifest does not list yet, as
+ * `{ [service]: { produced: [types], source: 'the code that emits them' } }`. A row is
+ * deleted once the manifest lists the type. Empty since openvibe-contracts 0.32.0: the
+ * Live, Media and Network manifests list every type they publish.
* Network's own consumer the topics server/notifications/events-consumer.js subscribes to (its
* Network's own consumer the topics server/notifications/events-consumer.js subscribes to (its
* TOPICS), so the registry cannot disagree with what Network actually consumes.
* TOPICS), so the registry cannot disagree with what Network actually consumes.
*
*
@@ -18,20 +18,7 @@
@@ -18,20 +18,7 @@
*/
*/
const contracts = require('openvibe-contracts');
const contracts = require('openvibe-contracts');
-const OBSERVED = {
- live: {
- produced: ['live.stream.started', 'live.stream.ended'],
- source: 'OpenVibe.Live server/events/stream-events.js (event_outbox, visibility public, priority important)',
- },
- media: {
- produced: ['media.vod.ready', 'media.vod.failed', 'media.clip.ready', 'media.clip.failed', 'media.object.uploaded', 'media.storage.alert', 'media.storage.recovered'],
- source: 'OpenVibe.Media server/events.js TYPES (event_outbox, visibility internal)',
- },
- network: {
- produced: ['network.module.updated'],
- source: 'OpenVibe.Network server/identity/module-events.js (network_event_outbox, visibility internal)',
- },
-};
+const OBSERVED = {};
const SEGMENT = /^[a-z0-9_]+$/;
const SEGMENT = /^[a-z0-9_]+$/;
function patternRe(p) {
function patternRe(p) {
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
// User modules, Wave 1 item 24 (server/identity/modules.js, module-events.js):
// User modules, Wave 1 item 24 (server/identity/modules.js, module-events.js):
// - every change emits network.module.updated in the same transaction (network_event_outbox), with the
// - every change emits network.module.updated in the same transaction (network_event_outbox), with the
// owner, namespace, changed keys and revision, never a private value; revisions survive deletes
// owner, namespace, changed keys and revision, never a private value; revisions survive deletes
-// - chat.preferences is owned by Chat (handoff over the 0.30.x manifest); Live's old write grant narrows
+// - chat.preferences is owned by Chat (the contracts' owner since 0.32.0); Live's old write grant narrows
// - owner-service DELETE; account removal and merge (onSubjectRemoved/onSubjectMerged) with events, and
// - owner-service DELETE; account removal and merge (onSubjectRemoved/onSubjectMerged) with events, and
// triggers that refuse deleting or re-keying an account while module rows remain; guest -> account link
// triggers that refuse deleting or re-keying an account while module rows remain; guest -> account link
// - onOwnerRemoved: a retired owner makes its namespace read-only; delete-after-retention sweeps
// - onOwnerRemoved: a retired owner makes its namespace read-only; delete-after-retention sweeps
@@ -80,8 +80,7 @@ const valid = (e) => { const v = contracts.validate('events.event-envelope@1', e
@@ -80,8 +80,7 @@ const valid = (e) => { const v = contracts.validate('events.event-envelope@1', e
const chat = { authorization: `Bearer ${chatTok.access_token}` };
const chat = { authorization: `Bearer ${chatTok.access_token}` };
const live = { authorization: `Bearer ${(await svc('live')).access_token}` };
const live = { authorization: `Bearer ${(await svc('live')).access_token}` };
- // ── Handoff: chat.preferences belongs to Chat while the manifest still says live ──
- assert.strictEqual(contracts.modules.get('chat.preferences').owner, 'live', 'Contracts moved the owner: delete the OWNER_HANDOFFS entry and this line');
+ // ── Ownership: chat.preferences belongs to Chat ──
assert.strictEqual(modulesLib.ownerOf('chat.preferences'), 'chat');
assert.strictEqual(modulesLib.ownerOf('chat.preferences'), 'chat');
assert.strictEqual(modulesLib.ownerOf('chat.tts_defaults'), 'live');
assert.strictEqual(modulesLib.ownerOf('chat.tts_defaults'), 'live');
let r = await call('PUT', `/internal/modules/chat.preferences/${ANN}`, { headers: live, body: { data: { timestamps: true } } });
let r = await call('PUT', `/internal/modules/chat.preferences/${ANN}`, { headers: live, body: { data: { timestamps: true } } });
@@ -44,6 +44,9 @@ const ready = observability.createNetworkReadiness({
@@ -44,6 +44,9 @@ const ready = observability.createNetworkReadiness({
discordService: { isReady: () => discordReady, _getSetting: () => 'configured-token' },
discordService: { isReady: () => discordReady, _getSetting: () => 'configured-token' },
});
});
app.get('/api/ready', ready.handler);
app.get('/api/ready', ready.handler);
+// The release manifest as server/index.js mounts it: GET /release.json, POST /release-metrics into /metrics.
+const release = require('openvibe-shared/release').createRelease({ service: 'network', root: path.join(__dirname, '..') });
+release.mount(app, { registry: observability.registry });
const server = http.createServer(app);
const server = http.createServer(app);
(async () => {
(async () => {
@@ -72,8 +75,20 @@ const server = http.createServer(app);
@@ -72,8 +75,20 @@ const server = http.createServer(app);
r = await fetch(`${base}/internal/coins/credit`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: 'Bearer not-a-token' }, body: '{}' });
r = await fetch(`${base}/internal/coins/credit`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: 'Bearer not-a-token' }, body: '{}' });
assert.strictEqual(r.status, 401);
assert.strictEqual(r.status, 401);
+ // ── Release manifest (ADR-016) and open tabs' update reports ──
+ const rel = await get('/release.json');
+ assert.strictEqual(rel.status, 200);
+ const manifest = JSON.parse(rel.text);
+ assert.deepStrictEqual(require('openvibe-contracts').validate('registry.release-manifest@1', manifest).errors, []);
+ assert.strictEqual(release.validate().valid, true);
+ assert.strictEqual(manifest.metrics_url, '/release-metrics');
+ r = await fetch(`${base}/release-metrics`, { method: 'POST', headers: { 'content-type': 'text/plain' }, body: JSON.stringify({ counts: { applied: { style: 2 }, reloaded: { nonsense: 1 } } }) });
+ assert.strictEqual(r.status, 204);
+
const m = await get('/metrics');
const m = await get('/metrics');
assert.strictEqual(m.status, 200);
assert.strictEqual(m.status, 200);
+ assert.ok(m.text.includes('release_client_updates_total{outcome="applied",reason="style"} 2\n'), m.text);
+ assert.ok(m.text.includes('release_client_updates_total{outcome="reloaded",reason="other"} 1\n'), 'unknown reasons never become labels');
assert.ok(m.text.includes('network_tokens_issued_total{grant_type="client_credentials"} 2\n'), m.text);
assert.ok(m.text.includes('network_tokens_issued_total{grant_type="client_credentials"} 2\n'), m.text);
assert.ok(m.text.includes('network_token_failures_total{grant_type="client_credentials",error="invalid_client"} 1\n'));
assert.ok(m.text.includes('network_token_failures_total{grant_type="client_credentials",error="invalid_client"} 1\n'));
assert.ok(/network_token_failures_total\{grant_type="refresh_token",error="[a-z_]+"\} 1\n/.test(m.text));
assert.ok(/network_token_failures_total\{grant_type="refresh_token",error="[a-z_]+"\} 1\n/.test(m.text));
@@ -35,14 +35,17 @@ const rel = (contractsVersion, packages) => [200, { service: 'x', release: 'abcd
@@ -35,14 +35,17 @@ const rel = (contractsVersion, packages) => [200, { service: 'x', release: 'abcd
assert.strictEqual(driftOf('0.28.0', 'v0.30.1'), 'behind');
assert.strictEqual(driftOf('0.28.0', 'v0.30.1'), 'behind');
assert.strictEqual(driftOf('0.30.1', 'v0.30.1'), 'current');
assert.strictEqual(driftOf('0.30.1', 'v0.30.1'), 'current');
- // 2. topics: every produced type, payload contract and consumer; Live and Media from Network's observation.
+ // 2. topics: every produced type, payload contract and consumer; Live and Media from their manifests.
const built = buildTopics();
const built = buildTopics();
const by = Object.fromEntries(built.topics.map(t => [t.topic, t]));
const by = Object.fromEntries(built.topics.map(t => [t.topic, t]));
const produced = new Set(contracts.services.manifests.flatMap(m => m.eventsProduced || []));
const produced = new Set(contracts.services.manifests.flatMap(m => m.eventsProduced || []));
for (const t of produced) assert.ok(by[t], `${t} is listed`);
for (const t of produced) assert.ok(by[t], `${t} is listed`);
for (const c of contracts.catalog.filter(c => c.schema.startsWith('events/payloads/'))) assert.strictEqual(by[c.id].payload_contract.id, `${c.id}@1`);
for (const c of contracts.catalog.filter(c => c.schema.startsWith('events/payloads/'))) assert.strictEqual(by[c.id].payload_contract.id, `${c.id}@1`);
- for (const id of ['live', 'media']) for (const t of OBSERVED[id].produced) {
- assert.deepStrictEqual(by[t].producers, [{ service: id, declared: (contracts.services.get(id).eventsProduced || []).includes(t) ? 'manifest' : 'observed' }]);
+ for (const id of ['live', 'media']) for (const t of contracts.services.get(id).eventsProduced) {
+ assert.deepStrictEqual(by[t].producers, [{ service: id, declared: 'manifest' }]);
+ }
+ for (const [id, o] of Object.entries(OBSERVED)) for (const t of o.produced) {
+ assert.ok(!(contracts.services.get(id).eventsProduced || []).includes(t), `${id}'s manifest lists ${t}: delete it from OBSERVED`);
}
}
for (const t of TOPICS) assert.ok(by[t].consumers.some(c => c.service === 'network'), `Network consumes ${t}`);
for (const t of TOPICS) assert.ok(by[t].consumers.some(c => c.service === 'network'), `Network consumes ${t}`);
assert.ok(by['wiki.index_document.upserted'].consumers.some(c => c.service === 'search' && c.pattern === '*.index_document.upserted'), 'a consumer pattern is listed on each topic it matches');
assert.ok(by['wiki.index_document.upserted'].consumers.some(c => c.service === 'search' && c.pattern === '*.index_document.upserted'), 'a consumer pattern is listed on each topic it matches');
@@ -65,7 +68,7 @@ const rel = (contractsVersion, packages) => [200, { service: 'x', release: 'abcd
@@ -65,7 +68,7 @@ const rel = (contractsVersion, packages) => [200, { service: 'x', release: 'abcd
let r = await get('/api/v1/registry/topics');
let r = await get('/api/v1/registry/topics');
assert.ok(r.body.topics.length >= produced.size, 'no longer empty');
assert.ok(r.body.topics.length >= produced.size, 'no longer empty');
r = await get('/api/v1/registry/topics?producer=live');
r = await get('/api/v1/registry/topics?producer=live');
- assert.deepStrictEqual(r.body.topics.map(t => t.topic).sort(), ['live.stream.ended', 'live.stream.started']);
+ assert.deepStrictEqual(r.body.topics.map(t => t.topic).sort(), ['live.release.deployed', 'live.stream.ended', 'live.stream.started']);
r = await get('/api/v1/registry/topics?consumer=network');
r = await get('/api/v1/registry/topics?consumer=network');
assert.deepStrictEqual(r.body.topics.map(t => t.topic).sort(), [...TOPICS].sort());
assert.deepStrictEqual(r.body.topics.map(t => t.topic).sort(), [...TOPICS].sort());
r = await get('/api/v1/registry/topics?prefix=media.vod.');
r = await get('/api/v1/registry/topics?prefix=media.vod.');