openvibers/openvibe.network Commits
Network: the developer console moves to OpenVibe.Services; Codes becomes the coding-agent harness (contracts v0.113.0) (#81)
Changes
@@@ -771,7 +771,7 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md). The rules the code keeps:Same as the parent [OpenVibe](../LICENSE) project.<!-- versions:start -->--- openvibe-contracts: v0.112.0++- openvibe-contracts: v0.113.0- openvibe-sdk: v0.35.1- openvibe-shared: v2.13.2<!-- versions:end -->@@@ -6,7 +6,7 @@"runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9","port": 4000,"domain": "openvibe.network (with auth., api., admin., my. and themes.openvibe.network in its manifest)",-- "contracts": "openvibe-contracts v0.112.0",++ "contracts": "openvibe-contracts v0.113.0","sdk": "openvibe-sdk v0.35.1","shared": "openvibe-shared v2.13.2","capabilities": [nnew file mode 100644@@@ -0,0 +1,10 @@++-- phase: migrate++-- The developer portal moved from OpenVibe.Codes to OpenVibe.Services (owner decision 2026-10-08; openvibe-contracts++-- 0.113.0 retired codes.release.read|manage for services.release.read|manage, audience openvibe.services). An app's++-- grant moves with it: same app, same status and history, the new capability and audience. Production held 17 approved++-- codes.release.manage grants (sandbox apps from the developer path) and no services.release.* row, so nothing++-- collides. Rollback: the reverse UPDATE (a Network release from before this one refuses the new names as unknown).++UPDATE dev_grants SET capability = 'services.release.manage', audience = 'openvibe.services'++ WHERE capability = 'codes.release.manage';++UPDATE dev_grants SET capability = 'services.release.read', audience = 'openvibe.services'++ WHERE capability = 'codes.release.read';@@@ -21,7 +21,7 @@"iovalkey": "^0.4.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.112.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.113.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.1","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.13.2","pg": "^8.23.0",@@@ -1556,9 +1556,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.112.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.112.0",-- "integrity": "sha512-ujIdCjv+oHfwg/+Hw0tyt2erVnornjDiKVPN8snHvybfysR/DBZQ2G32bQj8oAaK90s12ADEpdrXT1eust3eAw==",++ "version": "0.113.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.113.0",++ "integrity": "sha512-HdSr3C27r29ciHabb8cFqqZ0658Ki3zbYSz1461xjO4knktweGgJsefJnhGTx7iQBbbJ7XDCAUoTnvouuotNYg==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -1,7 +1,7 @@{"name": "openvibe-network","version": "1.0.0",-- "description": "OpenVibe.Network \u2014 Identity, SSO, Accounts, Themes, Notifications, and OpenCoins Wallet",++ "description": "OpenVibe.Network — Identity, SSO, Accounts, Themes, Notifications, and OpenCoins Wallet","main": "server/index.js","scripts": {"start": "node server/index.js",@@@ -30,7 +30,7 @@"iovalkey": "^0.4.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.112.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.113.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.1","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.13.2","pg": "^8.23.0",@@@ -11,7 +11,7 @@ const cache = require('openvibe-shared/cache-policy');* tips.interaction.moderated a paid message filtered, held, hidden or restored* billing.staff.action a staff money action in the Billing console* <service>.moderation.action common.moderation-action@1 (Contracts 0.53.0) from tools, games, wiki,-- * blog, news, reviews, deals, coupons, trade and codes: a staff or++ * blog, news, reviews, deals, coupons, trade and services: a staff or* moderator action on someone else's content there; the service is the* event's prefix*@@@ -26,7 +26,7 @@ const { staff } = require('openvibe-contracts');const { staffClaims } = require('../auth/staff-claims');// Producers whose event is common.moderation-action@1 (action, target { type, id, owner_subject }, actor_subject, reason, details).--const COMMON_SERVICES = Object.freeze(['media', 'tools', 'games', 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes']);++const COMMON_SERVICES = Object.freeze(['media', 'tools', 'games', 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade', 'services']);const TOPICS = Object.freeze(['chat.moderation.action', 'live.moderation.action', 'community.moderation.action', 'space.moderation.action', 'tips.interaction.moderated', 'billing.staff.action',...COMMON_SERVICES.map((svc) => `${svc}.moderation.action`)]);const str = (v, n) => (v == null || v === '' ? null : String(v).replace(/[\u0000-\u001f\u007f]/g, ' ').slice(0, n));@@@ -71,7 +71,7 @@ function rowOf(event) {case 'deals.moderation.action':case 'coupons.moderation.action':case 'trade.moderation.action':-- case 'codes.moderation.action': {++ case 'services.moderation.action': {const t = obj(p.target);return { ...base, service: event.event_type.split('.')[0], action: str(p.action, 64) || 'unknown', actor_subject: str(p.actor_subject || actorId, 64),target_type: str(t.type, 40), target_id: str(t.id, 200), target_subject: str(t.owner_subject, 64), scope: null, reason: str(p.reason, 500), details: json(p.details) };@@@ -43,11 +43,11 @@ const AREAS = [{ item: 'Space forum moderation', where: [{ kind: 'api', api: 'OpenVibe.Space forum moderator routes', note: 'no console yet' }], service: 'space' },{ item: 'Payments, cashouts and the money freeze', where: [{ kind: 'url', url: 'https://billing.openvibe.network/', note: 'Billing console' }, { kind: 'tab', tab: 'cashouts' }, { kind: 'tab', tab: 'payments' }], service: 'billing' },{ item: 'Loyalty (OpenCoins, channel points)', where: [{ kind: 'api', api: 'live.loyalty user module; Network /internal/coins/*', note: 'summaries only; loyalty is never money' }], service: 'network' },-- { item: 'Developer apps and trust (Codes)', where: [{ kind: 'url', url: 'https://openvibe.codes/staff' }], service: 'codes' },++ { item: 'Developer apps and trust (Services)', where: [{ kind: 'url', url: 'https://openvibe.services/staff' }], service: 'services' },] },{ area: 'Releases and readiness', items: [{ item: 'Migration and readiness', where: [{ kind: 'url', url: 'https://openvibe.network/status' }, { kind: 'cli', cli: 'ovhost status, ovhost validate <service>' }], service: 'host' },-- { item: 'Compatibility status (contracts, pins, the register)', where: [{ kind: 'url', url: 'https://openvibe.codes/docs' }, { kind: 'cli', cli: 'ovhost validate <service> (contracts range)' }], service: 'codes' },++ { item: 'Compatibility status (contracts, pins, the register)', where: [{ kind: 'url', url: 'https://openvibe.services/docs' }, { kind: 'cli', cli: 'ovhost validate <service> (contracts range)' }], service: 'codes' },{ item: 'Deploys', where: [{ kind: 'tab', tab: 'deploy' }, { kind: 'cli', cli: 'ovhost deploy <service>' }], service: 'host' },{ item: 'Domains and certificates', where: [{ kind: 'tab', tab: 'domains' }, { kind: 'cli', cli: 'ovhost certs' }], service: 'host' },] },@@@ -103,6 +103,8 @@ async function seedDb(db, { log = console } = {}) {{ client_id: 'coupons', name: 'OpenVibe.Coupons', redirect_uris: ['https://openvibe.coupons/auth/callback'] },{ client_id: 'host', name: 'OpenVibe.Host', redirect_uris: ['https://openvibe.host/auth/callback'] },{ client_id: 'codes', name: 'OpenVibe.Codes', redirect_uris: ['https://openvibe.codes/auth/callback'] },++ // The developer console (moved from openvibe.codes on 2026-10-08); the client already exists as Services' principal.++ { client_id: 'services', name: 'OpenVibe.Services', redirect_uris: ['https://openvibe.services/auth/callback'] },{ client_id: 'billing', name: 'OpenVibe.Billing', redirect_uris: ['https://billing.openvibe.network/auth/callback'] },{ client_id: 'ai', name: 'OpenVibe.AI', redirect_uris: ['https://ai.openvibe.services/auth/callback'] },{ client_id: 'bot', name: 'OpenVibe.Bot', redirect_uris: ['https://openvibe.bot/auth/callback'] },@@@ -52,7 +52,7 @@ function grantableCatalog() {* sandbox runs on free and local capacity only (ai.app.run, plan T6). openvibe.network is deliberately* not one of them.*/--const DEFAULT_SANDBOX_AUDIENCES = Object.freeze(['openvibe.media', 'openvibe.events', 'openvibe.tools', 'openvibe.codes', 'openvibe.ai']);++const DEFAULT_SANDBOX_AUDIENCES = Object.freeze(['openvibe.media', 'openvibe.events', 'openvibe.tools', 'openvibe.services', 'openvibe.ai']);/*** Capabilities every project's SANDBOX apps may hold without a staff decision when@@@ -66,7 +66,7 @@ const DEFAULT_SANDBOX_ALLOWANCE = Object.freeze(['media.object.upload', 'media.object.read', 'media.object.list', 'media.object.delete','events.app.publish', 'events.app.read', 'events.app.subscribe','tools.job.create', 'tools.job.read', 'tools.job.cancel', 'tools.tool.read', 'tools.tool.run',-- 'codes.release.manage',++ 'services.release.manage','ai.app.run',]);@@@ -45,7 +45,8 @@ const INTENTS = [['games', 'fa-gamepad', 'Play', 'Browser games that already know you.', 'https://openvibe.games/', 'play game games gaming multiplayer'],['blog', 'fa-pen-nib', 'Write', 'A blog of your own, with drafts, scheduling and feeds.', 'https://openvibe.blog/write', 'write blog post posts article publish newsletter'],['wiki', 'fa-book-open', 'Look it up', 'Wiki pages with sources and history.', 'https://openvibe.wiki/', 'wiki learn read knowledge research look'],-- ['codes', 'fa-code', 'Build on OpenVibe', 'API, SDK, OAuth and webhooks for your app.', 'https://openvibe.codes/', 'build api sdk developer developers oauth webhook webhooks app integrate'],++ ['services', 'fa-server', 'Build on OpenVibe', 'One console for projects, keys, grants and the API docs of every service.', 'https://openvibe.services/', 'build api sdk developer developers oauth webhook webhooks app integrate console platform'],++ ['codes', 'fa-code', 'Code with any agent', 'Claude Code, Codex, OpenCode or your own model on one task, with hand-offs.', 'https://openvibe.codes/', 'code coding agent agents ai claude codex opencode deepseek harness contribute'],];function intents(catalog) {@@@ -184,7 +184,7 @@ const DEFAULT_GRANTS = [// Plan T13: OpenVibe.Services merges every authority's resource index (ADR-048 section 3): it reads each one's// GET /api/v1/resources under that authority's <id>.resource.read, with a token for that authority's audience.['services', 'network.resource.read', SELF_AUDIENCE, []],-- ...['media', 'events', 'codes', 'host'].map(id => ['services', `${id}.resource.read`, `openvibe.${id}`, []]),++ ...['media', 'events', 'host'].map(id => ['services', `${id}.resource.read`, `openvibe.${id}`, []]),// Plan T14: OpenVibe.Run publishes run.job.* and hands each job to a paired Node through Bot's internal jobs API// (POST /api/v1/jobs, /jobs/:id/cancel, GET /jobs/:id, capability bot.job.dispatch, service-to-service only).['run', 'events.event.publish', 'openvibe.events', []],@@@ -203,7 +203,7 @@ const DEFAULT_GRANTS = [// use and revoke a proof mod through Games' staff API.['probe', 'games.mod.manage', 'openvibe.games', []],// Wave 20: the Codes portal relays its release events.-- ['codes', 'events.event.publish', 'openvibe.events', []],++ ['services', 'events.event.publish', 'openvibe.events', []],['ai', 'events.event.publish', 'openvibe.events', []], // ai.run.* (AI server/events.js, 2026-09-24)// Wave 13: Live's AI features run as OpenVibe.AI workflows (AI_SERVICE=remote in live.env).// Live runs its own live.* workflows, network.site_copy as the footer-copy fallback, and media.analyze@@@ -315,6 +315,10 @@ const REVOKED_GRANTS = [['live', 'network.coins.transfer', SELF_AUDIENCE],// The Live read mirror is retired (2026-10-05): Chat #25 removed the sender, Live the receiver.['chat', 'live.chat_mirror.write', 'openvibe.live'],++ // The developer portal moved from Codes to Services (contracts 0.113.0): Codes owns no resources and publishes no++ // event any more; Services publishes the services.* events and reads its own resources in process.++ ['services', 'codes.resource.read', 'openvibe.codes'],++ ['codes', 'events.event.publish', 'openvibe.events'],];// Default grants whose namespaces changed: a row still exactly as the old default seeded it is moved to@@@ -69,7 +69,7 @@ function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', fetchImpl = glreturn { router: r, feed, queryOf };}--const SITES = ['live', 'network', 'tools', 'media', 'community', 'space', 'chat', 'games', 'blog', 'wiki', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes', 'host', 'ai', 'search', 'sources', 'events', 'billing', 'tips', 'vip', 'openre', 'bot', 'sites'];++const SITES = ['live', 'network', 'tools', 'media', 'community', 'space', 'chat', 'games', 'blog', 'wiki', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes', 'services', 'host', 'ai', 'search', 'sources', 'events', 'billing', 'tips', 'vip', 'openre', 'bot', 'sites'];const NAMES = { ai: 'AI', vip: 'VIP', openre: 'OpenRe' };const nameOf = (id) => NAMES[id] || (id ? id.charAt(0).toUpperCase() + id.slice(1) : '');@@@ -44,7 +44,7 @@ const quiet = { log() {}, warn() {}, error() {} };// The real directory: 0016 applies on top of 0015, once.const first = await asOwner((db) => db.migrate({ dir: MIGRATIONS, log: quiet }));-- assert.deepStrictEqual(first.applied.map((m) => [m.id, m.name, m.phase]), [['0016', 'agents', 'expand'], ['0017', 'agent_grants', 'expand'], ['0018', 'confirmations', 'expand'], ['0019', 'agent_budgets', 'expand'], ['0020', 'user_owned_trust', 'expand']]);++ assert.deepStrictEqual(first.applied.map((m) => [m.id, m.name, m.phase]), [['0016', 'agents', 'expand'], ['0017', 'agent_grants', 'expand'], ['0018', 'confirmations', 'expand'], ['0019', 'agent_budgets', 'expand'], ['0020', 'user_owned_trust', 'expand'], ['0021', 'services_portal', 'migrate']]);assert.ok(await hasAgents());assert.deepStrictEqual((await asOwner((db) => db.migrate({ dir: MIGRATIONS, log: quiet }))).applied, [], 'already applied');// Expand only, IF NOT EXISTS throughout: running the file again changes nothing and fails nothing.@@@ -67,11 +67,11 @@ async function withoutCatalog(hidden, fn) {(async () => {// ── Settings: defaults, env overrides, and the public-only rule ──let s = policy.settings({});-- assert.deepStrictEqual([...s.sandboxAudiences].sort(), ['openvibe.ai', 'openvibe.codes', 'openvibe.events', 'openvibe.media', 'openvibe.tools']);++ assert.deepStrictEqual([...s.sandboxAudiences].sort(), ['openvibe.ai', 'openvibe.events', 'openvibe.media', 'openvibe.services', 'openvibe.tools']);assert.ok(!s.sandboxAudiences.has('openvibe.network'), 'Network itself never defaults to accepting sandbox tokens');-- assert.deepStrictEqual(s.sandboxAllowance, ['ai.app.run', 'codes.release.manage', 'events.app.publish', 'events.app.read', 'events.app.subscribe',-- 'media.object.delete', 'media.object.list', 'media.object.read', 'media.object.upload', 'tools.job.cancel', 'tools.job.create', 'tools.job.read', 'tools.tool.read', 'tools.tool.run'],-- 'the default sandbox allowance (openvibe-contracts 0.59.0 defines media.object.list and .delete, 0.109.0 ai.app.run)');++ assert.deepStrictEqual(s.sandboxAllowance, ['ai.app.run', 'events.app.publish', 'events.app.read', 'events.app.subscribe',++ 'media.object.delete', 'media.object.list', 'media.object.read', 'media.object.upload', 'services.release.manage', 'tools.job.cancel', 'tools.job.create', 'tools.job.read', 'tools.tool.read', 'tools.tool.run'],++ 'the default sandbox allowance (openvibe-contracts 0.59.0 defines media.object.list and .delete, 0.109.0 ai.app.run, 0.113.0 services.release.manage)');assert.deepStrictEqual(policy.DEFAULT_SANDBOX_ALLOWANCE.filter(id => id.startsWith('events.')), ['events.app.publish', 'events.app.read', 'events.app.subscribe']);s = policy.settings({ developer: { sandboxAudiences: '', sandboxAllowance: '' } });assert.strictEqual(s.sandboxAudiences.size, 0, 'set-but-empty means none');@@@ -58,7 +58,7 @@ const rows = async (userId) => await db.prepare('SELECT * FROM notifications WHE(async () => {// The subscriptions this consumer is meant for, and nothing else.assert.deepStrictEqual(TOPICS, ['deals.watch.matched', 'trade.alert.triggered', 'live.stream.started', 'live.stream.ended', 'chat.moderation.action', 'live.moderation.action', 'community.moderation.action', 'space.moderation.action', 'tips.interaction.moderated', 'billing.staff.action',-- ...['media', 'tools', 'games', 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes'].map((svc) => `${svc}.moderation.action`),++ ...['media', 'tools', 'games', 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade', 'services'].map((svc) => `${svc}.moderation.action`),'tools.usage.recorded', 'events.usage.recorded']);assert.deepStrictEqual(topicsFrom([]), TOPICS, 'scripts/subscribe-events.js subscribes every topic');assert.deepStrictEqual(topicsFrom(['--topic', 'live.stream.started']), ['live.stream.started']);@@@ -14,7 +14,7 @@ assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');assert.equal(siteForHost('evil.example'), null);assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the registry\'s featured list says so)');let p = svc.payloadFor('openvibe.media');--assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'codes', 'blog', 'wiki', 'host', 'bot'], 'cold start follows the base order');++assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'bot', 'codes'], 'cold start follows the base order');assert.strictEqual(p.nav.find(n => n.id === 'space').url, 'https://openvibe.space/', 'the forum goes to Space');assert.strictEqual(p.nav.find(n => n.id === 'community').url, 'https://openvibe.community/', 'Community remains for pastes and Pulse');assert.equal(p.footer.legal.dmca, 'https://openvibe.media/dmca', 'legal links stay on the site\'s own domain');@@@ -66,7 +66,7 @@ const ev = (type, payload, actor = { type: 'user', id: MOD }) => ({ event_id: id// The ten services on common.moderation-action@1 (Contracts 0.53.0): subscribed, one row each, the service from the prefix.const contracts = require('openvibe-contracts');const { TOPICS } = require('../server/notifications/events-consumer');-- const COMMON = ['media', 'tools', 'games', 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes'];++ const COMMON = ['media', 'tools', 'games', 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade', 'services'];for (const svc of COMMON) {const type = `${svc}.moderation.action`;assert.ok(TOPICS.includes(type), `${type} is one of the consumer's subscriptions`);@@@ -23,7 +23,7 @@ const PINNED = {// openvibe-contracts v0.86-v0.90: Bot runs (its own exposure row); Zone is a charter only.bot: 'live', zone: 'placeholder',run: 'internal', // OpenVibe.Run runs on loopback since 2026-10-07 (contracts 0.109.0)-- services: 'internal', // OpenVibe.Services, the T13 control plane, runs on loopback since 2026-10-07 (contracts 0.109.0)++ services: 'live', // OpenVibe.Services: the developer platform at openvibe.services since 2026-10-08 (contracts 0.113.0; the portal moved from Codes)// openvibe-contracts v0.102.0: OpenVibe.Watch is a charter only (plan T18).watch: 'internal', // OpenVibe.Watch runs on loopback since 2026-10-07 (contracts 0.108.0)sdk: 'library', shared: 'library', contracts: 'library', publishing: 'library',@@@ -110,7 +110,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], chassert.strictEqual(sBy.shared.label, `not running (library, released v${require('openvibe-shared/package.json').version})`);// Contracts 0.83.0 added two placeholder services (media-hub, space), v0.86-v0.90 Bot (live) and two more (zone, run),// v0.95.0 Services; Space and AI went live in v0.110.0; none is retired-- assert.deepStrictEqual(st.exposure_summary, { live: 18, internal: 12, library: 4, repository: 1, placeholder: 2, retired: 0 });++ assert.deepStrictEqual(st.exposure_summary, { live: 19, internal: 11, library: 4, repository: 1, placeholder: 2, 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());@@@ -126,7 +126,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch// 3. The Frame: the nav lists only sites whose domain serves the service; the rest are soon with a reason.const open = SITES.filter(s => s.status === 'open').map(s => s.id);-- assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'codes', 'blog', 'wiki', 'host', 'space', 'bot']); // by site position; bot: a site since openvibe-contracts 0.106.0; host: live since 0.107.0; space: live since 0.110.0 (the forum, 2026-10-07)++ assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'space', 'bot', 'codes']); // by site position; bot: a site since openvibe-contracts 0.106.0; host: live since 0.107.0; space: live since 0.110.0 (the forum, 2026-10-07)for (const s of SITES) {assert.ok(s.service && PINNED[s.service], `${s.id} names its service`);assert.strictEqual(s.status === 'open', PINNED[s.service] === 'live', `${s.id} nav status follows exposure`);@@@ -52,7 +52,7 @@ const quiet = { log() {}, warn() {}, error() {} };// ── 0020 applies on top of 0019, once, and rewrites nothing.const first = await asOwner((o) => o.migrate({ dir: MIGRATIONS, log: quiet }));-- assert.deepStrictEqual(first.applied.map((m) => [m.id, m.name, m.phase]), [['0020', 'user_owned_trust', 'expand']]);++ assert.deepStrictEqual(first.applied.map((m) => [m.id, m.name, m.phase]), [['0020', 'user_owned_trust', 'expand'], ['0021', 'services_portal', 'migrate']]);assert.deepStrictEqual((await asOwner((o) => o.migrate({ dir: MIGRATIONS, log: quiet }))).applied, [], 'already applied');await asOwner((o) => o.exec(fs.readFileSync(path.join(MIGRATIONS, '0020_user_owned_trust.sql'), 'utf8'))); // safe to run twiceassert.deepStrictEqual(await rows(), seeded, 'no existing row changes');
@@ -771,7 +771,7 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md). The rules the code keeps:
@@ -771,7 +771,7 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md). The rules the code keeps:
Same as the parent [OpenVibe](../LICENSE) project.
Same as the parent [OpenVibe](../LICENSE) project.
<!-- versions:start -->
<!-- versions:start -->
-- openvibe-contracts: v0.112.0
+- openvibe-contracts: v0.113.0
- openvibe-sdk: v0.35.1
- openvibe-sdk: v0.35.1
- openvibe-shared: v2.13.2
- openvibe-shared: v2.13.2
<!-- versions:end -->
<!-- versions:end -->
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
"runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"port": 4000,
"port": 4000,
"domain": "openvibe.network (with auth., api., admin., my. and themes.openvibe.network in its manifest)",
"domain": "openvibe.network (with auth., api., admin., my. and themes.openvibe.network in its manifest)",
- "contracts": "openvibe-contracts v0.112.0",
+ "contracts": "openvibe-contracts v0.113.0",
"sdk": "openvibe-sdk v0.35.1",
"sdk": "openvibe-sdk v0.35.1",
"shared": "openvibe-shared v2.13.2",
"shared": "openvibe-shared v2.13.2",
"capabilities": [
"capabilities": [
new file mode 100644
new file mode 100644
@@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
+-- phase: migrate
+-- The developer portal moved from OpenVibe.Codes to OpenVibe.Services (owner decision 2026-10-08; openvibe-contracts
+-- 0.113.0 retired codes.release.read|manage for services.release.read|manage, audience openvibe.services). An app's
+-- grant moves with it: same app, same status and history, the new capability and audience. Production held 17 approved
+-- codes.release.manage grants (sandbox apps from the developer path) and no services.release.* row, so nothing
+-- collides. Rollback: the reverse UPDATE (a Network release from before this one refuses the new names as unknown).
+UPDATE dev_grants SET capability = 'services.release.manage', audience = 'openvibe.services'
+ WHERE capability = 'codes.release.manage';
+UPDATE dev_grants SET capability = 'services.release.read', audience = 'openvibe.services'
+ WHERE capability = 'codes.release.read';
@@ -21,7 +21,7 @@
@@ -21,7 +21,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.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.112.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.113.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.1",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.13.2",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.13.2",
"pg": "^8.23.0",
"pg": "^8.23.0",
@@ -1556,9 +1556,9 @@
@@ -1556,9 +1556,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.112.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.112.0",
- "integrity": "sha512-ujIdCjv+oHfwg/+Hw0tyt2erVnornjDiKVPN8snHvybfysR/DBZQ2G32bQj8oAaK90s12ADEpdrXT1eust3eAw==",
+ "version": "0.113.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.113.0",
+ "integrity": "sha512-HdSr3C27r29ciHabb8cFqqZ0658Ki3zbYSz1461xjO4knktweGgJsefJnhGTx7iQBbbJ7XDCAUoTnvouuotNYg==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
{
{
"name": "openvibe-network",
"name": "openvibe-network",
"version": "1.0.0",
"version": "1.0.0",
- "description": "OpenVibe.Network \u2014 Identity, SSO, Accounts, Themes, Notifications, and OpenCoins Wallet",
+ "description": "OpenVibe.Network — Identity, SSO, Accounts, Themes, Notifications, and OpenCoins Wallet",
"main": "server/index.js",
"main": "server/index.js",
"scripts": {
"scripts": {
"start": "node server/index.js",
"start": "node server/index.js",
@@ -30,7 +30,7 @@
@@ -30,7 +30,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.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.112.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.113.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.1",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.13.2",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.13.2",
"pg": "^8.23.0",
"pg": "^8.23.0",
@@ -11,7 +11,7 @@ const cache = require('openvibe-shared/cache-policy');
@@ -11,7 +11,7 @@ const cache = require('openvibe-shared/cache-policy');
* tips.interaction.moderated a paid message filtered, held, hidden or restored
* tips.interaction.moderated a paid message filtered, held, hidden or restored
* billing.staff.action a staff money action in the Billing console
* billing.staff.action a staff money action in the Billing console
* <service>.moderation.action common.moderation-action@1 (Contracts 0.53.0) from tools, games, wiki,
* <service>.moderation.action common.moderation-action@1 (Contracts 0.53.0) from tools, games, wiki,
- * blog, news, reviews, deals, coupons, trade and codes: a staff or
+ * blog, news, reviews, deals, coupons, trade and services: a staff or
* moderator action on someone else's content there; the service is the
* moderator action on someone else's content there; the service is the
* event's prefix
* event's prefix
*
*
@@ -26,7 +26,7 @@ const { staff } = require('openvibe-contracts');
@@ -26,7 +26,7 @@ const { staff } = require('openvibe-contracts');
const { staffClaims } = require('../auth/staff-claims');
const { staffClaims } = require('../auth/staff-claims');
// Producers whose event is common.moderation-action@1 (action, target { type, id, owner_subject }, actor_subject, reason, details).
// Producers whose event is common.moderation-action@1 (action, target { type, id, owner_subject }, actor_subject, reason, details).
-const COMMON_SERVICES = Object.freeze(['media', 'tools', 'games', 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes']);
+const COMMON_SERVICES = Object.freeze(['media', 'tools', 'games', 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade', 'services']);
const TOPICS = Object.freeze(['chat.moderation.action', 'live.moderation.action', 'community.moderation.action', 'space.moderation.action', 'tips.interaction.moderated', 'billing.staff.action',
const TOPICS = Object.freeze(['chat.moderation.action', 'live.moderation.action', 'community.moderation.action', 'space.moderation.action', 'tips.interaction.moderated', 'billing.staff.action',
...COMMON_SERVICES.map((svc) => `${svc}.moderation.action`)]);
...COMMON_SERVICES.map((svc) => `${svc}.moderation.action`)]);
const str = (v, n) => (v == null || v === '' ? null : String(v).replace(/[\u0000-\u001f\u007f]/g, ' ').slice(0, n));
const str = (v, n) => (v == null || v === '' ? null : String(v).replace(/[\u0000-\u001f\u007f]/g, ' ').slice(0, n));
@@ -71,7 +71,7 @@ function rowOf(event) {
@@ -71,7 +71,7 @@ function rowOf(event) {
case 'deals.moderation.action':
case 'deals.moderation.action':
case 'coupons.moderation.action':
case 'coupons.moderation.action':
case 'trade.moderation.action':
case 'trade.moderation.action':
- case 'codes.moderation.action': {
+ case 'services.moderation.action': {
const t = obj(p.target);
const t = obj(p.target);
return { ...base, service: event.event_type.split('.')[0], action: str(p.action, 64) || 'unknown', actor_subject: str(p.actor_subject || actorId, 64),
return { ...base, service: event.event_type.split('.')[0], action: str(p.action, 64) || 'unknown', actor_subject: str(p.actor_subject || actorId, 64),
target_type: str(t.type, 40), target_id: str(t.id, 200), target_subject: str(t.owner_subject, 64), scope: null, reason: str(p.reason, 500), details: json(p.details) };
target_type: str(t.type, 40), target_id: str(t.id, 200), target_subject: str(t.owner_subject, 64), scope: null, reason: str(p.reason, 500), details: json(p.details) };
@@ -43,11 +43,11 @@ const AREAS = [
@@ -43,11 +43,11 @@ const AREAS = [
{ item: 'Space forum moderation', where: [{ kind: 'api', api: 'OpenVibe.Space forum moderator routes', note: 'no console yet' }], service: 'space' },
{ item: 'Space forum moderation', where: [{ kind: 'api', api: 'OpenVibe.Space forum moderator routes', note: 'no console yet' }], service: 'space' },
{ item: 'Payments, cashouts and the money freeze', where: [{ kind: 'url', url: 'https://billing.openvibe.network/', note: 'Billing console' }, { kind: 'tab', tab: 'cashouts' }, { kind: 'tab', tab: 'payments' }], service: 'billing' },
{ item: 'Payments, cashouts and the money freeze', where: [{ kind: 'url', url: 'https://billing.openvibe.network/', note: 'Billing console' }, { kind: 'tab', tab: 'cashouts' }, { kind: 'tab', tab: 'payments' }], service: 'billing' },
{ item: 'Loyalty (OpenCoins, channel points)', where: [{ kind: 'api', api: 'live.loyalty user module; Network /internal/coins/*', note: 'summaries only; loyalty is never money' }], service: 'network' },
{ item: 'Loyalty (OpenCoins, channel points)', where: [{ kind: 'api', api: 'live.loyalty user module; Network /internal/coins/*', note: 'summaries only; loyalty is never money' }], service: 'network' },
- { item: 'Developer apps and trust (Codes)', where: [{ kind: 'url', url: 'https://openvibe.codes/staff' }], service: 'codes' },
+ { item: 'Developer apps and trust (Services)', where: [{ kind: 'url', url: 'https://openvibe.services/staff' }], service: 'services' },
] },
] },
{ area: 'Releases and readiness', items: [
{ area: 'Releases and readiness', items: [
{ item: 'Migration and readiness', where: [{ kind: 'url', url: 'https://openvibe.network/status' }, { kind: 'cli', cli: 'ovhost status, ovhost validate <service>' }], service: 'host' },
{ item: 'Migration and readiness', where: [{ kind: 'url', url: 'https://openvibe.network/status' }, { kind: 'cli', cli: 'ovhost status, ovhost validate <service>' }], service: 'host' },
- { item: 'Compatibility status (contracts, pins, the register)', where: [{ kind: 'url', url: 'https://openvibe.codes/docs' }, { kind: 'cli', cli: 'ovhost validate <service> (contracts range)' }], service: 'codes' },
+ { item: 'Compatibility status (contracts, pins, the register)', where: [{ kind: 'url', url: 'https://openvibe.services/docs' }, { kind: 'cli', cli: 'ovhost validate <service> (contracts range)' }], service: 'codes' },
{ item: 'Deploys', where: [{ kind: 'tab', tab: 'deploy' }, { kind: 'cli', cli: 'ovhost deploy <service>' }], service: 'host' },
{ item: 'Deploys', where: [{ kind: 'tab', tab: 'deploy' }, { kind: 'cli', cli: 'ovhost deploy <service>' }], service: 'host' },
{ item: 'Domains and certificates', where: [{ kind: 'tab', tab: 'domains' }, { kind: 'cli', cli: 'ovhost certs' }], service: 'host' },
{ item: 'Domains and certificates', where: [{ kind: 'tab', tab: 'domains' }, { kind: 'cli', cli: 'ovhost certs' }], service: 'host' },
] },
] },
@@ -103,6 +103,8 @@ async function seedDb(db, { log = console } = {}) {
@@ -103,6 +103,8 @@ async function seedDb(db, { log = console } = {}) {
{ client_id: 'coupons', name: 'OpenVibe.Coupons', redirect_uris: ['https://openvibe.coupons/auth/callback'] },
{ client_id: 'coupons', name: 'OpenVibe.Coupons', redirect_uris: ['https://openvibe.coupons/auth/callback'] },
{ client_id: 'host', name: 'OpenVibe.Host', redirect_uris: ['https://openvibe.host/auth/callback'] },
{ client_id: 'host', name: 'OpenVibe.Host', redirect_uris: ['https://openvibe.host/auth/callback'] },
{ client_id: 'codes', name: 'OpenVibe.Codes', redirect_uris: ['https://openvibe.codes/auth/callback'] },
{ client_id: 'codes', name: 'OpenVibe.Codes', redirect_uris: ['https://openvibe.codes/auth/callback'] },
+ // The developer console (moved from openvibe.codes on 2026-10-08); the client already exists as Services' principal.
+ { client_id: 'services', name: 'OpenVibe.Services', redirect_uris: ['https://openvibe.services/auth/callback'] },
{ client_id: 'billing', name: 'OpenVibe.Billing', redirect_uris: ['https://billing.openvibe.network/auth/callback'] },
{ client_id: 'billing', name: 'OpenVibe.Billing', redirect_uris: ['https://billing.openvibe.network/auth/callback'] },
{ client_id: 'ai', name: 'OpenVibe.AI', redirect_uris: ['https://ai.openvibe.services/auth/callback'] },
{ client_id: 'ai', name: 'OpenVibe.AI', redirect_uris: ['https://ai.openvibe.services/auth/callback'] },
{ client_id: 'bot', name: 'OpenVibe.Bot', redirect_uris: ['https://openvibe.bot/auth/callback'] },
{ client_id: 'bot', name: 'OpenVibe.Bot', redirect_uris: ['https://openvibe.bot/auth/callback'] },
@@ -52,7 +52,7 @@ function grantableCatalog() {
@@ -52,7 +52,7 @@ function grantableCatalog() {
* sandbox runs on free and local capacity only (ai.app.run, plan T6). openvibe.network is deliberately
* sandbox runs on free and local capacity only (ai.app.run, plan T6). openvibe.network is deliberately
* not one of them.
* not one of them.
*/
*/
-const DEFAULT_SANDBOX_AUDIENCES = Object.freeze(['openvibe.media', 'openvibe.events', 'openvibe.tools', 'openvibe.codes', 'openvibe.ai']);
+const DEFAULT_SANDBOX_AUDIENCES = Object.freeze(['openvibe.media', 'openvibe.events', 'openvibe.tools', 'openvibe.services', 'openvibe.ai']);
/**
/**
* Capabilities every project's SANDBOX apps may hold without a staff decision when
* Capabilities every project's SANDBOX apps may hold without a staff decision when
@@ -66,7 +66,7 @@ const DEFAULT_SANDBOX_ALLOWANCE = Object.freeze([
@@ -66,7 +66,7 @@ const DEFAULT_SANDBOX_ALLOWANCE = Object.freeze([
'media.object.upload', 'media.object.read', 'media.object.list', 'media.object.delete',
'media.object.upload', 'media.object.read', 'media.object.list', 'media.object.delete',
'events.app.publish', 'events.app.read', 'events.app.subscribe',
'events.app.publish', 'events.app.read', 'events.app.subscribe',
'tools.job.create', 'tools.job.read', 'tools.job.cancel', 'tools.tool.read', 'tools.tool.run',
'tools.job.create', 'tools.job.read', 'tools.job.cancel', 'tools.tool.read', 'tools.tool.run',
- 'codes.release.manage',
+ 'services.release.manage',
'ai.app.run',
'ai.app.run',
]);
]);
@@ -45,7 +45,8 @@ const INTENTS = [
@@ -45,7 +45,8 @@ const INTENTS = [
['games', 'fa-gamepad', 'Play', 'Browser games that already know you.', 'https://openvibe.games/', 'play game games gaming multiplayer'],
['games', 'fa-gamepad', 'Play', 'Browser games that already know you.', 'https://openvibe.games/', 'play game games gaming multiplayer'],
['blog', 'fa-pen-nib', 'Write', 'A blog of your own, with drafts, scheduling and feeds.', 'https://openvibe.blog/write', 'write blog post posts article publish newsletter'],
['blog', 'fa-pen-nib', 'Write', 'A blog of your own, with drafts, scheduling and feeds.', 'https://openvibe.blog/write', 'write blog post posts article publish newsletter'],
['wiki', 'fa-book-open', 'Look it up', 'Wiki pages with sources and history.', 'https://openvibe.wiki/', 'wiki learn read knowledge research look'],
['wiki', 'fa-book-open', 'Look it up', 'Wiki pages with sources and history.', 'https://openvibe.wiki/', 'wiki learn read knowledge research look'],
- ['codes', 'fa-code', 'Build on OpenVibe', 'API, SDK, OAuth and webhooks for your app.', 'https://openvibe.codes/', 'build api sdk developer developers oauth webhook webhooks app integrate'],
+ ['services', 'fa-server', 'Build on OpenVibe', 'One console for projects, keys, grants and the API docs of every service.', 'https://openvibe.services/', 'build api sdk developer developers oauth webhook webhooks app integrate console platform'],
+ ['codes', 'fa-code', 'Code with any agent', 'Claude Code, Codex, OpenCode or your own model on one task, with hand-offs.', 'https://openvibe.codes/', 'code coding agent agents ai claude codex opencode deepseek harness contribute'],
];
];
function intents(catalog) {
function intents(catalog) {
@@ -184,7 +184,7 @@ const DEFAULT_GRANTS = [
@@ -184,7 +184,7 @@ const DEFAULT_GRANTS = [
// Plan T13: OpenVibe.Services merges every authority's resource index (ADR-048 section 3): it reads each one's
// Plan T13: OpenVibe.Services merges every authority's resource index (ADR-048 section 3): it reads each one's
// GET /api/v1/resources under that authority's <id>.resource.read, with a token for that authority's audience.
// GET /api/v1/resources under that authority's <id>.resource.read, with a token for that authority's audience.
['services', 'network.resource.read', SELF_AUDIENCE, []],
['services', 'network.resource.read', SELF_AUDIENCE, []],
- ...['media', 'events', 'codes', 'host'].map(id => ['services', `${id}.resource.read`, `openvibe.${id}`, []]),
+ ...['media', 'events', 'host'].map(id => ['services', `${id}.resource.read`, `openvibe.${id}`, []]),
// Plan T14: OpenVibe.Run publishes run.job.* and hands each job to a paired Node through Bot's internal jobs API
// Plan T14: OpenVibe.Run publishes run.job.* and hands each job to a paired Node through Bot's internal jobs API
// (POST /api/v1/jobs, /jobs/:id/cancel, GET /jobs/:id, capability bot.job.dispatch, service-to-service only).
// (POST /api/v1/jobs, /jobs/:id/cancel, GET /jobs/:id, capability bot.job.dispatch, service-to-service only).
['run', 'events.event.publish', 'openvibe.events', []],
['run', 'events.event.publish', 'openvibe.events', []],
@@ -203,7 +203,7 @@ const DEFAULT_GRANTS = [
@@ -203,7 +203,7 @@ const DEFAULT_GRANTS = [
// use and revoke a proof mod through Games' staff API.
// use and revoke a proof mod through Games' staff API.
['probe', 'games.mod.manage', 'openvibe.games', []],
['probe', 'games.mod.manage', 'openvibe.games', []],
// Wave 20: the Codes portal relays its release events.
// Wave 20: the Codes portal relays its release events.
- ['codes', 'events.event.publish', 'openvibe.events', []],
+ ['services', 'events.event.publish', 'openvibe.events', []],
['ai', 'events.event.publish', 'openvibe.events', []], // ai.run.* (AI server/events.js, 2026-09-24)
['ai', 'events.event.publish', 'openvibe.events', []], // ai.run.* (AI server/events.js, 2026-09-24)
// Wave 13: Live's AI features run as OpenVibe.AI workflows (AI_SERVICE=remote in live.env).
// Wave 13: Live's AI features run as OpenVibe.AI workflows (AI_SERVICE=remote in live.env).
// Live runs its own live.* workflows, network.site_copy as the footer-copy fallback, and media.analyze
// Live runs its own live.* workflows, network.site_copy as the footer-copy fallback, and media.analyze
@@ -315,6 +315,10 @@ const REVOKED_GRANTS = [
@@ -315,6 +315,10 @@ const REVOKED_GRANTS = [
['live', 'network.coins.transfer', SELF_AUDIENCE],
['live', 'network.coins.transfer', SELF_AUDIENCE],
// The Live read mirror is retired (2026-10-05): Chat #25 removed the sender, Live the receiver.
// The Live read mirror is retired (2026-10-05): Chat #25 removed the sender, Live the receiver.
['chat', 'live.chat_mirror.write', 'openvibe.live'],
['chat', 'live.chat_mirror.write', 'openvibe.live'],
+ // The developer portal moved from Codes to Services (contracts 0.113.0): Codes owns no resources and publishes no
+ // event any more; Services publishes the services.* events and reads its own resources in process.
+ ['services', 'codes.resource.read', 'openvibe.codes'],
+ ['codes', 'events.event.publish', 'openvibe.events'],
];
];
// Default grants whose namespaces changed: a row still exactly as the old default seeded it is moved to
// Default grants whose namespaces changed: a row still exactly as the old default seeded it is moved to
@@ -69,7 +69,7 @@ function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', fetchImpl = gl
@@ -69,7 +69,7 @@ function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', fetchImpl = gl
return { router: r, feed, queryOf };
return { router: r, feed, queryOf };
}
}
-const SITES = ['live', 'network', 'tools', 'media', 'community', 'space', 'chat', 'games', 'blog', 'wiki', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes', 'host', 'ai', 'search', 'sources', 'events', 'billing', 'tips', 'vip', 'openre', 'bot', 'sites'];
+const SITES = ['live', 'network', 'tools', 'media', 'community', 'space', 'chat', 'games', 'blog', 'wiki', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes', 'services', 'host', 'ai', 'search', 'sources', 'events', 'billing', 'tips', 'vip', 'openre', 'bot', 'sites'];
const NAMES = { ai: 'AI', vip: 'VIP', openre: 'OpenRe' };
const NAMES = { ai: 'AI', vip: 'VIP', openre: 'OpenRe' };
const nameOf = (id) => NAMES[id] || (id ? id.charAt(0).toUpperCase() + id.slice(1) : '');
const nameOf = (id) => NAMES[id] || (id ? id.charAt(0).toUpperCase() + id.slice(1) : '');
@@ -44,7 +44,7 @@ const quiet = { log() {}, warn() {}, error() {} };
@@ -44,7 +44,7 @@ const quiet = { log() {}, warn() {}, error() {} };
// The real directory: 0016 applies on top of 0015, once.
// The real directory: 0016 applies on top of 0015, once.
const first = await asOwner((db) => db.migrate({ dir: MIGRATIONS, log: quiet }));
const first = await asOwner((db) => db.migrate({ dir: MIGRATIONS, log: quiet }));
- assert.deepStrictEqual(first.applied.map((m) => [m.id, m.name, m.phase]), [['0016', 'agents', 'expand'], ['0017', 'agent_grants', 'expand'], ['0018', 'confirmations', 'expand'], ['0019', 'agent_budgets', 'expand'], ['0020', 'user_owned_trust', 'expand']]);
+ assert.deepStrictEqual(first.applied.map((m) => [m.id, m.name, m.phase]), [['0016', 'agents', 'expand'], ['0017', 'agent_grants', 'expand'], ['0018', 'confirmations', 'expand'], ['0019', 'agent_budgets', 'expand'], ['0020', 'user_owned_trust', 'expand'], ['0021', 'services_portal', 'migrate']]);
assert.ok(await hasAgents());
assert.ok(await hasAgents());
assert.deepStrictEqual((await asOwner((db) => db.migrate({ dir: MIGRATIONS, log: quiet }))).applied, [], 'already applied');
assert.deepStrictEqual((await asOwner((db) => db.migrate({ dir: MIGRATIONS, log: quiet }))).applied, [], 'already applied');
// Expand only, IF NOT EXISTS throughout: running the file again changes nothing and fails nothing.
// Expand only, IF NOT EXISTS throughout: running the file again changes nothing and fails nothing.
@@ -67,11 +67,11 @@ async function withoutCatalog(hidden, fn) {
@@ -67,11 +67,11 @@ async function withoutCatalog(hidden, fn) {
(async () => {
(async () => {
// ── Settings: defaults, env overrides, and the public-only rule ──
// ── Settings: defaults, env overrides, and the public-only rule ──
let s = policy.settings({});
let s = policy.settings({});
- assert.deepStrictEqual([...s.sandboxAudiences].sort(), ['openvibe.ai', 'openvibe.codes', 'openvibe.events', 'openvibe.media', 'openvibe.tools']);
+ assert.deepStrictEqual([...s.sandboxAudiences].sort(), ['openvibe.ai', 'openvibe.events', 'openvibe.media', 'openvibe.services', 'openvibe.tools']);
assert.ok(!s.sandboxAudiences.has('openvibe.network'), 'Network itself never defaults to accepting sandbox tokens');
assert.ok(!s.sandboxAudiences.has('openvibe.network'), 'Network itself never defaults to accepting sandbox tokens');
- assert.deepStrictEqual(s.sandboxAllowance, ['ai.app.run', 'codes.release.manage', 'events.app.publish', 'events.app.read', 'events.app.subscribe',
- 'media.object.delete', 'media.object.list', 'media.object.read', 'media.object.upload', 'tools.job.cancel', 'tools.job.create', 'tools.job.read', 'tools.tool.read', 'tools.tool.run'],
- 'the default sandbox allowance (openvibe-contracts 0.59.0 defines media.object.list and .delete, 0.109.0 ai.app.run)');
+ assert.deepStrictEqual(s.sandboxAllowance, ['ai.app.run', 'events.app.publish', 'events.app.read', 'events.app.subscribe',
+ 'media.object.delete', 'media.object.list', 'media.object.read', 'media.object.upload', 'services.release.manage', 'tools.job.cancel', 'tools.job.create', 'tools.job.read', 'tools.tool.read', 'tools.tool.run'],
+ 'the default sandbox allowance (openvibe-contracts 0.59.0 defines media.object.list and .delete, 0.109.0 ai.app.run, 0.113.0 services.release.manage)');
assert.deepStrictEqual(policy.DEFAULT_SANDBOX_ALLOWANCE.filter(id => id.startsWith('events.')), ['events.app.publish', 'events.app.read', 'events.app.subscribe']);
assert.deepStrictEqual(policy.DEFAULT_SANDBOX_ALLOWANCE.filter(id => id.startsWith('events.')), ['events.app.publish', 'events.app.read', 'events.app.subscribe']);
s = policy.settings({ developer: { sandboxAudiences: '', sandboxAllowance: '' } });
s = policy.settings({ developer: { sandboxAudiences: '', sandboxAllowance: '' } });
assert.strictEqual(s.sandboxAudiences.size, 0, 'set-but-empty means none');
assert.strictEqual(s.sandboxAudiences.size, 0, 'set-but-empty means none');
@@ -58,7 +58,7 @@ const rows = async (userId) => await db.prepare('SELECT * FROM notifications WHE
@@ -58,7 +58,7 @@ const rows = async (userId) => await db.prepare('SELECT * FROM notifications WHE
(async () => {
(async () => {
// The subscriptions this consumer is meant for, and nothing else.
// The subscriptions this consumer is meant for, and nothing else.
assert.deepStrictEqual(TOPICS, ['deals.watch.matched', 'trade.alert.triggered', 'live.stream.started', 'live.stream.ended', 'chat.moderation.action', 'live.moderation.action', 'community.moderation.action', 'space.moderation.action', 'tips.interaction.moderated', 'billing.staff.action',
assert.deepStrictEqual(TOPICS, ['deals.watch.matched', 'trade.alert.triggered', 'live.stream.started', 'live.stream.ended', 'chat.moderation.action', 'live.moderation.action', 'community.moderation.action', 'space.moderation.action', 'tips.interaction.moderated', 'billing.staff.action',
- ...['media', 'tools', 'games', 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes'].map((svc) => `${svc}.moderation.action`),
+ ...['media', 'tools', 'games', 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade', 'services'].map((svc) => `${svc}.moderation.action`),
'tools.usage.recorded', 'events.usage.recorded']);
'tools.usage.recorded', 'events.usage.recorded']);
assert.deepStrictEqual(topicsFrom([]), TOPICS, 'scripts/subscribe-events.js subscribes every topic');
assert.deepStrictEqual(topicsFrom([]), TOPICS, 'scripts/subscribe-events.js subscribes every topic');
assert.deepStrictEqual(topicsFrom(['--topic', 'live.stream.started']), ['live.stream.started']);
assert.deepStrictEqual(topicsFrom(['--topic', 'live.stream.started']), ['live.stream.started']);
@@ -14,7 +14,7 @@ assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');
@@ -14,7 +14,7 @@ assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');
assert.equal(siteForHost('evil.example'), null);
assert.equal(siteForHost('evil.example'), null);
assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the registry\'s featured list says so)');
assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the registry\'s featured list says so)');
let p = svc.payloadFor('openvibe.media');
let p = svc.payloadFor('openvibe.media');
-assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'codes', 'blog', 'wiki', 'host', 'bot'], 'cold start follows the base order');
+assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'bot', 'codes'], 'cold start follows the base order');
assert.strictEqual(p.nav.find(n => n.id === 'space').url, 'https://openvibe.space/', 'the forum goes to Space');
assert.strictEqual(p.nav.find(n => n.id === 'space').url, 'https://openvibe.space/', 'the forum goes to Space');
assert.strictEqual(p.nav.find(n => n.id === 'community').url, 'https://openvibe.community/', 'Community remains for pastes and Pulse');
assert.strictEqual(p.nav.find(n => n.id === 'community').url, 'https://openvibe.community/', 'Community remains for pastes and Pulse');
assert.equal(p.footer.legal.dmca, 'https://openvibe.media/dmca', 'legal links stay on the site\'s own domain');
assert.equal(p.footer.legal.dmca, 'https://openvibe.media/dmca', 'legal links stay on the site\'s own domain');
@@ -66,7 +66,7 @@ const ev = (type, payload, actor = { type: 'user', id: MOD }) => ({ event_id: id
@@ -66,7 +66,7 @@ const ev = (type, payload, actor = { type: 'user', id: MOD }) => ({ event_id: id
// The ten services on common.moderation-action@1 (Contracts 0.53.0): subscribed, one row each, the service from the prefix.
// The ten services on common.moderation-action@1 (Contracts 0.53.0): subscribed, one row each, the service from the prefix.
const contracts = require('openvibe-contracts');
const contracts = require('openvibe-contracts');
const { TOPICS } = require('../server/notifications/events-consumer');
const { TOPICS } = require('../server/notifications/events-consumer');
- const COMMON = ['media', 'tools', 'games', 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes'];
+ const COMMON = ['media', 'tools', 'games', 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade', 'services'];
for (const svc of COMMON) {
for (const svc of COMMON) {
const type = `${svc}.moderation.action`;
const type = `${svc}.moderation.action`;
assert.ok(TOPICS.includes(type), `${type} is one of the consumer's subscriptions`);
assert.ok(TOPICS.includes(type), `${type} is one of the consumer's subscriptions`);
@@ -23,7 +23,7 @@ const PINNED = {
@@ -23,7 +23,7 @@ const PINNED = {
// openvibe-contracts v0.86-v0.90: Bot runs (its own exposure row); Zone is a charter only.
// openvibe-contracts v0.86-v0.90: Bot runs (its own exposure row); Zone is a charter only.
bot: 'live', zone: 'placeholder',
bot: 'live', zone: 'placeholder',
run: 'internal', // OpenVibe.Run runs on loopback since 2026-10-07 (contracts 0.109.0)
run: 'internal', // OpenVibe.Run runs on loopback since 2026-10-07 (contracts 0.109.0)
- services: 'internal', // OpenVibe.Services, the T13 control plane, runs on loopback since 2026-10-07 (contracts 0.109.0)
+ services: 'live', // OpenVibe.Services: the developer platform at openvibe.services since 2026-10-08 (contracts 0.113.0; the portal moved from Codes)
// openvibe-contracts v0.102.0: OpenVibe.Watch is a charter only (plan T18).
// openvibe-contracts v0.102.0: OpenVibe.Watch is a charter only (plan T18).
watch: 'internal', // OpenVibe.Watch runs on loopback since 2026-10-07 (contracts 0.108.0)
watch: 'internal', // OpenVibe.Watch runs on loopback since 2026-10-07 (contracts 0.108.0)
sdk: 'library', shared: 'library', contracts: 'library', publishing: 'library',
sdk: 'library', shared: 'library', contracts: 'library', publishing: 'library',
@@ -110,7 +110,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -110,7 +110,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
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})`);
// Contracts 0.83.0 added two placeholder services (media-hub, space), v0.86-v0.90 Bot (live) and two more (zone, run),
// Contracts 0.83.0 added two placeholder services (media-hub, space), v0.86-v0.90 Bot (live) and two more (zone, run),
// v0.95.0 Services; Space and AI went live in v0.110.0; none is retired
// v0.95.0 Services; Space and AI went live in v0.110.0; none is retired
- assert.deepStrictEqual(st.exposure_summary, { live: 18, internal: 12, library: 4, repository: 1, placeholder: 2, retired: 0 });
+ assert.deepStrictEqual(st.exposure_summary, { live: 19, internal: 11, library: 4, repository: 1, placeholder: 2, 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());
@@ -126,7 +126,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -126,7 +126,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
// 3. The Frame: the nav lists only sites whose domain serves the service; the rest are soon with a reason.
// 3. The Frame: the nav lists only sites whose domain serves the service; the rest are soon with a reason.
const open = SITES.filter(s => s.status === 'open').map(s => s.id);
const open = SITES.filter(s => s.status === 'open').map(s => s.id);
- assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'codes', 'blog', 'wiki', 'host', 'space', 'bot']); // by site position; bot: a site since openvibe-contracts 0.106.0; host: live since 0.107.0; space: live since 0.110.0 (the forum, 2026-10-07)
+ assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'space', 'bot', 'codes']); // by site position; bot: a site since openvibe-contracts 0.106.0; host: live since 0.107.0; space: live since 0.110.0 (the forum, 2026-10-07)
for (const s of SITES) {
for (const s of SITES) {
assert.ok(s.service && PINNED[s.service], `${s.id} names its service`);
assert.ok(s.service && PINNED[s.service], `${s.id} names its service`);
assert.strictEqual(s.status === 'open', PINNED[s.service] === 'live', `${s.id} nav status follows exposure`);
assert.strictEqual(s.status === 'open', PINNED[s.service] === 'live', `${s.id} nav status follows exposure`);
@@ -52,7 +52,7 @@ const quiet = { log() {}, warn() {}, error() {} };
@@ -52,7 +52,7 @@ const quiet = { log() {}, warn() {}, error() {} };
// ── 0020 applies on top of 0019, once, and rewrites nothing.
// ── 0020 applies on top of 0019, once, and rewrites nothing.
const first = await asOwner((o) => o.migrate({ dir: MIGRATIONS, log: quiet }));
const first = await asOwner((o) => o.migrate({ dir: MIGRATIONS, log: quiet }));
- assert.deepStrictEqual(first.applied.map((m) => [m.id, m.name, m.phase]), [['0020', 'user_owned_trust', 'expand']]);
+ assert.deepStrictEqual(first.applied.map((m) => [m.id, m.name, m.phase]), [['0020', 'user_owned_trust', 'expand'], ['0021', 'services_portal', 'migrate']]);
assert.deepStrictEqual((await asOwner((o) => o.migrate({ dir: MIGRATIONS, log: quiet }))).applied, [], 'already applied');
assert.deepStrictEqual((await asOwner((o) => o.migrate({ dir: MIGRATIONS, log: quiet }))).applied, [], 'already applied');
await asOwner((o) => o.exec(fs.readFileSync(path.join(MIGRATIONS, '0020_user_owned_trust.sql'), 'utf8'))); // safe to run twice
await asOwner((o) => o.exec(fs.readFileSync(path.join(MIGRATIONS, '0020_user_owned_trust.sql'), 'utf8'))); // safe to run twice
assert.deepStrictEqual(await rows(), seeded, 'no existing row changes');
assert.deepStrictEqual(await rows(), seeded, 'no existing row changes');