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
Changes
@@@ -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"},@@@ -393,6 +393,8 @@ if (ecosystemInternal.ignored.length) console.warn(`[Registry] ignored (not a loconst 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.@@@ -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 };nnew file mode 100644@@@ -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 };nnew file mode 100644@@@ -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); });
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
"scripts": {
"scripts": {
"start": "node server/index.js",
"start": "node server/index.js",
"dev": "NODE_ENV=development 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",
"reset-db": "node server/reset-db.js",
"grant-admin": "node server/grant-admin.js"
"grant-admin": "node server/grant-admin.js"
},
},
@@ -393,6 +393,8 @@ if (ecosystemInternal.ignored.length) console.warn(`[Registry] ignored (not a lo
@@ -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 });
const ecosystem = require('./registry/ecosystem').createEcosystemRegistry({ issuer: config.jwt.issuer, internalOverrides: ecosystemInternal.overrides });
app.use(ecosystem.router());
app.use(ecosystem.router());
ecosystem.start();
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.
// Operator status: GET /status (server-rendered, noindex), /api/v1/status, /api/v1/status/slo.
app.use(require('./status/routes').createStatusRoutes({ ecosystem }));
app.use(require('./status/routes').createStatusRoutes({ ecosystem }));
// What shipped network-wide: the changelog proxy every site's widget reads, and /updates.
// What shipped network-wide: the changelog proxy every site's widget reads, and /updates.
@@ -107,9 +107,22 @@ function publicOriginOf(m) {
@@ -107,9 +107,22 @@ function publicOriginOf(m) {
return e && e.state === 'live' && e.public_site === 'service' ? m.publicOrigin || null : null;
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 }. */
/** The released libraries (sdk, shared, contracts): { id, package, release, distribution }. */
function libraries() {
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 }));
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 };
new file mode 100644
new file mode 100644
@@ -0,0 +1,53 @@
@@ -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 };
new file mode 100644
new file mode 100644
@@ -0,0 +1,25 @@
@@ -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); });