openvibers/openvibe.network Commits

The registry reports each released library's latest published tag (read from GitHub at boot and hourly; the installed version until then, and whenever a lookup fails) instead of the version Network happens to install — it said openvibe-sdk v0.5.0 while v0.8.0 was out

OP OpenVibers committed 24 Sept 2026 · eb9a161727 · 5 files, +95 −2


Changes

ddiff --git a/package.json b/package.json
iindex e7d48164..7c840a34 100644
---- a/package.json
++++ b/package.json
@@@ -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/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-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",
++ "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/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-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",
"reset-db": "node server/reset-db.js",
"grant-admin": "node server/grant-admin.js"
},
ddiff --git a/server/index.js b/server/index.js
iindex a2eec6e9..be9a3a36 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -393,6 +393,8 @@ if (ecosystemInternal.ignored.length) console.warn(`[Registry] ignored (not a lo
const ecosystem = require('./registry/ecosystem').createEcosystemRegistry({ issuer: config.jwt.issuer, internalOverrides: ecosystemInternal.overrides });
app.use(ecosystem.router());
ecosystem.start();
++// The released libraries' latest published tags (not the versions Network installs) for the registry.
++require('./registry/library-tags').createLibraryTags({ onUpdate: require('./registry/exposure').setLibraryReleases }).start();
// Operator status: GET /status (server-rendered, noindex), /api/v1/status, /api/v1/status/slo.
app.use(require('./status/routes').createStatusRoutes({ ecosystem }));
// What shipped network-wide: the changelog proxy every site's widget reads, and /updates.
ddiff --git a/server/registry/exposure.js b/server/registry/exposure.js
iindex 49795921..90267189 100644
---- a/server/registry/exposure.js
++++ b/server/registry/exposure.js
@@@ -107,9 +107,22 @@ function publicOriginOf(m) {
return e && e.state === 'live' && e.public_site === 'service' ? m.publicOrigin || null : null;
}
++/**
++ * The latest published tags (server/registry/library-tags.js) replace the installed versions once
++ * known: { 'openvibe-sdk': 'v0.8.0', … }. Unknown packages keep the installed version.
++ */
++function setLibraryReleases(tags) {
++ for (const e of Object.values(EXPOSURE)) {
++ if (e.state !== 'library' || !e.package || !tags || !tags[e.package]) continue;
++ const repo = e.distribution ? e.distribution.split('/')[4] : null;
++ e.release = tags[e.package];
++ if (repo) e.distribution = `https://codeload.github.com/OpenVibers/${repo}/tar.gz/refs/tags/${e.release}`;
++ }
++}
++
/** The released libraries (sdk, shared, contracts): { id, package, release, distribution }. */
function libraries() {
return Object.entries(EXPOSURE).filter(([, e]) => e.state === 'library').map(([id, e]) => ({ id, package: e.package, release: e.release, distribution: e.distribution || null }));
}
--module.exports = { STATES, LABEL, EXPOSURE, exposureOf, publicOriginOf, libraries };
++module.exports = { STATES, LABEL, EXPOSURE, exposureOf, publicOriginOf, libraries, setLibraryReleases };
ddiff --git a/server/registry/library-tags.js b/server/registry/library-tags.js
nnew file mode 100644
iindex 00000000..dadd267f
---- /dev/null
++++ b/server/registry/library-tags.js
@@@ -0,0 +1,53 @@
++'use strict';
++/**
++ * The released libraries' latest published tags (vX.Y.Z on GitHub), so the registry reports what a
++ * consumer can pin today rather than the version Network happens to have installed (it said
++ * openvibe-sdk v0.5.0 while v0.8.0 was out). Read at boot and hourly; a failure keeps the last
++ * answer, and until there is one the installed version stands. GITHUB_TOKEN raises the rate limit.
++ */
++const REPOS = { 'openvibe-sdk': 'OpenVibe.SDK', 'openvibe-shared': 'OpenVibe.Shared', 'openvibe-contracts': 'OpenVibe.Contracts', 'openvibe-publishing': 'OpenVibe.Publishing' };
++const SEMVER = /^v(\d+)\.(\d+)\.(\d+)$/;
++
++/** The highest vX.Y.Z among tag names, or null. */
++function latestOf(names) {
++ let best = null;
++ for (const n of names || []) {
++ const m = SEMVER.exec(String(n));
++ if (!m) continue;
++ const v = m.slice(1).map(Number);
++ if (!best || v[0] > best.v[0] || (v[0] === best.v[0] && (v[1] > best.v[1] || (v[1] === best.v[1] && v[2] > best.v[2])))) best = { name: n, v };
++ }
++ return best ? best.name : null;
++}
++
++function createLibraryTags({ fetchImpl = globalThis.fetch, token = process.env.GITHUB_TOKEN || '', intervalMs = 3600e3, onUpdate = null, log = console } = {}) {
++ const latest = new Map(); // package -> 'vX.Y.Z'
++ let timer = null;
++ async function refresh() {
++ for (const [pkg, repo] of Object.entries(REPOS)) {
++ try {
++ const res = await fetchImpl(`https://api.github.com/repos/OpenVibers/${repo}/tags?per_page=100`, {
++ headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'OpenVibe.Network registry', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
++ signal: AbortSignal.timeout(10000),
++ });
++ if (!res.ok) continue;
++ const tag = latestOf((await res.json()).map((t) => t && t.name));
++ if (tag) latest.set(pkg, tag);
++ } catch (err) { if (log && log.warn) log.warn(`[registry] ${repo} tags: ${err.message}`); }
++ }
++ const out = Object.fromEntries(latest);
++ if (onUpdate && latest.size) onUpdate(out);
++ return out;
++ }
++ function start() {
++ if (timer) return;
++ const first = setTimeout(() => { refresh().catch(() => {}); }, 5000);
++ if (first.unref) first.unref();
++ timer = setInterval(() => { refresh().catch(() => {}); }, intervalMs);
++ if (timer.unref) timer.unref();
++ }
++ function stop() { if (timer) clearInterval(timer); timer = null; }
++ return { refresh, start, stop, get: (pkg) => latest.get(pkg) || null };
++}
++
++module.exports = { createLibraryTags, latestOf, REPOS };
ddiff --git a/test/library-tags.test.js b/test/library-tags.test.js
nnew file mode 100644
iindex 00000000..9481beda
---- /dev/null
++++ b/test/library-tags.test.js
@@@ -0,0 +1,25 @@
++'use strict';
++// The registry reports each library's latest published tag, not the version Network has installed.
++const assert = require('assert');
++const { createLibraryTags, latestOf } = require('../server/registry/library-tags');
++const exposure = require('../server/registry/exposure');
++
++assert.strictEqual(latestOf(['v0.5.0', 'v0.10.0', 'v0.9.3', 'latest', 'v0.10.0-rc1']), 'v0.10.0');
++assert.strictEqual(latestOf([]), null);
++
++(async () => {
++ const tags = { 'OpenVibe.SDK': ['v0.7.0', 'v0.8.0'], 'OpenVibe.Shared': ['v1.11.1', 'v1.9.0'], 'OpenVibe.Contracts': null, 'OpenVibe.Publishing': ['v0.3.2'] };
++ const fetchImpl = async (url) => {
++ const repo = /repos\/OpenVibers\/([^/]+)\/tags/.exec(url)[1];
++ if (!tags[repo]) return { ok: false, status: 403, json: async () => ({}) };
++ return { ok: true, json: async () => tags[repo].map((name) => ({ name })) };
++ };
++ const lt = createLibraryTags({ fetchImpl, onUpdate: exposure.setLibraryReleases, log: null });
++ await lt.refresh();
++ const libs = Object.fromEntries(exposure.libraries().map((l) => [l.package, l]));
++ assert.strictEqual(libs['openvibe-sdk'].release, 'v0.8.0');
++ assert.strictEqual(libs['openvibe-sdk'].distribution, 'https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.8.0');
++ assert.strictEqual(libs['openvibe-shared'].release, 'v1.11.1');
++ assert.strictEqual(libs['openvibe-contracts'].release, `v${require('openvibe-contracts/package.json').version}`, 'a refused lookup keeps the installed version');
++ console.log('library tags: all checks passed');
++})().catch((e) => { console.error(e); process.exit(1); });