openvibers/openvibe.network Commits
Manifest-derived registry (WS-C task 1, Contracts 0.42.0): the loopback addresses, exposure states and the frame's site list come from the service manifests (internalOrigin, exposure, site); INTERNAL, READY_PATHS, EXPOSURE and RAW_SITES are no longer written here, OV_<ID>_INTERNAL_URL still overrides; test/registry-manifest-fixture.test.js: a new manifest appears in discovery, the registry, the status page and the site list with no code edit; npm test also runs staff-claims.test.js, which it had missed
Changes
@@@ -20,7 +20,7 @@"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.41.1",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.42.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.12.0","uuid": "^11.1.0",@@@ -1465,9 +1465,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.41.1",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.41.1",-- "integrity": "sha512-PdXYQK9Nt9LBv45ZR8hdIW6xt9zrSJAETDP5roRWQ3J3c7+TP6iTBZp/X1bjdxNDnNzxMPthkGsO7+E5vSXrbw==",++ "version": "0.42.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.42.0",++ "integrity": "sha512-lQUPg3bkpusZmFRyJHaARwnA4J+WcmQfVdM28yOHYhVChiMs/3rr3hl10JucKOlxxx9Cii1Go0iv+IfxqVO8UA==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -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/github-integration.test.js && node test/revocation.test.js && node test/usernames.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 && node test/moderation-audit.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/github-integration.test.js && node test/revocation.test.js && node test/usernames.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-manifest-fixture.test.js && node test/staff-claims.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 && node test/moderation-audit.test.js","reset-db": "node server/reset-db.js","grant-admin": "node server/grant-admin.js"},@@@ -23,7 +23,7 @@"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.41.1",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.42.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.12.0","uuid": "^11.1.0",@@@ -10,30 +10,13 @@ const { exposureOf } = require('../registry/exposure');// A site is 'open' (in the nav) only when its service's public domain serves the service itself, per// Network's exposure overlay (server/registry/exposure.js); every other site is 'soon' and says why// (state: internal = runs on loopback only, placeholder = planned). The two lists cannot disagree.--// host comes from the service's manifest (openvibe-contracts publicOrigin); an entry names its host only--// while its manifest has no public origin. TODO(plan §5.1/§5.3): name, icon, tagline, what and the--// legal profile are presentation with no manifest field yet; they move when the manifest schema has them.--const RAW_SITES = [-- { id: 'live', name: 'Live', icon: 'live', service: 'live', profile: 'streaming', tagline: 'Streams, clips and chat', what: 'Live streaming from a browser, OBS or a phone, with restreaming, clips, VODs, chat games and channel points.' },-- { id: 'tools', name: 'Tools', icon: 'tools', service: 'tools', profile: 'tools', tagline: 'Every online tool', what: 'More than 160 online tools: converters, downloaders, PDF and image tools, developer utilities and network diagnostics, each on its own address.' },-- { id: 'community', name: 'Community', icon: 'community', service: 'community', profile: 'ugc', tagline: 'Pastes and posts', what: 'Share text and code with a link, comment, and follow what people on OpenVibe are making.' },-- { id: 'games', name: 'Games', icon: 'games', service: 'games', profile: 'games', tagline: 'Browser games', what: 'Scraplandia, a shared pixel canvas and other games that run in a tab and know your OpenVibe account.' },-- { id: 'media', name: 'Media', icon: 'media', service: 'media', profile: 'hosting', tagline: 'VODs, clips, files', what: 'The public media library behind every site: recorded streams, clips, images and uploads.' },-- { id: 'network', name: 'Network', icon: 'network', service: 'network', profile: 'account', tagline: 'Account and themes', what: 'The account behind every site: one sign-in, themes, notifications, history and linked services.' },-- { id: 'chat', name: 'Chat', icon: 'chat', service: 'chat', profile: 'ugc', tagline: 'One chat for the network', what: 'Global chat, rooms, stream chat and direct messages under one account, with or without JavaScript.' },-- { id: 'codes', name: 'Codes', icon: 'codes', service: 'codes', profile: 'ugc', tagline: 'Build on OpenVibe', what: 'The developer portal: API and SDK docs, the service and capability registry, event schemas, credentials, playgrounds and mod publishing.' },-- { id: 'blog', name: 'Blog', icon: 'blog', service: 'blog', profile: 'info', tagline: 'Long-form, yours to keep', what: 'The official OpenVibe blog and a blog for every member: drafts, scheduling, media, comments and feeds.' },-- { id: 'wiki', name: 'Wiki', icon: 'wiki', service: 'wiki', profile: 'ugc', tagline: 'Knowledge, with sources', what: 'Wiki spaces with page trees, revision history, citations and discussion, editable together.' },-- { id: 'news', name: 'News', icon: 'news', service: 'news', profile: 'info', tagline: 'Sourced, not spun', what: 'Source-backed stories: clustered coverage, cited summaries, multiple perspectives and timelines.' },-- { id: 'reviews', name: 'Reviews', icon: 'reviews', service: 'reviews', profile: 'ugc', tagline: 'Reviews with receipts', what: 'Review signals gathered across sources with provenance, pros and cons, and community discussion. No invented ratings.' },-- { id: 'tips', name: 'Tips', icon: 'tips', service: 'tips', profile: 'streaming', tagline: 'Support creators', what: 'Tips, goals, paid messages and alerts for creators, settled by the network\'s billing ledger.' },-- { id: 'vip', name: 'VIP', icon: 'vip', service: 'vip', profile: 'account', tagline: 'Memberships and perks', what: 'Creator and network memberships: plans, perks and benefits recognised on every OpenVibe site.' },-- { id: 'trade', name: 'Trade', icon: 'trade', service: 'trade', profile: 'ugc', tagline: 'Watchlists and alerts', what: 'Informational watchlists, sourced market context and alerts. No custody, no order execution.' },-- { id: 'host', name: 'Host', icon: 'host', service: 'host', profile: 'hosting', tagline: 'Deploy and host', what: 'The network\'s deployment plane first, then simple hosting for community sites, bots and mods.' },-- { id: 'deals', name: 'Deals', icon: 'deals', service: 'deals', profile: 'info', tagline: 'Deals worth sharing', what: 'Deals submitted and voted on by the community, with source, price and freshness always shown.' },-- { id: 'coupons', name: 'Coupons', icon: 'coupons', service: 'coupons', profile: 'info', tagline: 'Codes that work', what: 'Coupon codes with merchant matching, restrictions, expiry and real-people validity reports.' },-- { id: 'stream', name: 'OpenRe.Stream', icon: 'stream', service: 'openre', profile: 'streaming', tagline: 'Restream anywhere', what: 'Ingest once and send the stream to every platform at once; keys, destinations and output health in one place.' },--];++// Since openvibe-contracts 0.42.0 (WS-C task 1) the list is the manifests' `site` blocks, in `position` order:++// name, icon, tagline, what, the legal profile, and a host only while the manifest has no publicOrigin. A++// site's id is its icon name (live, tools, …, stream for OpenRe.Stream).++const RAW_SITES = contracts.services.manifests.filter((m) => m.site).sort((a, b) => (a.site.position ?? 999) - (b.site.position ?? 999)).map((m) => ({++ id: m.site.icon, name: m.site.name, icon: m.site.icon, service: m.id, profile: m.site.legalProfile, tagline: m.site.tagline, what: m.site.what,++ ...(m.site.host ? { host: m.site.host } : {}),++}));function manifestHost(service) {const m = contracts.services.get(service);try { return m && m.publicOrigin ? new URL(m.publicOrigin).hostname : null; } catch { return null; }@@@ -38,30 +38,14 @@ const { buildTopics, eventsOf } = require('./topics');const { driftOf } = require('./versions');const { SITES } = require('../frame/sites');--// Where each running service answers health checks from this host (manifests carry the public origin).--// TODO(plan §5.1 "contract manifests plus validated environment overrides"): registry.service-manifest@1 has--// no internal address, so these loopback ports stay here until Contracts adds one (or Network reads--// OpenVibe.Host's host.json). Until then OV_<ID>_INTERNAL_URL overrides any of them (internalFromEnv below),--// validated as a loopback http(s) URL, which is the environment half of that rule.--const INTERNAL = {-- network: 'http://127.0.0.1:4000', live: 'http://127.0.0.1:3000', media: 'http://127.0.0.1:4100',-- tools: 'http://127.0.0.1:4001', games: 'http://127.0.0.1:8000', community: 'http://127.0.0.1:4200',-- events: 'http://127.0.0.1:4300', billing: 'http://127.0.0.1:4600',-- // Assigned ports of services that are placeholders or new; a placeholder is never polled.-- chat: 'http://127.0.0.1:4400', openre: 'http://127.0.0.1:4500', tips: 'http://127.0.0.1:4610', vip: 'http://127.0.0.1:4620',-- ai: 'http://127.0.0.1:4700', search: 'http://127.0.0.1:4710', sources: 'http://127.0.0.1:4720', wiki: 'http://127.0.0.1:4800',-- blog: 'http://127.0.0.1:4810', news: 'http://127.0.0.1:4820', reviews: 'http://127.0.0.1:4830', deals: 'http://127.0.0.1:4840',-- coupons: 'http://127.0.0.1:4850', trade: 'http://127.0.0.1:4860', codes: 'http://127.0.0.1:4900', host: 'http://127.0.0.1:4910',--};++// Where each running service answers health checks from this host: its manifest's `internalOrigin`++// (openvibe-contracts ≥ 0.42.0, WS-C task 1). OV_<ID>_INTERNAL_URL overrides any of them (internalFromEnv++// below), validated as a loopback http(s) URL: the environment half of "manifests plus validated overrides".++const INTERNAL = Object.fromEntries(contracts.services.manifests.filter((m) => m.internalOrigin).map((m) => [m.id, m.internalOrigin]));const POLL_MS = 60 * 1000;--// Readiness paths of services whose manifest (openvibe-contracts) does not carry `ready` yet; a manifest's--// own `ready` always wins. Each answers with openvibe-shared/ready's shape; until a service deploys it, its--// health path is used and the row says so (basis: 'health'). Only entries whose manifest still lacks `ready`--// may stay (test/registry-ecosystem.test.js): tools left when its manifest gained one. TODO(contracts): add--// `ready: /api/ready` to the network and media manifests, then this map is empty.--const READY_PATHS = { network: '/api/ready', media: '/api/ready' };--// Liveness paths of services whose manifest in the pinned openvibe-contracts has none (none since--// 0.30, where AI's manifest carries /api/health and /api/ready).++// Readiness and liveness paths come from the manifests (`ready`, `health`); these maps are for a manifest++// that lacks one and stay empty (test/registry-ecosystem.test.js).++const READY_PATHS = {};const HEALTH_PATHS = {};/** Row status from a readiness body (openvibe-shared/ready shape, or an older ad-hoc one). */@@@ -24,75 +24,41 @@* When a service goes public, change its row here in the same commit that points its domain at it,* after checking the domain no longer answers with "this page is a placeholder".*-- * TODO(plan §5.1 "data from contract manifests plus validated overrides"): this overlay is the override-- * half. It cannot come from the manifests (they say maturity, and the question here is what a domain-- * serves today), so it stays Network's, pinned id by id by test/registry-exposure.test.js. Candidates to-- * derive it later: each repository's STATUS.json `deployed`, or OpenVibe.Sites' sites.json (which domains-- * still get a placeholder page), once both are published somewhere Network can read.++ * Since openvibe-contracts 0.42.0 (WS-C task 1) the states are the manifests' `exposure`: a service going++ * public is one manifest edit in Contracts (in the release that points its domain at it), no Network code.*/--const STATES = ['live', 'internal', 'library', 'repository', 'placeholder'];++const contracts = require('openvibe-contracts');++++const STATES = ['live', 'internal', 'library', 'repository', 'placeholder', 'retired'];const LABEL = {live: 'live (public)',internal: 'internal (loopback only, no public site yet)',library: 'library (released)',repository: 'repository (code with CI, nothing released or running)',placeholder: 'placeholder (planned)',++ retired: 'retired',};--// public_site: what the service's public domain(s) answer today — 'service' (the service itself),--// 'placeholder' (a Sites/admin placeholder page) or null (no public domain).--const SITE_PLACEHOLDER = 'the domain serves the OpenVibe.Sites placeholder page';--const ADMIN_PLACEHOLDER = 'the domain serves the admin.openvibe.network placeholder page';----// The libraries' release is the version Network itself installs (its pins move with every release),--// so this never goes stale the way hand-written numbers did.--function library(pkg, repo, note) {++// The libraries' release starts as the version Network itself installs (its pins move with every release)++// and becomes the latest published tag once library-tags.js has one (setLibraryReleases).++function libraryRelease(pkg, repo) {let version = null;try { version = require(`${pkg}/package.json`).version; } catch { /* not installed */ }const release = version ? `v${version}` : null;-- const out = { state: 'library', public_site: null, package: pkg, release,-- distribution: release ? `https://codeload.github.com/OpenVibers/${repo}/tar.gz/refs/tags/${release}` : null };-- if (note) out.note = note;-- return out;++ return { release, distribution: release && repo ? `https://codeload.github.com/OpenVibers/${repo}/tar.gz/refs/tags/${release}` : null };}--const EXPOSURE = {-- network: { state: 'live', public_site: 'service' },-- live: { state: 'live', public_site: 'service' },-- tools: { state: 'live', public_site: 'service' },-- media: { state: 'live', public_site: 'service' },-- games: { state: 'live', public_site: 'service' },-- community: { state: 'live', public_site: 'service' },-- events: { state: 'live', public_site: 'service' },-- billing: { state: 'live', public_site: 'service', note: 'the public domain is the staff console; accounts pay through each product' },-- codes: { state: 'live', public_site: 'service' },-- blog: { state: 'live', public_site: 'service' },-- wiki: { state: 'live', public_site: 'service' },-- sites: { state: 'live', public_site: 'service', note: 'serves the placeholder pages of domains whose service is not public yet' },---- news: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },-- reviews: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },-- deals: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },-- coupons: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },-- trade: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },-- host: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },-- tips: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },-- vip: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },-- openre: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },-- search: { state: 'live', public_site: 'service', note: 'public search page and query API at search.openvibe.network (since 2026-09-23)' },-- sources: { state: 'internal', public_site: null, note: 'internal ingestion service; sources.openvibe.network says so (its API is loopback-only)' },-- chat: { state: 'live', public_site: 'service', note: 'openvibe.chat since 2026-09-24 (global chat, messages, settings); also serves Live\'s chat paths' },-- ai: { state: 'internal', public_site: null, note: 'called by other services at 127.0.0.1:4700; ai.openvibe.network serves the OpenVibe.Sites placeholder page' },---- sdk: library('openvibe-sdk', 'OpenVibe.SDK'),-- shared: library('openvibe-shared', 'OpenVibe.Shared', 'also served at /shared/* by each site'),-- contracts: library('openvibe-contracts', 'OpenVibe.Contracts', 'schemas are also served by this registry at their $id URLs'),-- publishing: library('openvibe-publishing', 'OpenVibe.Publishing', 'the shared publishing runtime used by Wiki, Blog, News, Reviews, Deals, Coupons and Trade'),-- examples: { state: 'repository', public_site: null, note: 'example apps with CI in OpenVibers/OpenVibe.Examples; not published as a package' },---- realtime: { state: 'placeholder', public_site: null },--};++/**++ * Each manifest's `exposure` (openvibe-contracts ≥ 0.42.0, WS-C task 1): { state, public_site, note } plus,++ * for libraries, package / release / distribution. A service going public is a manifest change in Contracts.++ */++const EXPOSURE = Object.fromEntries(contracts.services.manifests.filter((m) => m.exposure && STATES.includes(m.exposure.state)).map((m) => {++ const e = { state: m.exposure.state, public_site: m.exposure.publicSite == null ? null : m.exposure.publicSite };++ if (m.exposure.note) e.note = m.exposure.note;++ if (e.state === 'library') Object.assign(e, { package: m.exposure.package, ...libraryRelease(m.exposure.package, m.exposure.repo) });++ return [m.id, e];++}));/** The exposure of one service id; an id nobody classified is 'unknown', never assumed public. */function exposureOf(id) {@@@ -101,7 +101,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], chassert.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})`);-- assert.deepStrictEqual(st.exposure_summary, { live: 14, internal: 11, library: 4, repository: 1, placeholder: 1 });++ assert.deepStrictEqual(st.exposure_summary, { live: 14, internal: 11, 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());nnew file mode 100644@@@ -0,0 +1,70 @@++'use strict';++// WS-C task 1, the manifest-derived registry: a new service manifest (a fixture added the way Contracts adds++// one) appears in the discovery document, the registry's service list, the status page and the frame's site++// list, polled at its internalOrigin and linked at its publicOrigin, with no Network code edit; one whose++// exposure is internal stays out of the navigation and is polled on loopback only.++const assert = require('assert');++const http = require('http');++const express = require('express');++const contracts = require('openvibe-contracts');++++const base = { version: '0.1.0', status: 'alpha', repository: 'OpenVibers/OpenVibe.Zines', health: '/api/health', ready: '/api/ready',++ capabilities: [], eventsProduced: [], eventsConsumed: [], namespacesOwned: [], contractRanges: {} };++++(async () => {++ const svc = http.createServer((req, res) => {++ res.setHeader('content-type', 'application/json');++ if (req.url === '/api/ready') return res.end(JSON.stringify({ ready: true, status: 'ready', failed: [], degraded: [], checks: { db: { status: 'ok', required: true } } }));++ if (req.url === '/api/health') return res.end('{"status":"ok"}');++ res.statusCode = 404; res.end('{}');++ });++ await new Promise((r) => svc.listen(0, '127.0.0.1', r));++ const origin = `http://127.0.0.1:${svc.address().port}`;++ const fixtures = [++ { ...base, id: 'zines', name: 'OpenVibe.Zines', domains: ['openvibe.zines'], publicOrigin: 'https://openvibe.zines', internalOrigin: origin,++ exposure: { state: 'live', publicSite: 'service' },++ site: { name: 'Zines', icon: 'zines', tagline: 'Small magazines', what: 'Make and read small magazines.', legalProfile: 'ugc', position: 90 } },++ { ...base, id: 'almanac', name: 'OpenVibe.Almanac', domains: ['openvibe.almanac'], publicOrigin: 'https://openvibe.almanac', internalOrigin: origin,++ exposure: { state: 'internal', publicSite: 'placeholder', note: 'the domain serves a placeholder' },++ site: { name: 'Almanac', icon: 'almanac', tagline: 'Dates', what: 'What happened when.', legalProfile: 'info', position: 91 } },++ ];++ for (const m of fixtures) {++ const v = contracts.validate('registry.service-manifest@1', m);++ assert.ok(v.valid, JSON.stringify(v.errors));++ contracts.services.manifests.push(m);++ }++ // Loaded after the fixtures, like a Network release that installs a Contracts release with them.++ const { createEcosystemRegistry, INTERNAL } = require('../server/registry/ecosystem');++ const { exposureOf } = require('../server/registry/exposure');++ const { SITES, siteForHost } = require('../server/frame/sites');++ const { createStatusRoutes } = require('../server/status/routes');++++ assert.strictEqual(INTERNAL.zines, origin, 'polled at its internalOrigin');++ assert.strictEqual(exposureOf('zines').state, 'live');++ assert.deepStrictEqual(SITES.filter((s) => ['zines', 'almanac'].includes(s.service)).map((s) => [s.id, s.status, s.host, s.profile]), [['zines', 'open', 'openvibe.zines', 'ugc'], ['almanac', 'soon', 'openvibe.almanac', 'info']]);++ assert.strictEqual(siteForHost('openvibe.zines').service, 'zines');++++ const eco = createEcosystemRegistry({ issuer: 'https://openvibe.network' });++ await eco.pollAll();++ const app = express();++ app.use(createStatusRoutes({ ecosystem: eco }));++ app.use(eco.router());++ const srv = http.createServer(app);++ await new Promise((r) => srv.listen(0, '127.0.0.1', r));++ const url = `http://127.0.0.1:${srv.address().port}`;++ const json = (p) => fetch(url + p).then((r) => r.json());++++ const disc = Object.fromEntries((await json('/.well-known/openvibe')).services.map((s) => [s.id, s]));++ assert.strictEqual(disc.zines.state, 'live'); assert.strictEqual(disc.almanac.state, 'internal');++ const listed = await json('/api/v1/registry/services');++ const rows = Object.fromEntries((listed.services || listed).map((s) => [s.id, s]));++ assert.ok(rows.zines && rows.almanac, 'both in the registry');++ const status = Object.fromEntries((await json('/api/v1/status')).services.map((s) => [s.id, s]));++ assert.strictEqual(status.zines.label, 'up'); assert.strictEqual(status.zines.origin, 'https://openvibe.zines');++ assert.strictEqual(status.almanac.label, 'up (loopback only)'); assert.strictEqual(status.almanac.origin, null);++ assert.strictEqual(status.almanac.planned_origin, 'https://openvibe.almanac');++++ srv.close(); svc.close(); eco.stop && eco.stop();++ console.log('registry manifest fixture: all checks passed');++ process.exit(0);++})().catch((err) => { console.error(err); process.exit(1); });
@@ -20,7 +20,7 @@
@@ -20,7 +20,7 @@
"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.41.1",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.42.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.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.12.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.12.0",
"uuid": "^11.1.0",
"uuid": "^11.1.0",
@@ -1465,9 +1465,9 @@
@@ -1465,9 +1465,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.41.1",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.41.1",
- "integrity": "sha512-PdXYQK9Nt9LBv45ZR8hdIW6xt9zrSJAETDP5roRWQ3J3c7+TP6iTBZp/X1bjdxNDnNzxMPthkGsO7+E5vSXrbw==",
+ "version": "0.42.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.42.0",
+ "integrity": "sha512-lQUPg3bkpusZmFRyJHaARwnA4J+WcmQfVdM28yOHYhVChiMs/3rr3hl10JucKOlxxx9Cii1Go0iv+IfxqVO8UA==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.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/github-integration.test.js && node test/revocation.test.js && node test/usernames.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 && node test/moderation-audit.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/github-integration.test.js && node test/revocation.test.js && node test/usernames.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-manifest-fixture.test.js && node test/staff-claims.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 && node test/moderation-audit.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"
},
},
@@ -23,7 +23,7 @@
@@ -23,7 +23,7 @@
"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.41.1",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.42.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.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.12.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.12.0",
"uuid": "^11.1.0",
"uuid": "^11.1.0",
@@ -10,30 +10,13 @@ const { exposureOf } = require('../registry/exposure');
@@ -10,30 +10,13 @@ const { exposureOf } = require('../registry/exposure');
// A site is 'open' (in the nav) only when its service's public domain serves the service itself, per
// A site is 'open' (in the nav) only when its service's public domain serves the service itself, per
// Network's exposure overlay (server/registry/exposure.js); every other site is 'soon' and says why
// Network's exposure overlay (server/registry/exposure.js); every other site is 'soon' and says why
// (state: internal = runs on loopback only, placeholder = planned). The two lists cannot disagree.
// (state: internal = runs on loopback only, placeholder = planned). The two lists cannot disagree.
-// host comes from the service's manifest (openvibe-contracts publicOrigin); an entry names its host only
-// while its manifest has no public origin. TODO(plan §5.1/§5.3): name, icon, tagline, what and the
-// legal profile are presentation with no manifest field yet; they move when the manifest schema has them.
-const RAW_SITES = [
- { id: 'live', name: 'Live', icon: 'live', service: 'live', profile: 'streaming', tagline: 'Streams, clips and chat', what: 'Live streaming from a browser, OBS or a phone, with restreaming, clips, VODs, chat games and channel points.' },
- { id: 'tools', name: 'Tools', icon: 'tools', service: 'tools', profile: 'tools', tagline: 'Every online tool', what: 'More than 160 online tools: converters, downloaders, PDF and image tools, developer utilities and network diagnostics, each on its own address.' },
- { id: 'community', name: 'Community', icon: 'community', service: 'community', profile: 'ugc', tagline: 'Pastes and posts', what: 'Share text and code with a link, comment, and follow what people on OpenVibe are making.' },
- { id: 'games', name: 'Games', icon: 'games', service: 'games', profile: 'games', tagline: 'Browser games', what: 'Scraplandia, a shared pixel canvas and other games that run in a tab and know your OpenVibe account.' },
- { id: 'media', name: 'Media', icon: 'media', service: 'media', profile: 'hosting', tagline: 'VODs, clips, files', what: 'The public media library behind every site: recorded streams, clips, images and uploads.' },
- { id: 'network', name: 'Network', icon: 'network', service: 'network', profile: 'account', tagline: 'Account and themes', what: 'The account behind every site: one sign-in, themes, notifications, history and linked services.' },
- { id: 'chat', name: 'Chat', icon: 'chat', service: 'chat', profile: 'ugc', tagline: 'One chat for the network', what: 'Global chat, rooms, stream chat and direct messages under one account, with or without JavaScript.' },
- { id: 'codes', name: 'Codes', icon: 'codes', service: 'codes', profile: 'ugc', tagline: 'Build on OpenVibe', what: 'The developer portal: API and SDK docs, the service and capability registry, event schemas, credentials, playgrounds and mod publishing.' },
- { id: 'blog', name: 'Blog', icon: 'blog', service: 'blog', profile: 'info', tagline: 'Long-form, yours to keep', what: 'The official OpenVibe blog and a blog for every member: drafts, scheduling, media, comments and feeds.' },
- { id: 'wiki', name: 'Wiki', icon: 'wiki', service: 'wiki', profile: 'ugc', tagline: 'Knowledge, with sources', what: 'Wiki spaces with page trees, revision history, citations and discussion, editable together.' },
- { id: 'news', name: 'News', icon: 'news', service: 'news', profile: 'info', tagline: 'Sourced, not spun', what: 'Source-backed stories: clustered coverage, cited summaries, multiple perspectives and timelines.' },
- { id: 'reviews', name: 'Reviews', icon: 'reviews', service: 'reviews', profile: 'ugc', tagline: 'Reviews with receipts', what: 'Review signals gathered across sources with provenance, pros and cons, and community discussion. No invented ratings.' },
- { id: 'tips', name: 'Tips', icon: 'tips', service: 'tips', profile: 'streaming', tagline: 'Support creators', what: 'Tips, goals, paid messages and alerts for creators, settled by the network\'s billing ledger.' },
- { id: 'vip', name: 'VIP', icon: 'vip', service: 'vip', profile: 'account', tagline: 'Memberships and perks', what: 'Creator and network memberships: plans, perks and benefits recognised on every OpenVibe site.' },
- { id: 'trade', name: 'Trade', icon: 'trade', service: 'trade', profile: 'ugc', tagline: 'Watchlists and alerts', what: 'Informational watchlists, sourced market context and alerts. No custody, no order execution.' },
- { id: 'host', name: 'Host', icon: 'host', service: 'host', profile: 'hosting', tagline: 'Deploy and host', what: 'The network\'s deployment plane first, then simple hosting for community sites, bots and mods.' },
- { id: 'deals', name: 'Deals', icon: 'deals', service: 'deals', profile: 'info', tagline: 'Deals worth sharing', what: 'Deals submitted and voted on by the community, with source, price and freshness always shown.' },
- { id: 'coupons', name: 'Coupons', icon: 'coupons', service: 'coupons', profile: 'info', tagline: 'Codes that work', what: 'Coupon codes with merchant matching, restrictions, expiry and real-people validity reports.' },
- { id: 'stream', name: 'OpenRe.Stream', icon: 'stream', service: 'openre', profile: 'streaming', tagline: 'Restream anywhere', what: 'Ingest once and send the stream to every platform at once; keys, destinations and output health in one place.' },
-];
+// Since openvibe-contracts 0.42.0 (WS-C task 1) the list is the manifests' `site` blocks, in `position` order:
+// name, icon, tagline, what, the legal profile, and a host only while the manifest has no publicOrigin. A
+// site's id is its icon name (live, tools, …, stream for OpenRe.Stream).
+const RAW_SITES = contracts.services.manifests.filter((m) => m.site).sort((a, b) => (a.site.position ?? 999) - (b.site.position ?? 999)).map((m) => ({
+ id: m.site.icon, name: m.site.name, icon: m.site.icon, service: m.id, profile: m.site.legalProfile, tagline: m.site.tagline, what: m.site.what,
+ ...(m.site.host ? { host: m.site.host } : {}),
+}));
function manifestHost(service) {
function manifestHost(service) {
const m = contracts.services.get(service);
const m = contracts.services.get(service);
try { return m && m.publicOrigin ? new URL(m.publicOrigin).hostname : null; } catch { return null; }
try { return m && m.publicOrigin ? new URL(m.publicOrigin).hostname : null; } catch { return null; }
@@ -38,30 +38,14 @@ const { buildTopics, eventsOf } = require('./topics');
@@ -38,30 +38,14 @@ const { buildTopics, eventsOf } = require('./topics');
const { driftOf } = require('./versions');
const { driftOf } = require('./versions');
const { SITES } = require('../frame/sites');
const { SITES } = require('../frame/sites');
-// Where each running service answers health checks from this host (manifests carry the public origin).
-// TODO(plan §5.1 "contract manifests plus validated environment overrides"): registry.service-manifest@1 has
-// no internal address, so these loopback ports stay here until Contracts adds one (or Network reads
-// OpenVibe.Host's host.json). Until then OV_<ID>_INTERNAL_URL overrides any of them (internalFromEnv below),
-// validated as a loopback http(s) URL, which is the environment half of that rule.
-const INTERNAL = {
- network: 'http://127.0.0.1:4000', live: 'http://127.0.0.1:3000', media: 'http://127.0.0.1:4100',
- tools: 'http://127.0.0.1:4001', games: 'http://127.0.0.1:8000', community: 'http://127.0.0.1:4200',
- events: 'http://127.0.0.1:4300', billing: 'http://127.0.0.1:4600',
- // Assigned ports of services that are placeholders or new; a placeholder is never polled.
- chat: 'http://127.0.0.1:4400', openre: 'http://127.0.0.1:4500', tips: 'http://127.0.0.1:4610', vip: 'http://127.0.0.1:4620',
- ai: 'http://127.0.0.1:4700', search: 'http://127.0.0.1:4710', sources: 'http://127.0.0.1:4720', wiki: 'http://127.0.0.1:4800',
- blog: 'http://127.0.0.1:4810', news: 'http://127.0.0.1:4820', reviews: 'http://127.0.0.1:4830', deals: 'http://127.0.0.1:4840',
- coupons: 'http://127.0.0.1:4850', trade: 'http://127.0.0.1:4860', codes: 'http://127.0.0.1:4900', host: 'http://127.0.0.1:4910',
-};
+// Where each running service answers health checks from this host: its manifest's `internalOrigin`
+// (openvibe-contracts ≥ 0.42.0, WS-C task 1). OV_<ID>_INTERNAL_URL overrides any of them (internalFromEnv
+// below), validated as a loopback http(s) URL: the environment half of "manifests plus validated overrides".
+const INTERNAL = Object.fromEntries(contracts.services.manifests.filter((m) => m.internalOrigin).map((m) => [m.id, m.internalOrigin]));
const POLL_MS = 60 * 1000;
const POLL_MS = 60 * 1000;
-// Readiness paths of services whose manifest (openvibe-contracts) does not carry `ready` yet; a manifest's
-// own `ready` always wins. Each answers with openvibe-shared/ready's shape; until a service deploys it, its
-// health path is used and the row says so (basis: 'health'). Only entries whose manifest still lacks `ready`
-// may stay (test/registry-ecosystem.test.js): tools left when its manifest gained one. TODO(contracts): add
-// `ready: /api/ready` to the network and media manifests, then this map is empty.
-const READY_PATHS = { network: '/api/ready', media: '/api/ready' };
-// Liveness paths of services whose manifest in the pinned openvibe-contracts has none (none since
-// 0.30, where AI's manifest carries /api/health and /api/ready).
+// Readiness and liveness paths come from the manifests (`ready`, `health`); these maps are for a manifest
+// that lacks one and stay empty (test/registry-ecosystem.test.js).
+const READY_PATHS = {};
const HEALTH_PATHS = {};
const HEALTH_PATHS = {};
/** Row status from a readiness body (openvibe-shared/ready shape, or an older ad-hoc one). */
/** Row status from a readiness body (openvibe-shared/ready shape, or an older ad-hoc one). */
@@ -24,75 +24,41 @@
@@ -24,75 +24,41 @@
* When a service goes public, change its row here in the same commit that points its domain at it,
* When a service goes public, change its row here in the same commit that points its domain at it,
* after checking the domain no longer answers with "this page is a placeholder".
* after checking the domain no longer answers with "this page is a placeholder".
*
*
- * TODO(plan §5.1 "data from contract manifests plus validated overrides"): this overlay is the override
- * half. It cannot come from the manifests (they say maturity, and the question here is what a domain
- * serves today), so it stays Network's, pinned id by id by test/registry-exposure.test.js. Candidates to
- * derive it later: each repository's STATUS.json `deployed`, or OpenVibe.Sites' sites.json (which domains
- * still get a placeholder page), once both are published somewhere Network can read.
+ * Since openvibe-contracts 0.42.0 (WS-C task 1) the states are the manifests' `exposure`: a service going
+ * public is one manifest edit in Contracts (in the release that points its domain at it), no Network code.
*/
*/
-const STATES = ['live', 'internal', 'library', 'repository', 'placeholder'];
+const contracts = require('openvibe-contracts');
+
+const STATES = ['live', 'internal', 'library', 'repository', 'placeholder', 'retired'];
const LABEL = {
const LABEL = {
live: 'live (public)',
live: 'live (public)',
internal: 'internal (loopback only, no public site yet)',
internal: 'internal (loopback only, no public site yet)',
library: 'library (released)',
library: 'library (released)',
repository: 'repository (code with CI, nothing released or running)',
repository: 'repository (code with CI, nothing released or running)',
placeholder: 'placeholder (planned)',
placeholder: 'placeholder (planned)',
+ retired: 'retired',
};
};
-// public_site: what the service's public domain(s) answer today — 'service' (the service itself),
-// 'placeholder' (a Sites/admin placeholder page) or null (no public domain).
-const SITE_PLACEHOLDER = 'the domain serves the OpenVibe.Sites placeholder page';
-const ADMIN_PLACEHOLDER = 'the domain serves the admin.openvibe.network placeholder page';
-
-// The libraries' release is the version Network itself installs (its pins move with every release),
-// so this never goes stale the way hand-written numbers did.
-function library(pkg, repo, note) {
+// The libraries' release starts as the version Network itself installs (its pins move with every release)
+// and becomes the latest published tag once library-tags.js has one (setLibraryReleases).
+function libraryRelease(pkg, repo) {
let version = null;
let version = null;
try { version = require(`${pkg}/package.json`).version; } catch { /* not installed */ }
try { version = require(`${pkg}/package.json`).version; } catch { /* not installed */ }
const release = version ? `v${version}` : null;
const release = version ? `v${version}` : null;
- const out = { state: 'library', public_site: null, package: pkg, release,
- distribution: release ? `https://codeload.github.com/OpenVibers/${repo}/tar.gz/refs/tags/${release}` : null };
- if (note) out.note = note;
- return out;
+ return { release, distribution: release && repo ? `https://codeload.github.com/OpenVibers/${repo}/tar.gz/refs/tags/${release}` : null };
}
}
-const EXPOSURE = {
- network: { state: 'live', public_site: 'service' },
- live: { state: 'live', public_site: 'service' },
- tools: { state: 'live', public_site: 'service' },
- media: { state: 'live', public_site: 'service' },
- games: { state: 'live', public_site: 'service' },
- community: { state: 'live', public_site: 'service' },
- events: { state: 'live', public_site: 'service' },
- billing: { state: 'live', public_site: 'service', note: 'the public domain is the staff console; accounts pay through each product' },
- codes: { state: 'live', public_site: 'service' },
- blog: { state: 'live', public_site: 'service' },
- wiki: { state: 'live', public_site: 'service' },
- sites: { state: 'live', public_site: 'service', note: 'serves the placeholder pages of domains whose service is not public yet' },
-
- news: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },
- reviews: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },
- deals: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },
- coupons: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },
- trade: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },
- host: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },
- tips: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },
- vip: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },
- openre: { state: 'internal', public_site: 'placeholder', note: SITE_PLACEHOLDER },
- search: { state: 'live', public_site: 'service', note: 'public search page and query API at search.openvibe.network (since 2026-09-23)' },
- sources: { state: 'internal', public_site: null, note: 'internal ingestion service; sources.openvibe.network says so (its API is loopback-only)' },
- chat: { state: 'live', public_site: 'service', note: 'openvibe.chat since 2026-09-24 (global chat, messages, settings); also serves Live\'s chat paths' },
- ai: { state: 'internal', public_site: null, note: 'called by other services at 127.0.0.1:4700; ai.openvibe.network serves the OpenVibe.Sites placeholder page' },
-
- sdk: library('openvibe-sdk', 'OpenVibe.SDK'),
- shared: library('openvibe-shared', 'OpenVibe.Shared', 'also served at /shared/* by each site'),
- contracts: library('openvibe-contracts', 'OpenVibe.Contracts', 'schemas are also served by this registry at their $id URLs'),
- publishing: library('openvibe-publishing', 'OpenVibe.Publishing', 'the shared publishing runtime used by Wiki, Blog, News, Reviews, Deals, Coupons and Trade'),
- examples: { state: 'repository', public_site: null, note: 'example apps with CI in OpenVibers/OpenVibe.Examples; not published as a package' },
-
- realtime: { state: 'placeholder', public_site: null },
-};
+/**
+ * Each manifest's `exposure` (openvibe-contracts ≥ 0.42.0, WS-C task 1): { state, public_site, note } plus,
+ * for libraries, package / release / distribution. A service going public is a manifest change in Contracts.
+ */
+const EXPOSURE = Object.fromEntries(contracts.services.manifests.filter((m) => m.exposure && STATES.includes(m.exposure.state)).map((m) => {
+ const e = { state: m.exposure.state, public_site: m.exposure.publicSite == null ? null : m.exposure.publicSite };
+ if (m.exposure.note) e.note = m.exposure.note;
+ if (e.state === 'library') Object.assign(e, { package: m.exposure.package, ...libraryRelease(m.exposure.package, m.exposure.repo) });
+ return [m.id, e];
+}));
/** The exposure of one service id; an id nobody classified is 'unknown', never assumed public. */
/** The exposure of one service id; an id nobody classified is 'unknown', never assumed public. */
function exposureOf(id) {
function exposureOf(id) {
@@ -101,7 +101,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -101,7 +101,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
assert.strictEqual(sBy.live.label, 'up');
assert.strictEqual(sBy.live.label, 'up');
assert.strictEqual(sBy.live.origin, 'https://openvibe.live');
assert.strictEqual(sBy.live.origin, 'https://openvibe.live');
assert.strictEqual(sBy.shared.label, `not running (library, released v${require('openvibe-shared/package.json').version})`);
assert.strictEqual(sBy.shared.label, `not running (library, released v${require('openvibe-shared/package.json').version})`);
- assert.deepStrictEqual(st.exposure_summary, { live: 14, internal: 11, library: 4, repository: 1, placeholder: 1 });
+ assert.deepStrictEqual(st.exposure_summary, { live: 14, internal: 11, library: 4, repository: 1, placeholder: 1, retired: 0 });
// The page: no row for a service whose domain serves a placeholder shows a bare "Up".
// 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());
const html = await fetch(base + '/status').then(x => x.text());
new file mode 100644
new file mode 100644
@@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
+'use strict';
+// WS-C task 1, the manifest-derived registry: a new service manifest (a fixture added the way Contracts adds
+// one) appears in the discovery document, the registry's service list, the status page and the frame's site
+// list, polled at its internalOrigin and linked at its publicOrigin, with no Network code edit; one whose
+// exposure is internal stays out of the navigation and is polled on loopback only.
+const assert = require('assert');
+const http = require('http');
+const express = require('express');
+const contracts = require('openvibe-contracts');
+
+const base = { version: '0.1.0', status: 'alpha', repository: 'OpenVibers/OpenVibe.Zines', health: '/api/health', ready: '/api/ready',
+ capabilities: [], eventsProduced: [], eventsConsumed: [], namespacesOwned: [], contractRanges: {} };
+
+(async () => {
+ const svc = http.createServer((req, res) => {
+ res.setHeader('content-type', 'application/json');
+ if (req.url === '/api/ready') return res.end(JSON.stringify({ ready: true, status: 'ready', failed: [], degraded: [], checks: { db: { status: 'ok', required: true } } }));
+ if (req.url === '/api/health') return res.end('{"status":"ok"}');
+ res.statusCode = 404; res.end('{}');
+ });
+ await new Promise((r) => svc.listen(0, '127.0.0.1', r));
+ const origin = `http://127.0.0.1:${svc.address().port}`;
+ const fixtures = [
+ { ...base, id: 'zines', name: 'OpenVibe.Zines', domains: ['openvibe.zines'], publicOrigin: 'https://openvibe.zines', internalOrigin: origin,
+ exposure: { state: 'live', publicSite: 'service' },
+ site: { name: 'Zines', icon: 'zines', tagline: 'Small magazines', what: 'Make and read small magazines.', legalProfile: 'ugc', position: 90 } },
+ { ...base, id: 'almanac', name: 'OpenVibe.Almanac', domains: ['openvibe.almanac'], publicOrigin: 'https://openvibe.almanac', internalOrigin: origin,
+ exposure: { state: 'internal', publicSite: 'placeholder', note: 'the domain serves a placeholder' },
+ site: { name: 'Almanac', icon: 'almanac', tagline: 'Dates', what: 'What happened when.', legalProfile: 'info', position: 91 } },
+ ];
+ for (const m of fixtures) {
+ const v = contracts.validate('registry.service-manifest@1', m);
+ assert.ok(v.valid, JSON.stringify(v.errors));
+ contracts.services.manifests.push(m);
+ }
+ // Loaded after the fixtures, like a Network release that installs a Contracts release with them.
+ const { createEcosystemRegistry, INTERNAL } = require('../server/registry/ecosystem');
+ const { exposureOf } = require('../server/registry/exposure');
+ const { SITES, siteForHost } = require('../server/frame/sites');
+ const { createStatusRoutes } = require('../server/status/routes');
+
+ assert.strictEqual(INTERNAL.zines, origin, 'polled at its internalOrigin');
+ assert.strictEqual(exposureOf('zines').state, 'live');
+ assert.deepStrictEqual(SITES.filter((s) => ['zines', 'almanac'].includes(s.service)).map((s) => [s.id, s.status, s.host, s.profile]), [['zines', 'open', 'openvibe.zines', 'ugc'], ['almanac', 'soon', 'openvibe.almanac', 'info']]);
+ assert.strictEqual(siteForHost('openvibe.zines').service, 'zines');
+
+ const eco = createEcosystemRegistry({ issuer: 'https://openvibe.network' });
+ await eco.pollAll();
+ const app = express();
+ app.use(createStatusRoutes({ ecosystem: eco }));
+ app.use(eco.router());
+ const srv = http.createServer(app);
+ await new Promise((r) => srv.listen(0, '127.0.0.1', r));
+ const url = `http://127.0.0.1:${srv.address().port}`;
+ const json = (p) => fetch(url + p).then((r) => r.json());
+
+ const disc = Object.fromEntries((await json('/.well-known/openvibe')).services.map((s) => [s.id, s]));
+ assert.strictEqual(disc.zines.state, 'live'); assert.strictEqual(disc.almanac.state, 'internal');
+ const listed = await json('/api/v1/registry/services');
+ const rows = Object.fromEntries((listed.services || listed).map((s) => [s.id, s]));
+ assert.ok(rows.zines && rows.almanac, 'both in the registry');
+ const status = Object.fromEntries((await json('/api/v1/status')).services.map((s) => [s.id, s]));
+ assert.strictEqual(status.zines.label, 'up'); assert.strictEqual(status.zines.origin, 'https://openvibe.zines');
+ assert.strictEqual(status.almanac.label, 'up (loopback only)'); assert.strictEqual(status.almanac.origin, null);
+ assert.strictEqual(status.almanac.planned_origin, 'https://openvibe.almanac');
+
+ srv.close(); svc.close(); eco.stop && eco.stop();
+ console.log('registry manifest fixture: all checks passed');
+ process.exit(0);
+})().catch((err) => { console.error(err); process.exit(1); });