openvibers/openvibe.services Commits
Mod the network (/mod): every way to extend OpenVibe today, each with its link, and where packaged mods stand (staff installs until ADR-013's signing and review decisions) (#104)
Changes
@@@ -4,6 +4,7 @@ Notable changes to OpenVibe.Services. The service is unreleased; everything belo## Unreleased++- **Mod the network** (`/mod`, `server/http/mod.js`; the owner 2026-10-25: "openvibe.services as the place to mod the network or build your own sites with the SDK and APIs"). Every way to extend OpenVibe today, each with its link: your own site with the shared home-page kit, chat bots, alerts and work into a team room (Chat room hooks), watches that act, agents, Bot drivers and screen shares, game worlds, events, and changing OpenVibe itself on Codes. Packaged mods say plainly that staff install them until ADR-013's signing and review decisions are made. In the navigation, the sitemap, llms.txt and the home's quick start. `test/mod-page.test.js`.- Control operations (ADR-048, plan T13 step 10): `POST /api/v1/resources/control` behind `services.resource.control` (contracts v0.154.0, was v0.150.0).- It takes a `common.resource-control-request@1` and answers the owning authority's `common.resource-control-result@1`. The request is forwarded through `openvibe-sdk/resources` `createResourceClient`.- Callers: a first-party service token, or a person who owns or administers the project.@@@ -57,6 +57,7 @@ const PAGE_TEXT = {'/docs/sdk': ['SDK reference', 'openvibe-sdk modules, from their type definitions.'],'/oauth': ['OAuth callback helper', 'Authorization code + PKCE, with a callback that never exchanges the code.'],'/tools/webhooks': ['Webhook signature tester', 'Decides as a receiver requiring signature v2 does.'],++ '/mod': ['Mod the network', 'Every way to extend OpenVibe today (sites, bots, room hooks, watches, agents, drivers, worlds, events) and where packaged mods stand.'],'/manifests/validate': ['Manifest validation', 'App and mod manifests, validated against the pinned contracts.'],'/policy': ['Policy', 'The portal\'s policy pages: decisions, compatibility, licensing, transparency and community.'],'/policy/rfc': ['Proposals and decisions', 'How a contract, capability or event changes, and the ADR record.'],@@@ -112,6 +113,7 @@ function publicPages({ docs }) {{ path: '/docs/sdk', changefreq: 'weekly', priority: 0.7 },{ path: '/oauth', changefreq: 'monthly', priority: 0.5 },{ path: '/tools/webhooks', changefreq: 'monthly', priority: 0.5 },++ { path: '/mod', changefreq: 'weekly', priority: 0.8 },{ path: '/manifests/validate', changefreq: 'monthly', priority: 0.5 },{ path: '/policy', changefreq: 'monthly', priority: 0.5 },{ path: '/policy/rfc', changefreq: 'monthly', priority: 0.4 },@@@ -229,6 +231,9 @@ function createDiscoveryRoutes(ctx) {{ title: 'Coding harnesses', url: 'https://openvibe.codes/harnesses', note: 'the coding agents OpenVibe.Codes routes a task to (another site)' },{ title: 'SDK', url: abs('/docs/sdk') },] },++ { title: 'Extend OpenVibe', links: [++ { title: 'Mod the network', url: abs('/mod'), note: 'every way to extend OpenVibe today, and where packaged mods stand' },++ ] },{ title: 'Developer tools', links: [{ title: 'OAuth callback helper', url: abs('/oauth'), note: 'authorization code + PKCE, with a callback that never exchanges the code' },{ title: 'Webhook signature tester', url: abs('/tools/webhooks'), note: 'decides as a receiver requiring signature v2 does' },@@@ -103,7 +103,7 @@ function build() {id: 'build', title: 'What you can build today',lede: 'Each card is a working example in OpenVibe.Examples (MIT, with a smoke test) or a service you can call now.',items: BUILD,-- }).replace(/<\/section>$/, () => `<p class="sc-note">Start from one: <code>npx --package=https://codeload.github.com/OpenVibers/OpenVibe.Examples/tar.gz/refs/tags/v0.3.0 create-openvibe-app my-app --template web</code> (templates: web, server, bot, mod). <a href="${EXAMPLES}">All examples</a>.</p></section>`);++ }).replace(/<\/section>$/, () => `<p class="sc-note">Start from one: <code>npx --package=https://codeload.github.com/OpenVibers/OpenVibe.Examples/tar.gz/refs/tags/v0.3.0 create-openvibe-app my-app --template web</code> (templates: web, server, bot, mod). <a href="${EXAMPLES}">All examples</a> · <a href="/mod">Every way to mod the network</a>.</p></section>`);}/** Quick start: code that runs against production with the pinned SDK (the first sample needs no key at all). */nnew file mode 100644@@@ -0,0 +1,49 @@++'use strict';++/**++ * /mod: every way to change how the network works, for yourself, your community or everyone (the owner 2026-10-25:++ * "openvibe.services as the place to mod the network or build your own sites with the SDK and APIs"). Each card names++ * something that works today and links to where it is done or documented; packaged mods say plainly what waits on the++ * open decisions in ADR-013 (signing and review), so nothing here promises an install path that does not exist.++ */++const { html } = require('../render/html');++++const GH = 'https://github.com/OpenVibers';++const EXAMPLES = `${GH}/OpenVibe.Examples`;++const example = (name) => `${EXAMPLES}/tree/main/examples/${name}`;++++// What anyone can extend today, without asking anyone.++const TODAY = [++ { title: 'Your own site on the network', text: 'Sign-in with OpenVibe, the shared navbar and footer, and the home-page kit (hero, numbers, cards and rails, spotlight, FAQ, "Part of OpenVibe") that the network\'s own sites are built from.', href: `${GH}/OpenVibe.Shared/blob/main/docs/home-pages.md`, link: 'The home-page kit' },++ { title: 'A chat bot', text: 'A bot in its own room that always says it is a bot, from the chat-bot example, or Vibe and your own AI members in any room you run.', href: example('chat-bot'), link: 'chat-bot example' },++ { title: 'Alerts and work into a team room', text: 'A workspace room on OpenVibe.Chat takes signed hooks: a Watch firing, an OpenVibe event or a line of text arrives as a message from Vibe, and can queue a task for the team or for your own computer.', href: 'https://openvibe.chat/rooms', link: 'Rooms' },++ { title: 'Watches that act', text: 'Follow any page, feed or API, and when it changes: notify you, call a signed webhook, give an agent a task, start a job on your own machine, or open a coding task.', href: 'https://openvibe.watch/', link: 'OpenVibe.Watch' },++ { title: 'Agents of your own', text: 'Personal agents on OpenVibe.Actor with your instructions and tools, run now or on a schedule, under your own allowance.', href: 'https://openvibe.actor/docs', link: 'Actor docs' },++ { title: 'Robots, devices and computers', text: 'Write a driver as a plugin for OpenVibe.Node, pair the machine, and drive it through Bot\'s one gate with roles, limits and an e-stop. Share a computer\'s screen into a room.', href: 'https://openvibe.bot/docs', link: 'Bot docs' },++ { title: 'Game worlds', text: 'Build a world in the OpenVibe.Games editor and publish it: anyone can join it, and it shows up in search.', href: 'https://openvibe.games/worlds', link: 'Worlds' },++ { title: 'React to the whole network', text: 'Subscribe to events (streams going live, posts, recordings, jobs) with durable cursors or signed webhooks, and build on them.', href: example('event-subscriber'), link: 'event-subscriber example' },++ { title: 'Change OpenVibe itself', text: 'Every service is open source. Propose a change on OpenVibe.Codes, where OpenVibe is built, or send a pull request to the repository it touches.', href: 'https://openvibe.codes/', link: 'OpenVibe.Codes' },++];++++function modPage({ examplesTag = 'v0.3.0' } = {}) {++ const create = `npx --package=https://codeload.github.com/OpenVibers/OpenVibe.Examples/tar.gz/refs/tags/${examplesTag} create-openvibe-app my-mod --template mod`;++ return html`<h1>Mod the network</h1>++<p>OpenVibe is open source end to end, and every product has an API. These are the ways to change how it works for you, for your community, or for everyone, today.</p>++<h2 id="today">Extend it today</h2>++<ul class="cards">${TODAY.map((c) => html`<li><a href="${c.href}"><strong>${c.title}</strong></a><span>${c.text}</span><a class="muted" href="${c.href}">${c.link} →</a></li>`)}</ul>++<h2 id="packaged">Packaged mods</h2>++<p>A packaged mod changes a product from the inside: a game's rules, an overlay on a stream. It is described by a <strong>mod manifest</strong> (<code>mods.mod-manifest@1</code> in openvibe-contracts): its id, version, publisher, target runtime, the capabilities it asks for, its assets and its budget. Installing one gives it its own principal on OpenVibe.Network, granted only the capabilities that were approved, and revoking that grant stops it at once.</p>++<ol class="steps">++<li><strong>Start one</strong> from the template: <code>${create}</code></li>++<li><strong>Check its manifest</strong> against the pinned contracts on the <a href="/manifests/validate">manifest validator</a>, which also says which capabilities an install could grant.</li>++<li><strong>Ask for it</strong> to be installed: open a proposal on <a href="https://openvibe.codes/">OpenVibe.Codes</a>.</li>++</ol>++<p class="muted">What waits: today only staff install mods (with the approved capabilities), because how a mod release is signed and how it is reviewed are open decisions for the owner (<a href="/docs/adr/ADR-013">ADR-013</a>, amendment of 2026-09-24). Mods that run their own code also wait for sandboxed hosting. Until then a mod is data plus the scripting each game's own sandbox provides. This page will say when that changes.</p>++<h2 id="start">Where to start</h2>++<ol class="steps">++<li><a href="/projects">Create a project</a> and add a sandbox app: its sandbox capabilities are approved at once.</li>++<li>Pick a card above, or start from an example: <a href="${EXAMPLES}">OpenVibe.Examples</a> (MIT, each with a smoke test).</li>++<li>Read the <a href="/docs">platform reference</a> and the <a href="/docs/sdk">SDK</a>, generated from the same contracts the services enforce.</li>++</ol>`;++}++++module.exports = { modPage, TODAY };@@@ -137,6 +137,7 @@ ${raw(showcase.cta({ title: 'Start building', text: 'Sign in with your OpenVibeconst adr = (id) => docs.adrs.find((a) => a.id === id);// What shipped on OpenVibe.Services: the shared update log every OpenVibe site has.r.get('/updates', (req, res) => page(req, res, { index: true, cache: PUBLIC_CACHE, title: 'What shipped on OpenVibe.Services', body: raw(frame.updatesBody({ service: 'services', siteName: 'OpenVibe.Services' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`) }));++ r.get('/mod', (req, res) => page(req, res, { index: true, cache: PUBLIC_CACHE, title: 'Mod the network', crumbs: [{ label: 'Mod the network' }], body: require('./mod').modPage() }));r.get('/policy', (req, res) => page(req, res, {index: true, cache: PUBLIC_CACHE, title: 'Policy', crumbs: [{ label: 'Policy' }],body: html`<h1>Policy</h1><ul class="cards">@@@ -27,6 +27,7 @@ const NAV = [{ label: 'Docs', href: '/docs' },{ label: 'OAuth', href: '/oauth' },{ label: 'Webhooks', href: '/tools/webhooks' },++ { label: 'Mod', href: '/mod' },{ label: 'Manifests', href: '/manifests/validate' },{ label: 'Policy', href: '/policy' },];nnew file mode 100644@@@ -0,0 +1,34 @@++'use strict';++/**++ * /mod (server/http/mod.js): a public, indexable page that lists every way to extend OpenVibe today, each card with a++ * link, says plainly that packaged mods are installed by staff until ADR-013's open decisions are made, and is in the++ * navigation, the sitemap and llms.txt.++ */++const assert = require('assert');++const { boot, check, done } = require('./helpers/boot');++const { TODAY } = require('../server/http/mod');++++(async () => {++ const t = await boot();++ try {++ await check('the page: every card, the honest state of packaged mods, links that go somewhere', async () => {++ const r = await t.get('/mod');++ assert.strictEqual(r.status, 200);++ assert.match(r.text, /<h1>Mod the network<\/h1>/);++ TODAY.forEach((c) => {++ assert.ok(r.text.includes(c.title.replace(/'/g, ''').replace(/"/g, '"')), `missing card: ${c.title}`);++ assert.match(c.href, /^https:\/\//, `${c.title} links out over https`);++ });++ assert.match(r.text, /only staff install mods/);++ assert.match(r.text, /href="\/docs\/adr\/ADR-013"/);++ assert.match(r.text, /create-openvibe-app my-mod --template mod/);++ assert.ok(!/noindex/.test(r.text), 'indexable');++ });++ await check('found from the navigation, the sitemap and llms.txt', async () => {++ assert.match((await t.get('/')).text, /href="\/mod"/);++ assert.ok((await t.get('/sitemap.xml')).text.includes('<loc>https://openvibe.services/mod</loc>'));++ assert.ok((await t.get('/llms.txt')).text.includes('https://openvibe.services/mod'));++ assert.ok((await t.get('/llms-full.txt')).text.includes('Mod the network'));++ });++ } finally { await done(t); }++})();
@@ -4,6 +4,7 @@ Notable changes to OpenVibe.Services. The service is unreleased; everything belo
@@ -4,6 +4,7 @@ Notable changes to OpenVibe.Services. The service is unreleased; everything belo
## Unreleased
## Unreleased
+- **Mod the network** (`/mod`, `server/http/mod.js`; the owner 2026-10-25: "openvibe.services as the place to mod the network or build your own sites with the SDK and APIs"). Every way to extend OpenVibe today, each with its link: your own site with the shared home-page kit, chat bots, alerts and work into a team room (Chat room hooks), watches that act, agents, Bot drivers and screen shares, game worlds, events, and changing OpenVibe itself on Codes. Packaged mods say plainly that staff install them until ADR-013's signing and review decisions are made. In the navigation, the sitemap, llms.txt and the home's quick start. `test/mod-page.test.js`.
- Control operations (ADR-048, plan T13 step 10): `POST /api/v1/resources/control` behind `services.resource.control` (contracts v0.154.0, was v0.150.0).
- Control operations (ADR-048, plan T13 step 10): `POST /api/v1/resources/control` behind `services.resource.control` (contracts v0.154.0, was v0.150.0).
- It takes a `common.resource-control-request@1` and answers the owning authority's `common.resource-control-result@1`. The request is forwarded through `openvibe-sdk/resources` `createResourceClient`.
- It takes a `common.resource-control-request@1` and answers the owning authority's `common.resource-control-result@1`. The request is forwarded through `openvibe-sdk/resources` `createResourceClient`.
- Callers: a first-party service token, or a person who owns or administers the project.
- Callers: a first-party service token, or a person who owns or administers the project.
@@ -57,6 +57,7 @@ const PAGE_TEXT = {
@@ -57,6 +57,7 @@ const PAGE_TEXT = {
'/docs/sdk': ['SDK reference', 'openvibe-sdk modules, from their type definitions.'],
'/docs/sdk': ['SDK reference', 'openvibe-sdk modules, from their type definitions.'],
'/oauth': ['OAuth callback helper', 'Authorization code + PKCE, with a callback that never exchanges the code.'],
'/oauth': ['OAuth callback helper', 'Authorization code + PKCE, with a callback that never exchanges the code.'],
'/tools/webhooks': ['Webhook signature tester', 'Decides as a receiver requiring signature v2 does.'],
'/tools/webhooks': ['Webhook signature tester', 'Decides as a receiver requiring signature v2 does.'],
+ '/mod': ['Mod the network', 'Every way to extend OpenVibe today (sites, bots, room hooks, watches, agents, drivers, worlds, events) and where packaged mods stand.'],
'/manifests/validate': ['Manifest validation', 'App and mod manifests, validated against the pinned contracts.'],
'/manifests/validate': ['Manifest validation', 'App and mod manifests, validated against the pinned contracts.'],
'/policy': ['Policy', 'The portal\'s policy pages: decisions, compatibility, licensing, transparency and community.'],
'/policy': ['Policy', 'The portal\'s policy pages: decisions, compatibility, licensing, transparency and community.'],
'/policy/rfc': ['Proposals and decisions', 'How a contract, capability or event changes, and the ADR record.'],
'/policy/rfc': ['Proposals and decisions', 'How a contract, capability or event changes, and the ADR record.'],
@@ -112,6 +113,7 @@ function publicPages({ docs }) {
@@ -112,6 +113,7 @@ function publicPages({ docs }) {
{ path: '/docs/sdk', changefreq: 'weekly', priority: 0.7 },
{ path: '/docs/sdk', changefreq: 'weekly', priority: 0.7 },
{ path: '/oauth', changefreq: 'monthly', priority: 0.5 },
{ path: '/oauth', changefreq: 'monthly', priority: 0.5 },
{ path: '/tools/webhooks', changefreq: 'monthly', priority: 0.5 },
{ path: '/tools/webhooks', changefreq: 'monthly', priority: 0.5 },
+ { path: '/mod', changefreq: 'weekly', priority: 0.8 },
{ path: '/manifests/validate', changefreq: 'monthly', priority: 0.5 },
{ path: '/manifests/validate', changefreq: 'monthly', priority: 0.5 },
{ path: '/policy', changefreq: 'monthly', priority: 0.5 },
{ path: '/policy', changefreq: 'monthly', priority: 0.5 },
{ path: '/policy/rfc', changefreq: 'monthly', priority: 0.4 },
{ path: '/policy/rfc', changefreq: 'monthly', priority: 0.4 },
@@ -229,6 +231,9 @@ function createDiscoveryRoutes(ctx) {
@@ -229,6 +231,9 @@ function createDiscoveryRoutes(ctx) {
{ title: 'Coding harnesses', url: 'https://openvibe.codes/harnesses', note: 'the coding agents OpenVibe.Codes routes a task to (another site)' },
{ title: 'Coding harnesses', url: 'https://openvibe.codes/harnesses', note: 'the coding agents OpenVibe.Codes routes a task to (another site)' },
{ title: 'SDK', url: abs('/docs/sdk') },
{ title: 'SDK', url: abs('/docs/sdk') },
] },
] },
+ { title: 'Extend OpenVibe', links: [
+ { title: 'Mod the network', url: abs('/mod'), note: 'every way to extend OpenVibe today, and where packaged mods stand' },
+ ] },
{ title: 'Developer tools', links: [
{ title: 'Developer tools', links: [
{ title: 'OAuth callback helper', url: abs('/oauth'), note: 'authorization code + PKCE, with a callback that never exchanges the code' },
{ title: 'OAuth callback helper', url: abs('/oauth'), note: 'authorization code + PKCE, with a callback that never exchanges the code' },
{ title: 'Webhook signature tester', url: abs('/tools/webhooks'), note: 'decides as a receiver requiring signature v2 does' },
{ title: 'Webhook signature tester', url: abs('/tools/webhooks'), note: 'decides as a receiver requiring signature v2 does' },
@@ -103,7 +103,7 @@ function build() {
@@ -103,7 +103,7 @@ function build() {
id: 'build', title: 'What you can build today',
id: 'build', title: 'What you can build today',
lede: 'Each card is a working example in OpenVibe.Examples (MIT, with a smoke test) or a service you can call now.',
lede: 'Each card is a working example in OpenVibe.Examples (MIT, with a smoke test) or a service you can call now.',
items: BUILD,
items: BUILD,
- }).replace(/<\/section>$/, () => `<p class="sc-note">Start from one: <code>npx --package=https://codeload.github.com/OpenVibers/OpenVibe.Examples/tar.gz/refs/tags/v0.3.0 create-openvibe-app my-app --template web</code> (templates: web, server, bot, mod). <a href="${EXAMPLES}">All examples</a>.</p></section>`);
+ }).replace(/<\/section>$/, () => `<p class="sc-note">Start from one: <code>npx --package=https://codeload.github.com/OpenVibers/OpenVibe.Examples/tar.gz/refs/tags/v0.3.0 create-openvibe-app my-app --template web</code> (templates: web, server, bot, mod). <a href="${EXAMPLES}">All examples</a> · <a href="/mod">Every way to mod the network</a>.</p></section>`);
}
}
/** Quick start: code that runs against production with the pinned SDK (the first sample needs no key at all). */
/** Quick start: code that runs against production with the pinned SDK (the first sample needs no key at all). */
new file mode 100644
new file mode 100644
@@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
+'use strict';
+/**
+ * /mod: every way to change how the network works, for yourself, your community or everyone (the owner 2026-10-25:
+ * "openvibe.services as the place to mod the network or build your own sites with the SDK and APIs"). Each card names
+ * something that works today and links to where it is done or documented; packaged mods say plainly what waits on the
+ * open decisions in ADR-013 (signing and review), so nothing here promises an install path that does not exist.
+ */
+const { html } = require('../render/html');
+
+const GH = 'https://github.com/OpenVibers';
+const EXAMPLES = `${GH}/OpenVibe.Examples`;
+const example = (name) => `${EXAMPLES}/tree/main/examples/${name}`;
+
+// What anyone can extend today, without asking anyone.
+const TODAY = [
+ { title: 'Your own site on the network', text: 'Sign-in with OpenVibe, the shared navbar and footer, and the home-page kit (hero, numbers, cards and rails, spotlight, FAQ, "Part of OpenVibe") that the network\'s own sites are built from.', href: `${GH}/OpenVibe.Shared/blob/main/docs/home-pages.md`, link: 'The home-page kit' },
+ { title: 'A chat bot', text: 'A bot in its own room that always says it is a bot, from the chat-bot example, or Vibe and your own AI members in any room you run.', href: example('chat-bot'), link: 'chat-bot example' },
+ { title: 'Alerts and work into a team room', text: 'A workspace room on OpenVibe.Chat takes signed hooks: a Watch firing, an OpenVibe event or a line of text arrives as a message from Vibe, and can queue a task for the team or for your own computer.', href: 'https://openvibe.chat/rooms', link: 'Rooms' },
+ { title: 'Watches that act', text: 'Follow any page, feed or API, and when it changes: notify you, call a signed webhook, give an agent a task, start a job on your own machine, or open a coding task.', href: 'https://openvibe.watch/', link: 'OpenVibe.Watch' },
+ { title: 'Agents of your own', text: 'Personal agents on OpenVibe.Actor with your instructions and tools, run now or on a schedule, under your own allowance.', href: 'https://openvibe.actor/docs', link: 'Actor docs' },
+ { title: 'Robots, devices and computers', text: 'Write a driver as a plugin for OpenVibe.Node, pair the machine, and drive it through Bot\'s one gate with roles, limits and an e-stop. Share a computer\'s screen into a room.', href: 'https://openvibe.bot/docs', link: 'Bot docs' },
+ { title: 'Game worlds', text: 'Build a world in the OpenVibe.Games editor and publish it: anyone can join it, and it shows up in search.', href: 'https://openvibe.games/worlds', link: 'Worlds' },
+ { title: 'React to the whole network', text: 'Subscribe to events (streams going live, posts, recordings, jobs) with durable cursors or signed webhooks, and build on them.', href: example('event-subscriber'), link: 'event-subscriber example' },
+ { title: 'Change OpenVibe itself', text: 'Every service is open source. Propose a change on OpenVibe.Codes, where OpenVibe is built, or send a pull request to the repository it touches.', href: 'https://openvibe.codes/', link: 'OpenVibe.Codes' },
+];
+
+function modPage({ examplesTag = 'v0.3.0' } = {}) {
+ const create = `npx --package=https://codeload.github.com/OpenVibers/OpenVibe.Examples/tar.gz/refs/tags/${examplesTag} create-openvibe-app my-mod --template mod`;
+ return html`<h1>Mod the network</h1>
+<p>OpenVibe is open source end to end, and every product has an API. These are the ways to change how it works for you, for your community, or for everyone, today.</p>
+<h2 id="today">Extend it today</h2>
+<ul class="cards">${TODAY.map((c) => html`<li><a href="${c.href}"><strong>${c.title}</strong></a><span>${c.text}</span><a class="muted" href="${c.href}">${c.link} →</a></li>`)}</ul>
+<h2 id="packaged">Packaged mods</h2>
+<p>A packaged mod changes a product from the inside: a game's rules, an overlay on a stream. It is described by a <strong>mod manifest</strong> (<code>mods.mod-manifest@1</code> in openvibe-contracts): its id, version, publisher, target runtime, the capabilities it asks for, its assets and its budget. Installing one gives it its own principal on OpenVibe.Network, granted only the capabilities that were approved, and revoking that grant stops it at once.</p>
+<ol class="steps">
+<li><strong>Start one</strong> from the template: <code>${create}</code></li>
+<li><strong>Check its manifest</strong> against the pinned contracts on the <a href="/manifests/validate">manifest validator</a>, which also says which capabilities an install could grant.</li>
+<li><strong>Ask for it</strong> to be installed: open a proposal on <a href="https://openvibe.codes/">OpenVibe.Codes</a>.</li>
+</ol>
+<p class="muted">What waits: today only staff install mods (with the approved capabilities), because how a mod release is signed and how it is reviewed are open decisions for the owner (<a href="/docs/adr/ADR-013">ADR-013</a>, amendment of 2026-09-24). Mods that run their own code also wait for sandboxed hosting. Until then a mod is data plus the scripting each game's own sandbox provides. This page will say when that changes.</p>
+<h2 id="start">Where to start</h2>
+<ol class="steps">
+<li><a href="/projects">Create a project</a> and add a sandbox app: its sandbox capabilities are approved at once.</li>
+<li>Pick a card above, or start from an example: <a href="${EXAMPLES}">OpenVibe.Examples</a> (MIT, each with a smoke test).</li>
+<li>Read the <a href="/docs">platform reference</a> and the <a href="/docs/sdk">SDK</a>, generated from the same contracts the services enforce.</li>
+</ol>`;
+}
+
+module.exports = { modPage, TODAY };
@@ -137,6 +137,7 @@ ${raw(showcase.cta({ title: 'Start building', text: 'Sign in with your OpenVibe
@@ -137,6 +137,7 @@ ${raw(showcase.cta({ title: 'Start building', text: 'Sign in with your OpenVibe
const adr = (id) => docs.adrs.find((a) => a.id === id);
const adr = (id) => docs.adrs.find((a) => a.id === id);
// What shipped on OpenVibe.Services: the shared update log every OpenVibe site has.
// What shipped on OpenVibe.Services: the shared update log every OpenVibe site has.
r.get('/updates', (req, res) => page(req, res, { index: true, cache: PUBLIC_CACHE, title: 'What shipped on OpenVibe.Services', body: raw(frame.updatesBody({ service: 'services', siteName: 'OpenVibe.Services' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`) }));
r.get('/updates', (req, res) => page(req, res, { index: true, cache: PUBLIC_CACHE, title: 'What shipped on OpenVibe.Services', body: raw(frame.updatesBody({ service: 'services', siteName: 'OpenVibe.Services' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`) }));
+ r.get('/mod', (req, res) => page(req, res, { index: true, cache: PUBLIC_CACHE, title: 'Mod the network', crumbs: [{ label: 'Mod the network' }], body: require('./mod').modPage() }));
r.get('/policy', (req, res) => page(req, res, {
r.get('/policy', (req, res) => page(req, res, {
index: true, cache: PUBLIC_CACHE, title: 'Policy', crumbs: [{ label: 'Policy' }],
index: true, cache: PUBLIC_CACHE, title: 'Policy', crumbs: [{ label: 'Policy' }],
body: html`<h1>Policy</h1><ul class="cards">
body: html`<h1>Policy</h1><ul class="cards">
@@ -27,6 +27,7 @@ const NAV = [
@@ -27,6 +27,7 @@ const NAV = [
{ label: 'Docs', href: '/docs' },
{ label: 'Docs', href: '/docs' },
{ label: 'OAuth', href: '/oauth' },
{ label: 'OAuth', href: '/oauth' },
{ label: 'Webhooks', href: '/tools/webhooks' },
{ label: 'Webhooks', href: '/tools/webhooks' },
+ { label: 'Mod', href: '/mod' },
{ label: 'Manifests', href: '/manifests/validate' },
{ label: 'Manifests', href: '/manifests/validate' },
{ label: 'Policy', href: '/policy' },
{ label: 'Policy', href: '/policy' },
];
];
new file mode 100644
new file mode 100644
@@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
+'use strict';
+/**
+ * /mod (server/http/mod.js): a public, indexable page that lists every way to extend OpenVibe today, each card with a
+ * link, says plainly that packaged mods are installed by staff until ADR-013's open decisions are made, and is in the
+ * navigation, the sitemap and llms.txt.
+ */
+const assert = require('assert');
+const { boot, check, done } = require('./helpers/boot');
+const { TODAY } = require('../server/http/mod');
+
+(async () => {
+ const t = await boot();
+ try {
+ await check('the page: every card, the honest state of packaged mods, links that go somewhere', async () => {
+ const r = await t.get('/mod');
+ assert.strictEqual(r.status, 200);
+ assert.match(r.text, /<h1>Mod the network<\/h1>/);
+ TODAY.forEach((c) => {
+ assert.ok(r.text.includes(c.title.replace(/'/g, ''').replace(/"/g, '"')), `missing card: ${c.title}`);
+ assert.match(c.href, /^https:\/\//, `${c.title} links out over https`);
+ });
+ assert.match(r.text, /only staff install mods/);
+ assert.match(r.text, /href="\/docs\/adr\/ADR-013"/);
+ assert.match(r.text, /create-openvibe-app my-mod --template mod/);
+ assert.ok(!/noindex/.test(r.text), 'indexable');
+ });
+ await check('found from the navigation, the sitemap and llms.txt', async () => {
+ assert.match((await t.get('/')).text, /href="\/mod"/);
+ assert.ok((await t.get('/sitemap.xml')).text.includes('<loc>https://openvibe.services/mod</loc>'));
+ assert.ok((await t.get('/llms.txt')).text.includes('https://openvibe.services/mod'));
+ assert.ok((await t.get('/llms-full.txt')).text.includes('Mod the network'));
+ });
+ } finally { await done(t); }
+})();