openvibers/openvibe.contracts Commits

v0.42.0: service manifests carry internalOrigin (the loopback address the registry polls), exposure (where the service can be reached today: state, publicSite, note, library package/repo) and site (name, icon, tagline, what, legalProfile, position, host), with the values Network hard-coded; ready for network, media and games; tests: every manifest has an exposure, running services unique loopback ports, libraries name package and repo, unique site positions (WS-C task 1)

OP OpenVibers committed 25 Sept 2026 · 3e7d5ee0a6 · 38 files, +724 −27


Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex a55e42e6..50849bb5 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,17 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
the notes were in the tag and commit messages (`git tag -n1`).
++## 0.42.0 — 2026-09-25
++
++The service manifests carry what Network's registry and site list used to hard-code (roadmap WS-C task 1, "manifest-derived registry"):
++
++- **`internalOrigin`**: the loopback address a running service answers on, which the registry polls. An `OV_<ID>_INTERNAL_URL` loopback override can still replace it.
++- **`exposure`**: where the service can be reached today. `state` is live, internal, library, repository, placeholder or retired; `publicSite` says what the public domain answers (the service, a placeholder, or nothing); there is an optional `note`; libraries also give `package` and `repo`. This is separate from `status` (maturity).
++- **`site`**: how a site is presented, with `name`, `icon`, `tagline`, `what`, `legalProfile` (the legal wording of /terms, /privacy, /dmca), `position`, and `host` for a site without a public origin. A service without `site` is not a site.
++- **`ready`** is added to the network, media and games manifests (`/api/ready`).
++
++The values are exactly what Network hard-coded. A service going public is now one manifest edit here. The test requires every first-party manifest to have an exposure, every running service to have a unique loopback port, libraries to name their package and repository, and site positions to be unique. The additions are optional in the schema, so this release is additive.
++
## 0.41.1 — 2026-09-25
Plain-language descriptions for `ai.preferences`, `chat.preferences`, `games.progress.summary` and `live.profile`. my.openvibe.network now shows these descriptions to people on its AI & Data tab. No schema change.
ddiff --git a/contracts/registry/service-manifest.v1.json b/contracts/registry/service-manifest.v1.json
iindex 63b11717..a3c4f7a6 100644
---- a/contracts/registry/service-manifest.v1.json
++++ b/contracts/registry/service-manifest.v1.json
@@@ -4,23 +4,194 @@
"title": "ServiceManifest",
"description": "What a service is, where it lives and what it offers. The ecosystem registry is built from these, not from hard-coded route maps.",
"type": "object",
-- "required": ["id", "version", "status", "repository", "domains", "capabilities", "eventsProduced", "eventsConsumed", "namespacesOwned"],
++ "required": [
++ "id",
++ "version",
++ "status",
++ "repository",
++ "domains",
++ "capabilities",
++ "eventsProduced",
++ "eventsConsumed",
++ "namespacesOwned"
++ ],
"additionalProperties": false,
"properties": {
-- "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,39}$" },
-- "name": { "type": "string" },
-- "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?$" },
-- "status": { "enum": ["placeholder", "alpha", "beta", "stable", "degraded", "retired"] },
-- "repository": { "type": "string", "pattern": "^OpenVibers/[A-Za-z0-9._-]+$" },
-- "domains": { "type": "array", "items": { "type": "string" } },
-- "publicOrigin": { "type": "string", "format": "uri" },
-- "health": { "type": "string", "pattern": "^/" },
-- "ready": { "type": "string", "pattern": "^/" },
-- "capabilities": { "type": "array", "items": { "type": "string" } },
-- "eventsProduced": { "type": "array", "items": { "type": "string" } },
-- "eventsConsumed": { "type": "array", "items": { "type": "string" } },
-- "namespacesOwned": { "type": "array", "items": { "type": "string" } },
-- "contractRanges": { "type": "object", "additionalProperties": { "type": "string" } },
-- "notes": { "type": "string" }
++ "id": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]{1,39}$"
++ },
++ "name": {
++ "type": "string"
++ },
++ "version": {
++ "type": "string",
++ "pattern": "^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?$"
++ },
++ "status": {
++ "enum": [
++ "placeholder",
++ "alpha",
++ "beta",
++ "stable",
++ "degraded",
++ "retired"
++ ]
++ },
++ "repository": {
++ "type": "string",
++ "pattern": "^OpenVibers/[A-Za-z0-9._-]+$"
++ },
++ "domains": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "publicOrigin": {
++ "type": "string",
++ "format": "uri"
++ },
++ "health": {
++ "type": "string",
++ "pattern": "^/"
++ },
++ "ready": {
++ "type": "string",
++ "pattern": "^/"
++ },
++ "internalOrigin": {
++ "type": "string",
++ "pattern": "^http://(127\\.0\\.0\\.1|localhost|\\[::1\\]):[0-9]{2,5}$",
++ "description": "Where the service answers on its host (loopback only): the address the registry polls health and readiness from. A host inventory or an OV_<ID>_INTERNAL_URL loopback override may replace it; a service with nothing to run has none."
++ },
++ "exposure": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "state"
++ ],
++ "description": "Where the service can actually be reached today, which is separate from its maturity (status): live (its public domain serves it), internal (loopback only; the domain, if any, still serves a placeholder), library (a released package), repository (code with CI, nothing released or run), placeholder (charter only) or retired. When a service goes public, this changes in the same release that points its domain at it.",
++ "properties": {
++ "state": {
++ "enum": [
++ "live",
++ "internal",
++ "library",
++ "repository",
++ "placeholder",
++ "retired"
++ ]
++ },
++ "publicSite": {
++ "enum": [
++ "service",
++ "placeholder",
++ null
++ ],
++ "description": "What the public domain answers today: the service itself, a placeholder page, or nothing (no public domain)."
++ },
++ "note": {
++ "type": "string",
++ "maxLength": 300
++ },
++ "package": {
++ "type": "string",
++ "pattern": "^[a-z0-9@/._-]+$",
++ "description": "For a library: the npm package name."
++ },
++ "repo": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9._-]+$",
++ "description": "For a library: the OpenVibers repository its release tags live in."
++ }
++ }
++ },
++ "site": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "name",
++ "icon",
++ "tagline",
++ "what",
++ "legalProfile"
++ ],
++ "description": "How the network presents the service as a site people visit (the navigation, the network home page, legal pages). A service without it is not a site.",
++ "properties": {
++ "name": {
++ "type": "string",
++ "maxLength": 40,
++ "description": "Short name after 'OpenVibe.' (or a full name containing a dot)."
++ },
++ "icon": {
++ "type": "string",
++ "pattern": "^[a-z0-9-]+$"
++ },
++ "tagline": {
++ "type": "string",
++ "maxLength": 60
++ },
++ "what": {
++ "type": "string",
++ "maxLength": 300
++ },
++ "legalProfile": {
++ "enum": [
++ "streaming",
++ "tools",
++ "ugc",
++ "games",
++ "hosting",
++ "account",
++ "info"
++ ],
++ "description": "Which legal wording its /terms, /privacy and /dmca use (openvibe-shared/legal)."
++ },
++ "position": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "Order in site lists."
++ },
++ "host": {
++ "type": "string",
++ "pattern": "^[a-z0-9.-]+$",
++ "description": "Only while the manifest has no publicOrigin."
++ }
++ }
++ },
++ "capabilities": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "eventsProduced": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "eventsConsumed": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "namespacesOwned": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "contractRanges": {
++ "type": "object",
++ "additionalProperties": {
++ "type": "string"
++ }
++ },
++ "notes": {
++ "type": "string"
++ }
}
}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex c90bcd89..ce9bf123 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -453,6 +453,106 @@
"type": "string",
"pattern": "^/"
},
++ "internalOrigin": {
++ "type": "string",
++ "pattern": "^http://(127\\.0\\.0\\.1|localhost|\\[::1\\]):[0-9]{2,5}$",
++ "description": "Where the service answers on its host (loopback only): the address the registry polls health and readiness from. A host inventory or an OV_<ID>_INTERNAL_URL loopback override may replace it; a service with nothing to run has none."
++ },
++ "exposure": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "state"
++ ],
++ "description": "Where the service can actually be reached today, which is separate from its maturity (status): live (its public domain serves it), internal (loopback only; the domain, if any, still serves a placeholder), library (a released package), repository (code with CI, nothing released or run), placeholder (charter only) or retired. When a service goes public, this changes in the same release that points its domain at it.",
++ "properties": {
++ "state": {
++ "enum": [
++ "live",
++ "internal",
++ "library",
++ "repository",
++ "placeholder",
++ "retired"
++ ]
++ },
++ "publicSite": {
++ "enum": [
++ "service",
++ "placeholder",
++ null
++ ],
++ "description": "What the public domain answers today: the service itself, a placeholder page, or nothing (no public domain)."
++ },
++ "note": {
++ "type": "string",
++ "maxLength": 300
++ },
++ "package": {
++ "type": "string",
++ "pattern": "^[a-z0-9@/._-]+$",
++ "description": "For a library: the npm package name."
++ },
++ "repo": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9._-]+$",
++ "description": "For a library: the OpenVibers repository its release tags live in."
++ }
++ }
++ },
++ "site": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "name",
++ "icon",
++ "tagline",
++ "what",
++ "legalProfile"
++ ],
++ "description": "How the network presents the service as a site people visit (the navigation, the network home page, legal pages). A service without it is not a site.",
++ "properties": {
++ "name": {
++ "type": "string",
++ "maxLength": 40,
++ "description": "Short name after 'OpenVibe.' (or a full name containing a dot)."
++ },
++ "icon": {
++ "type": "string",
++ "pattern": "^[a-z0-9-]+$"
++ },
++ "tagline": {
++ "type": "string",
++ "maxLength": 60
++ },
++ "what": {
++ "type": "string",
++ "maxLength": 300
++ },
++ "legalProfile": {
++ "enum": [
++ "streaming",
++ "tools",
++ "ugc",
++ "games",
++ "hosting",
++ "account",
++ "info"
++ ],
++ "description": "Which legal wording its /terms, /privacy and /dmca use (openvibe-shared/legal)."
++ },
++ "position": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "Order in site lists."
++ },
++ "host": {
++ "type": "string",
++ "pattern": "^[a-z0-9.-]+$",
++ "description": "Only while the manifest has no publicOrigin."
++ }
++ }
++ },
"capabilities": {
"type": "array",
"items": {
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex ca95ef2e..792b89fa 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -143,6 +143,53 @@ export interface ServiceManifest {
publicOrigin?: string;
health?: string;
ready?: string;
++ /**
++ * Where the service answers on its host (loopback only): the address the registry polls health and readiness from. A host inventory or an OV_<ID>_INTERNAL_URL loopback override may replace it; a service with nothing to run has none.
++ */
++ internalOrigin?: string;
++ /**
++ * Where the service can actually be reached today, which is separate from its maturity (status): live (its public domain serves it), internal (loopback only; the domain, if any, still serves a placeholder), library (a released package), repository (code with CI, nothing released or run), placeholder (charter only) or retired. When a service goes public, this changes in the same release that points its domain at it.
++ */
++ exposure?: {
++ state: "live" | "internal" | "library" | "repository" | "placeholder" | "retired";
++ /**
++ * What the public domain answers today: the service itself, a placeholder page, or nothing (no public domain).
++ */
++ publicSite?: "service" | "placeholder" | null;
++ note?: string;
++ /**
++ * For a library: the npm package name.
++ */
++ package?: string;
++ /**
++ * For a library: the OpenVibers repository its release tags live in.
++ */
++ repo?: string;
++ };
++ /**
++ * How the network presents the service as a site people visit (the navigation, the network home page, legal pages). A service without it is not a site.
++ */
++ site?: {
++ /**
++ * Short name after 'OpenVibe.' (or a full name containing a dot).
++ */
++ name: string;
++ icon: string;
++ tagline: string;
++ what: string;
++ /**
++ * Which legal wording its /terms, /privacy and /dmca use (openvibe-shared/legal).
++ */
++ legalProfile: "streaming" | "tools" | "ugc" | "games" | "hosting" | "account" | "info";
++ /**
++ * Order in site lists.
++ */
++ position?: number;
++ /**
++ * Only while the manifest has no publicOrigin.
++ */
++ host?: string;
++ };
capabilities: string[];
eventsProduced: string[];
eventsConsumed: string[];
ddiff --git a/manifests/services/ai.json b/manifests/services/ai.json
iindex fa91e47c..89b5c0b1 100644
---- a/manifests/services/ai.json
++++ b/manifests/services/ai.json
@@@ -13,6 +13,12 @@
"publicOrigin": "https://ai.openvibe.network",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4700",
++ "exposure": {
++ "state": "internal",
++ "publicSite": null,
++ "note": "called by other services at 127.0.0.1:4700; ai.openvibe.network serves the OpenVibe.Sites placeholder page"
++ },
"capabilities": [
"ai.run.create",
"ai.run.read",
@@@ -27,6 +33,8 @@
"ai.run.cached"
],
"eventsConsumed": [],
-- "namespacesOwned": ["ai.usage_summary"],
++ "namespacesOwned": [
++ "ai.usage_summary"
++ ],
"notes": "Providers, models, routing profiles, versioned prompt templates and workflows, runs, request logs, citations, scoped cache, quotas and audit. Workflow output is a draft/evidence package attributed to workflow + model + run. ai.run.queued|succeeded|failed|cached have payload contracts (v0.30.0, status planned): AI publishes them once its Events client is wired in; it publishes none today. ai.run.create takes ai.run-request@1 and returns ai.run@1."
}
ddiff --git a/manifests/services/billing.json b/manifests/services/billing.json
iindex 998f024a..51109d70 100644
---- a/manifests/services/billing.json
++++ b/manifests/services/billing.json
@@@ -13,6 +13,12 @@
"publicOrigin": "https://billing.openvibe.network",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4600",
++ "exposure": {
++ "state": "live",
++ "publicSite": "service",
++ "note": "the public domain is the staff console; accounts pay through each product"
++ },
"capabilities": [
"billing.intent.create",
"billing.transfer.create",
ddiff --git a/manifests/services/blog.json b/manifests/services/blog.json
iindex c7e7b3bb..aa21a554 100644
---- a/manifests/services/blog.json
++++ b/manifests/services/blog.json
@@@ -13,6 +13,19 @@
"publicOrigin": "https://openvibe.blog",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4810",
++ "exposure": {
++ "state": "live",
++ "publicSite": "service"
++ },
++ "site": {
++ "name": "Blog",
++ "icon": "blog",
++ "tagline": "Long-form, yours to keep",
++ "what": "The official OpenVibe blog and a blog for every member: drafts, scheduling, media, comments and feeds.",
++ "legalProfile": "info",
++ "position": 8
++ },
"capabilities": [
"blog.blog.create",
"blog.blog.configure",
ddiff --git a/manifests/services/chat.json b/manifests/services/chat.json
iindex 18af23ee..0e416543 100644
---- a/manifests/services/chat.json
++++ b/manifests/services/chat.json
@@@ -7,7 +7,9 @@
"name": "OpenVibe.Chat",
"status": "alpha",
"repository": "OpenVibers/OpenVibe.Chat",
-- "domains": ["openvibe.chat"],
++ "domains": [
++ "openvibe.chat"
++ ],
"publicOrigin": "https://openvibe.chat",
"capabilities": [
"chat.live_bridge.write",
@@@ -23,8 +25,27 @@
"eventsConsumed": [
"network.user.token_valid_after"
],
-- "namespacesOwned": ["chat.preferences", "chat.tts_defaults", "chat.dm_settings", "chat.presence_prefs"],
++ "namespacesOwned": [
++ "chat.preferences",
++ "chat.tts_defaults",
++ "chat.dm_settings",
++ "chat.presence_prefs"
++ ],
"notes": "Wave 6: Live's chat (WebSocket, DMs, history, TTS, sounds, moderation) behind a service boundary on 127.0.0.1:4400, serving Live's chat paths since 2026-09-23. Since 2026-09-24 also its own site, openvibe.chat (global chat, messages, settings, sign-in with the Network), which works while Live is down.",
"health": "/health",
-- "ready": "/ready"
++ "ready": "/ready",
++ "internalOrigin": "http://127.0.0.1:4400",
++ "exposure": {
++ "state": "live",
++ "publicSite": "service",
++ "note": "openvibe.chat since 2026-09-24 (global chat, messages, settings); also serves Live's chat paths"
++ },
++ "site": {
++ "name": "Chat",
++ "icon": "chat",
++ "tagline": "One chat for the network",
++ "what": "Global chat, rooms, stream chat and direct messages under one account, with or without JavaScript.",
++ "legalProfile": "ugc",
++ "position": 6
++ }
}
ddiff --git a/manifests/services/codes.json b/manifests/services/codes.json
iindex 47dcf679..ee9f08a5 100644
---- a/manifests/services/codes.json
++++ b/manifests/services/codes.json
@@@ -13,6 +13,19 @@
"publicOrigin": "https://openvibe.codes",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4900",
++ "exposure": {
++ "state": "live",
++ "publicSite": "service"
++ },
++ "site": {
++ "name": "Codes",
++ "icon": "codes",
++ "tagline": "Build on OpenVibe",
++ "what": "The developer portal: API and SDK docs, the service and capability registry, event schemas, credentials, playgrounds and mod publishing.",
++ "legalProfile": "ugc",
++ "position": 7
++ },
"capabilities": [
"codes.release.read",
"codes.release.manage"
ddiff --git a/manifests/services/community.json b/manifests/services/community.json
iindex 43f26ae9..ae60d1ba 100644
---- a/manifests/services/community.json
++++ b/manifests/services/community.json
@@@ -41,5 +41,18 @@
"namespacesOwned": [
"community.*"
],
-- "ready": "/api/ready"
++ "ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4200",
++ "exposure": {
++ "state": "live",
++ "publicSite": "service"
++ },
++ "site": {
++ "name": "Community",
++ "icon": "community",
++ "tagline": "Pastes and posts",
++ "what": "Share text and code with a link, comment, and follow what people on OpenVibe are making.",
++ "legalProfile": "ugc",
++ "position": 2
++ }
}
ddiff --git a/manifests/services/contracts.json b/manifests/services/contracts.json
iindex 9436981c..da28f920 100644
---- a/manifests/services/contracts.json
++++ b/manifests/services/contracts.json
@@@ -7,6 +7,13 @@
"name": "OpenVibe.Contracts",
"status": "alpha",
"repository": "OpenVibers/OpenVibe.Contracts",
++ "exposure": {
++ "state": "library",
++ "publicSite": null,
++ "note": "schemas are also served by this registry at their $id URLs",
++ "package": "openvibe-contracts",
++ "repo": "OpenVibe.Contracts"
++ },
"domains": [],
"capabilities": [],
"eventsProduced": [],
ddiff --git a/manifests/services/coupons.json b/manifests/services/coupons.json
iindex ee4ea35c..da16e4b0 100644
---- a/manifests/services/coupons.json
++++ b/manifests/services/coupons.json
@@@ -13,6 +13,20 @@
"publicOrigin": "https://openvibe.coupons",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4850",
++ "exposure": {
++ "state": "internal",
++ "publicSite": "placeholder",
++ "note": "the domain serves the OpenVibe.Sites placeholder page"
++ },
++ "site": {
++ "name": "Coupons",
++ "icon": "coupons",
++ "tagline": "Codes that work",
++ "what": "Coupon codes with merchant matching, restrictions, expiry and real-people validity reports.",
++ "legalProfile": "info",
++ "position": 17
++ },
"capabilities": [
"coupons.coupon.submit",
"coupons.report.create",
ddiff --git a/manifests/services/deals.json b/manifests/services/deals.json
iindex 034585fd..21b8a426 100644
---- a/manifests/services/deals.json
++++ b/manifests/services/deals.json
@@@ -13,6 +13,20 @@
"publicOrigin": "https://openvibe.deals",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4840",
++ "exposure": {
++ "state": "internal",
++ "publicSite": "placeholder",
++ "note": "the domain serves the OpenVibe.Sites placeholder page"
++ },
++ "site": {
++ "name": "Deals",
++ "icon": "deals",
++ "tagline": "Deals worth sharing",
++ "what": "Deals submitted and voted on by the community, with source, price and freshness always shown.",
++ "legalProfile": "info",
++ "position": 16
++ },
"capabilities": [
"deals.offer.submit",
"deals.offer.update",
ddiff --git a/manifests/services/events.json b/manifests/services/events.json
iindex 3100c5af..fafcab58 100644
---- a/manifests/services/events.json
++++ b/manifests/services/events.json
@@@ -13,6 +13,11 @@
"publicOrigin": "https://events.openvibe.network",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4300",
++ "exposure": {
++ "state": "live",
++ "publicSite": "service"
++ },
"capabilities": [
"events.event.publish",
"events.event.read",
ddiff --git a/manifests/services/examples.json b/manifests/services/examples.json
iindex 851e9427..d9623f73 100644
---- a/manifests/services/examples.json
++++ b/manifests/services/examples.json
@@@ -7,6 +7,11 @@
"name": "OpenVibe.Examples",
"status": "alpha",
"repository": "OpenVibers/OpenVibe.Examples",
++ "exposure": {
++ "state": "repository",
++ "publicSite": null,
++ "note": "example apps with CI in OpenVibers/OpenVibe.Examples; not published as a package"
++ },
"domains": [],
"capabilities": [],
"eventsProduced": [],
ddiff --git a/manifests/services/games.json b/manifests/services/games.json
iindex c06a0529..dfd59581 100644
---- a/manifests/services/games.json
++++ b/manifests/services/games.json
@@@ -7,9 +7,26 @@
"name": "OpenVibe.Games",
"status": "stable",
"repository": "OpenVibers/OpenVibe.Games",
-- "domains": ["openvibe.games", "play.openvibe.games"],
++ "domains": [
++ "openvibe.games",
++ "play.openvibe.games"
++ ],
"publicOrigin": "https://openvibe.games",
"health": "/healthz",
++ "ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:8000",
++ "exposure": {
++ "state": "live",
++ "publicSite": "service"
++ },
++ "site": {
++ "name": "Games",
++ "icon": "games",
++ "tagline": "Browser games",
++ "what": "Scraplandia, a shared pixel canvas and other games that run in a tab and know your OpenVibe account.",
++ "legalProfile": "games",
++ "position": 3
++ },
"capabilities": [
"games.mod.manage",
"games.mod.read",
@@@ -28,7 +45,11 @@
"games.mod.grants_changed",
"games.mod.revoked"
],
-- "eventsConsumed": ["network.user.token_valid_after"],
-- "namespacesOwned": ["games.*"],
++ "eventsConsumed": [
++ "network.user.token_valid_after"
++ ],
++ "namespacesOwned": [
++ "games.*"
++ ],
"notes": "Scraplandia (browser runtime). Players are keyed by canonical Network subjects (ADR-0006 in the repo). Calls Events (events.event.publish), Media (media.object.upload, namespace games: map-editor assets) and Network identity (identity.subject.resolve, batch legacy adoption) with the games client-credentials principal. Mods: games-content@1 declarative packs only; executable mods wait for OpenVibe.Host Stage C. No achievement system exists, so there are no games.achievement.* events."
}
ddiff --git a/manifests/services/host.json b/manifests/services/host.json
iindex 82426482..b4477845 100644
---- a/manifests/services/host.json
++++ b/manifests/services/host.json
@@@ -13,6 +13,20 @@
"publicOrigin": "https://openvibe.host",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4910",
++ "exposure": {
++ "state": "internal",
++ "publicSite": "placeholder",
++ "note": "the domain serves the OpenVibe.Sites placeholder page"
++ },
++ "site": {
++ "name": "Host",
++ "icon": "host",
++ "tagline": "Deploy and host",
++ "what": "The network's deployment plane first, then simple hosting for community sites, bots and mods.",
++ "legalProfile": "hosting",
++ "position": 15
++ },
"capabilities": [
"host.site.manage",
"host.deploy.create",
ddiff --git a/manifests/services/live.json b/manifests/services/live.json
iindex 9f577cb6..9d3b146b 100644
---- a/manifests/services/live.json
++++ b/manifests/services/live.json
@@@ -14,6 +14,19 @@
"publicOrigin": "https://openvibe.live",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:3000",
++ "exposure": {
++ "state": "live",
++ "publicSite": "service"
++ },
++ "site": {
++ "name": "Live",
++ "icon": "live",
++ "tagline": "Streams, clips and chat",
++ "what": "Live streaming from a browser, OBS or a phone, with restreaming, clips, VODs, chat games and channel points.",
++ "legalProfile": "streaming",
++ "position": 0
++ },
"capabilities": [
"live.chat_context.read",
"live.chat_effects.write",
ddiff --git a/manifests/services/media.json b/manifests/services/media.json
iindex 00395c89..26460a57 100644
---- a/manifests/services/media.json
++++ b/manifests/services/media.json
@@@ -12,6 +12,20 @@
],
"publicOrigin": "https://openvibe.media",
"health": "/healthz",
++ "ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4100",
++ "exposure": {
++ "state": "live",
++ "publicSite": "service"
++ },
++ "site": {
++ "name": "Media",
++ "icon": "media",
++ "tagline": "VODs, clips, files",
++ "what": "The public media library behind every site: recorded streams, clips, images and uploads.",
++ "legalProfile": "hosting",
++ "position": 4
++ },
"capabilities": [
"media.object.upload",
"media.object.read",
ddiff --git a/manifests/services/network.json b/manifests/services/network.json
iindex 67fb85ba..4bfb567a 100644
---- a/manifests/services/network.json
++++ b/manifests/services/network.json
@@@ -17,6 +17,20 @@
],
"publicOrigin": "https://openvibe.network",
"health": "/api/health",
++ "ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4000",
++ "exposure": {
++ "state": "live",
++ "publicSite": "service"
++ },
++ "site": {
++ "name": "Network",
++ "icon": "network",
++ "tagline": "Account and themes",
++ "what": "The account behind every site: one sign-in, themes, notifications, history and linked services.",
++ "legalProfile": "account",
++ "position": 5
++ },
"capabilities": [
"identity.subject.resolve",
"mods.grant.manage",
ddiff --git a/manifests/services/news.json b/manifests/services/news.json
iindex 2302f79d..9e35f54d 100644
---- a/manifests/services/news.json
++++ b/manifests/services/news.json
@@@ -13,6 +13,20 @@
"publicOrigin": "https://openvibe.news",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4820",
++ "exposure": {
++ "state": "internal",
++ "publicSite": "placeholder",
++ "note": "the domain serves the OpenVibe.Sites placeholder page"
++ },
++ "site": {
++ "name": "News",
++ "icon": "news",
++ "tagline": "Sourced, not spun",
++ "what": "Source-backed stories: clustered coverage, cited summaries, multiple perspectives and timelines.",
++ "legalProfile": "info",
++ "position": 10
++ },
"capabilities": [
"news.story.create",
"news.story.read",
ddiff --git a/manifests/services/openre.json b/manifests/services/openre.json
iindex 070d0cc0..d1c6e4f0 100644
---- a/manifests/services/openre.json
++++ b/manifests/services/openre.json
@@@ -14,6 +14,20 @@
"publicOrigin": "https://openre.stream",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4500",
++ "exposure": {
++ "state": "internal",
++ "publicSite": "placeholder",
++ "note": "the domain serves the OpenVibe.Sites placeholder page"
++ },
++ "site": {
++ "name": "OpenRe.Stream",
++ "icon": "stream",
++ "tagline": "Restream anywhere",
++ "what": "Ingest once and send the stream to every platform at once; keys, destinations and output health in one place.",
++ "legalProfile": "streaming",
++ "position": 18
++ },
"capabilities": [
"openre.stream.read",
"openre.stream.write",
ddiff --git a/manifests/services/publishing.json b/manifests/services/publishing.json
iindex 4a99e762..44542191 100644
---- a/manifests/services/publishing.json
++++ b/manifests/services/publishing.json
@@@ -4,6 +4,13 @@
"name": "OpenVibe.Publishing",
"status": "alpha",
"repository": "OpenVibers/OpenVibe.Publishing",
++ "exposure": {
++ "state": "library",
++ "publicSite": null,
++ "note": "the shared publishing runtime used by Wiki, Blog, News, Reviews, Deals, Coupons and Trade",
++ "package": "openvibe-publishing",
++ "repo": "OpenVibe.Publishing"
++ },
"domains": [],
"capabilities": [],
"eventsProduced": [],
ddiff --git a/manifests/services/realtime.json b/manifests/services/realtime.json
iindex bf9abc33..ac245583 100644
---- a/manifests/services/realtime.json
++++ b/manifests/services/realtime.json
@@@ -7,6 +7,10 @@
"name": "OpenVibe.Realtime",
"status": "retired",
"repository": "OpenVibers/OpenVibe.Realtime",
++ "exposure": {
++ "state": "placeholder",
++ "publicSite": null
++ },
"domains": [],
"capabilities": [],
"eventsProduced": [],
ddiff --git a/manifests/services/reviews.json b/manifests/services/reviews.json
iindex a541ba15..5f623e7d 100644
---- a/manifests/services/reviews.json
++++ b/manifests/services/reviews.json
@@@ -13,6 +13,20 @@
"publicOrigin": "https://openvibe.reviews",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4830",
++ "exposure": {
++ "state": "internal",
++ "publicSite": "placeholder",
++ "note": "the domain serves the OpenVibe.Sites placeholder page"
++ },
++ "site": {
++ "name": "Reviews",
++ "icon": "reviews",
++ "tagline": "Reviews with receipts",
++ "what": "Review signals gathered across sources with provenance, pros and cons, and community discussion. No invented ratings.",
++ "legalProfile": "ugc",
++ "position": 11
++ },
"capabilities": [
"reviews.entity.resolve",
"reviews.entity.manage",
ddiff --git a/manifests/services/sdk.json b/manifests/services/sdk.json
iindex 74b0a51d..f4dffae6 100644
---- a/manifests/services/sdk.json
++++ b/manifests/services/sdk.json
@@@ -4,6 +4,12 @@
"name": "OpenVibe.SDK",
"status": "alpha",
"repository": "OpenVibers/OpenVibe.SDK",
++ "exposure": {
++ "state": "library",
++ "publicSite": null,
++ "package": "openvibe-sdk",
++ "repo": "OpenVibe.SDK"
++ },
"domains": [],
"capabilities": [],
"eventsProduced": [],
ddiff --git a/manifests/services/search.json b/manifests/services/search.json
iindex 68079ba8..86a2f02b 100644
---- a/manifests/services/search.json
++++ b/manifests/services/search.json
@@@ -13,6 +13,12 @@
"publicOrigin": "https://search.openvibe.network",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4710",
++ "exposure": {
++ "state": "live",
++ "publicSite": "service",
++ "note": "public search page and query API at search.openvibe.network (since 2026-09-23)"
++ },
"capabilities": [
"search.document.write",
"search.query.delegate",
ddiff --git a/manifests/services/shared.json b/manifests/services/shared.json
iindex 352f46f1..5ff25e0b 100644
---- a/manifests/services/shared.json
++++ b/manifests/services/shared.json
@@@ -4,6 +4,13 @@
"name": "OpenVibe.Shared",
"status": "alpha",
"repository": "OpenVibers/OpenVibe.Shared",
++ "exposure": {
++ "state": "library",
++ "publicSite": null,
++ "note": "also served at /shared/* by each site",
++ "package": "openvibe-shared",
++ "repo": "OpenVibe.Shared"
++ },
"domains": [],
"capabilities": [],
"eventsProduced": [],
ddiff --git a/manifests/services/sites.json b/manifests/services/sites.json
iindex d7a4ee13..bad5875b 100644
---- a/manifests/services/sites.json
++++ b/manifests/services/sites.json
@@@ -7,6 +7,11 @@
"name": "OpenVibe.Sites",
"status": "stable",
"repository": "OpenVibers/OpenVibe.Sites",
++ "exposure": {
++ "state": "live",
++ "publicSite": "service",
++ "note": "serves the placeholder pages of domains whose service is not public yet"
++ },
"domains": [],
"capabilities": [],
"eventsProduced": [],
ddiff --git a/manifests/services/sources.json b/manifests/services/sources.json
iindex a488c7f2..7b914d1c 100644
---- a/manifests/services/sources.json
++++ b/manifests/services/sources.json
@@@ -13,6 +13,12 @@
"publicOrigin": "https://sources.openvibe.network",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4720",
++ "exposure": {
++ "state": "internal",
++ "publicSite": null,
++ "note": "internal ingestion service; sources.openvibe.network says so (its API is loopback-only)"
++ },
"capabilities": [
"sources.source.read",
"sources.item.read",
ddiff --git a/manifests/services/tips.json b/manifests/services/tips.json
iindex b324632f..e5018219 100644
---- a/manifests/services/tips.json
++++ b/manifests/services/tips.json
@@@ -13,6 +13,20 @@
"publicOrigin": "https://openvibe.tips",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4610",
++ "exposure": {
++ "state": "internal",
++ "publicSite": "placeholder",
++ "note": "the domain serves the OpenVibe.Sites placeholder page"
++ },
++ "site": {
++ "name": "Tips",
++ "icon": "tips",
++ "tagline": "Support creators",
++ "what": "Tips, goals, paid messages and alerts for creators, settled by the network's billing ledger.",
++ "legalProfile": "streaming",
++ "position": 12
++ },
"capabilities": [
"tips.profile.get",
"tips.profile.update",
ddiff --git a/manifests/services/tools.json b/manifests/services/tools.json
iindex e1e228b2..b9d0f5f7 100644
---- a/manifests/services/tools.json
++++ b/manifests/services/tools.json
@@@ -13,6 +13,19 @@
"publicOrigin": "https://openvibe.tools",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4001",
++ "exposure": {
++ "state": "live",
++ "publicSite": "service"
++ },
++ "site": {
++ "name": "Tools",
++ "icon": "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.",
++ "legalProfile": "tools",
++ "position": 1
++ },
"capabilities": [
"tools.job.create",
"tools.job.read",
ddiff --git a/manifests/services/trade.json b/manifests/services/trade.json
iindex f79a9f3e..7d02ce80 100644
---- a/manifests/services/trade.json
++++ b/manifests/services/trade.json
@@@ -13,6 +13,20 @@
"publicOrigin": "https://openvibe.trade",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4860",
++ "exposure": {
++ "state": "internal",
++ "publicSite": "placeholder",
++ "note": "the domain serves the OpenVibe.Sites placeholder page"
++ },
++ "site": {
++ "name": "Trade",
++ "icon": "trade",
++ "tagline": "Watchlists and alerts",
++ "what": "Informational watchlists, sourced market context and alerts. No custody, no order execution.",
++ "legalProfile": "ugc",
++ "position": 14
++ },
"capabilities": [
"trade.instrument.resolve",
"trade.instrument.manage",
ddiff --git a/manifests/services/vip.json b/manifests/services/vip.json
iindex afb3a303..f0e61325 100644
---- a/manifests/services/vip.json
++++ b/manifests/services/vip.json
@@@ -13,6 +13,20 @@
"publicOrigin": "https://openvibe.vip",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4620",
++ "exposure": {
++ "state": "internal",
++ "publicSite": "placeholder",
++ "note": "the domain serves the OpenVibe.Sites placeholder page"
++ },
++ "site": {
++ "name": "VIP",
++ "icon": "vip",
++ "tagline": "Memberships and perks",
++ "what": "Creator and network memberships: plans, perks and benefits recognised on every OpenVibe site.",
++ "legalProfile": "account",
++ "position": 13
++ },
"capabilities": [
"vip.plan.create",
"vip.plan.update",
ddiff --git a/manifests/services/wiki.json b/manifests/services/wiki.json
iindex d31c9650..bfd5975a 100644
---- a/manifests/services/wiki.json
++++ b/manifests/services/wiki.json
@@@ -13,6 +13,19 @@
"publicOrigin": "https://openvibe.wiki",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4800",
++ "exposure": {
++ "state": "live",
++ "publicSite": "service"
++ },
++ "site": {
++ "name": "Wiki",
++ "icon": "wiki",
++ "tagline": "Knowledge, with sources",
++ "what": "Wiki spaces with page trees, revision history, citations and discussion, editable together.",
++ "legalProfile": "ugc",
++ "position": 9
++ },
"capabilities": [
"wiki.space.create",
"wiki.page.create",
ddiff --git a/package-lock.json b/package-lock.json
iindex e5a91716..d3a5739a 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-contracts",
-- "version": "0.41.1",
++ "version": "0.42.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-contracts",
-- "version": "0.41.1",
++ "version": "0.42.0",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 5e8323cb..598fcc08 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.41.1",
++ "version": "0.42.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"types": "generated/typescript/index.d.ts",
ddiff --git a/test/run.js b/test/run.js
iindex 32d20ca8..ca42d9cd 100644
---- a/test/run.js
++++ b/test/run.js
@@@ -546,6 +546,28 @@ client.invalidate(); await client.getToken(); ok(calls === 2, 'invalidate forces
const failing = serviceAuth.createTokenClient({ tokenUrl: 'x', clientId: 'live', clientSecret: 'bad', audience: 'a', fetchImpl: async () => ({ ok: false, status: 401, json: async () => ({ error: 'invalid_client' }) }) });
await assert.rejects(failing.getToken(), /401: invalid_client/);
++// ── Manifest-derived registry (WS-C task 1): addresses, exposure, sites ──
++{
++ const ms = services.manifests;
++ const ports = new Map();
++ for (const m of ms) {
++ ok(m.exposure && typeof m.exposure.state === 'string', `${m.id} says where it can be reached (exposure)`);
++ const runs = ['live', 'internal'].includes(m.exposure.state) && !!m.ready;
++ if (runs) ok(typeof m.internalOrigin === 'string', `${m.id} runs: it names its loopback address (internalOrigin)`);
++ if (m.internalOrigin) {
++ const port = new URL(m.internalOrigin).port;
++ ok(!ports.has(port), `${m.id} internalOrigin port ${port} is its own${ports.has(port) ? ` (also ${ports.get(port)})` : ''}`);
++ ports.set(port, m.id);
++ }
++ if (m.exposure.state === 'library') ok(m.exposure.package && m.exposure.repo, `${m.id} library names its package and repository`);
++ if (m.exposure.state === 'live' && m.exposure.publicSite === 'service') ok(!!m.publicOrigin || (m.domains || []).length > 0 || !!m.exposure.note, `${m.id} is live: it has a public origin, its domains, or a note saying where`);
++ if (m.site) ok(!!(m.publicOrigin || m.site.host), `${m.id} site has a host`);
++ }
++ const positions = ms.filter(m => m.site).map(m => m.site.position);
++ ok(new Set(positions).size === positions.length, 'site positions are unique');
++ ok(ms.filter(m => m.site).length >= 10, 'the network\'s sites are in the manifests');
++}
++
// ── User-module namespaces ────────────────────────────────────────────────
{
const { modules } = contracts;