openvibers/openvibe.services README.md

README.md

main 33.6 KB · 295 lines · markdown

1# OpenVibe.Services
2
3> The developer platform of the OpenVibe network: one console for projects and apps over OpenVibe.Network, scoped credentials, capability grants, usage and project export; app and mod releases; API, contract, SDK and event reference generated from what production runs; OAuth and webhook tools; and one resource index over every service.
4
5**Live site:** https://openvibe.services · **API docs:** https://openvibe.services · **For AI:** https://openvibe.services/llms.txt
6**Run it:** `npm ci && npm run dev` (Node 22 or newer) · **Tests:** `npm test`
7
8**Status:** alpha. Serves `https://openvibe.services` (openvibe-ovh, unit `openvibe-services` on 127.0.0.1:4930 behind nginx). The developer portal moved here from OpenVibe.Codes on 2026-10-08 (owner decision: Codes became the open coding-agent harness); its production data then was nil (0 manifests, 0 releases, 0 playground runs), so nothing was copied, and openvibe.codes answers its old portal paths with permanent redirects to the same paths here. Tests run against in-process stand-ins for Network, Events and Media. See [STATUS.json](STATUS.json) for exactly what works and what does not.
9**Domain:** `openvibe.services` · **Port:** 4930 · **Service id:** `services`
10**Plan:** plan T13 (the developer platform and the resource control plane); binding decisions ADR-014 (the portal over Network's projects API) and ADR-048 (the control plane) in OpenVibe.Contracts.
11**License:** AGPL-3.0 (same as every OpenVibe service).
12
13## Purpose
14
15A place where a developer outside the network goes from an OpenVibe account to a working, scoped integration using only public docs, the SDK and their own credentials. Services is a **console**: the identity authority (OpenVibe.Network) owns projects, apps, credentials, grants and quotas (ADR-014), and Services shows and forwards what the signed-in person asks for, with that person's own Network token.
16
17## Owns
18
19- **Release metadata** keyed to Network app ids: validated app and mod manifests, releases (`draft → published → deprecated → revoked`), an append-only release log.
20- **The merged resource index** (ADR-048, `services.resource.read`): every authority's resources, read from each one and merged, never stored as the truth; Services' own manifests (`services.manifest`, `mfs_`) and releases (`services.release`, `rel_`) are read in process. Trust tiers and playground runs are metadata and audit, never resources.
21- **Trust tiers** per ADR-013 (`unreviewed`, `reviewed`, `first-party`) — **metadata only**: a tier changes defaults and discovery, never a grant check. Databases written with the earlier four names are migrated at boot (untrusted→unreviewed, verified/trusted→reviewed, platform-maintained→first-party), idempotently.
22- **Playground run logs** (who ran what, outcome, stage, problem code; never a credential).
23- The events `services.app.published`, `services.app.deprecated`, `services.app.revoked`, and `services.moderation.action` for staff revocations and trust tier changes (the network's moderation audit log), through the openvibe-sdk transactional outbox.
24- The generated reference (rendered from the pinned packages at boot; nothing hand-written that can drift) and the console's pages.
25
26## Does not own
27
28- Projects, members, apps, credentials, grants, allowances, quotas, audit: **OpenVibe.Network** (`/api/v1/projects`). Services never copies them.
29- Secrets: Network returns a client secret once; Services renders it in that one response (`Cache-Control: no-store`) and keeps no copy. Tests scan every table, every log line and every later response for each secret.
30- Token issuance and quota enforcement: Network issues tokens; the service that owns a capability enforces its quota. Services labels quotas "recorded limit — enforced by `<service>`".
31- The registry (Network) and the contracts (OpenVibe.Contracts).
32
33## What is here
34
35| Surface | Path | Notes |
36|---|---|---|
37| Portal | `/projects`, `/projects/mine`, `/projects/:project`, `/projects/:project/resources`, `/projects/:project/apps/:app` | Create projects; add members; **your resources** (`/projects/mine`: what the signed-in person owns outside any project, such as robots, chat rooms, streams, watches and Actor tasks. Every authority is asked with `?owner=<their subject>` (openvibe-sdk 0.41), and the merged page is kept to their own rows in no project, so an authority without the filter yet never shows another person's. Only the session's own subject is ever asked for); **resources** (every member and staff: `/projects/:project/resources?service=&kind=`, the merged index for that one project, server-rendered: name and OVRN, kind, state, owner, created and updated, 50 a page on the index's own cursor; Network confirms membership before any authority is asked, and an authority that could not be read is named with the problem it gave); **secrets** (`/projects/:project/secrets`, ADR-056: Network's metadata for every member, name, version, fingerprint and which services may sign with each or receive it for which origins; admins and owners set a value, which passes through to Network once and is never shown, stored or logged here, delete it and grant or revoke services); create sandbox/production apps (secret shown once); rotate and revoke credentials; request, approve, deny and revoke grants through a scope editor that offers only grantable capabilities (public, or partner when in the allowance) with each one's description, owner and visibility; quotas; **usage** (owner, admins and staff: `/projects/:project/usage?days=7|30|90&env=all|production|sandbox`, Network's `GET /api/v1/projects/:project/usage` rendered server-side: usage by service, capability and day, each recorded quota's headroom as a `<meter>`, the published limits of the services used (their `/limits.json`), errors by code and the sampled failures with their trace ids and job or event ids; the numbers come from the services' hourly `*.usage.recorded` rollups, which Network adds up per day, so the current hour is not counted yet); audit (admin+); **export** a project's metadata as one JSON document (`/projects/:project/export`: Network's project, members, apps with credential ids and hints, grants, quotas and, for admin+, audit, plus Services' releases with manifests and logs, trust tiers and playground runs; never a secret); owners and admins also download the **full archive** (`POST /projects/:project/export/archive`, one zip: that document with the audit log, the release manifests, the project's Media objects with download URLs and its namespaces, and its retained app events, for production and sandbox; read with Network's read-only export tokens; all or nothing when a service fails; [format](https://openvibe.services/docs/export)) and **delete** a project (owner, confirmed by name: Network archives it — it offers archiving, not erasure — then Services deletes drafts, their manifests and playground runs, and revokes published releases so installers are told). Network's errors are shown with its status, code, detail and request id. |
38| OAuth helper | `/oauth`, `/oauth/test-callback` | Explains authorization code + PKCE (S256) for apps, builds a test authorize URL, and a callback that shows `code`/`state`/`error` and **never exchanges** the code. |
39| Webhook tools | `/tools/webhooks` | Verify a delivery as a receiver that requires signature v2 does: `X-OpenVibe-Signature-V2: t=<ts>,v2=<HMAC of "<ts>.<raw body>">` must match and `t` must be within ±300 s of now (the page shows the age and the window, and tells a stale-but-correct signature from a wrong one); `X-OpenVibe-Timestamp` must equal `t`. v2 only: the v1 header (`X-OpenVibe-Signature`) was retired on 2026-09-28 and has no field. In the browser with Web Crypto, or on the server without JavaScript with openvibe-sdk's `verifyDeliveryV2`; constant-time; secret dropped. Generate a sample delivery signed as Events signs it (`X-OpenVibe-Timestamp` and v2, `signDeliveryHeaders`) for any event type the contracts catalog lists. |
40| Docs | `/docs/*` | Contracts (field tables, fixtures as examples, raw schema), capability catalog (grantable ones highlighted), event types, SDK reference from its `.d.ts` files, ADRs. Every page states the versions it was generated from. The service registry is read live from Network, with health as Network reports it. |
41| Playgrounds | `/projects/:project/apps/:app/playground` | Media upload (sandbox, `media.object.upload`) and Events publish (`events.app.publish`: types `app.<project_key>.*`, source `app-<app ULID>`), run **as the app** with its own token (from its secret typed for that one request, or a pasted token). Refused — with the missing grant named, and nothing requested or called — unless Network lists the grant as approved. Sandbox apps only. |
42| Manifests | `/manifests/validate`, `/projects/:project/apps/:app/releases/new` | Validate `services.app-manifest@1` and `mods.mod-manifest@1` with openvibe-contracts; create, publish, deprecate and revoke releases. |
43| Resource index | `GET /api/v1/resources`, `GET /api/v1/resources/:ovrn`, `GET /api/v1/authorities` | The merged index over every authority (ADR-048, `services.resource.read`): see [The merged index](#the-merged-index) below. |
44| Resource control | `POST /api/v1/resources/control` | One `common.resource-control-request@1`, routed to the authority that owns the resource (ADR-048, `services.resource.control`): see [Control operations](#control-operations) below. |
45| Policy | `/policy/*` | Proposal process and the decision record; compatibility and deprecation policy rendered from ADR-002 and ADR-016 as published; licensing read from package metadata; transparency (what Services stores and does not). The community documents — code of conduct, contributing, the contributor ladder and the moderation policy — are OpenVibe.Codes' (`https://openvibe.codes/policy/<slug>`); their old addresses here redirect there. |
46| API | `/api/v1/*` | Public release reads (`services.release.read`: no token needed; a presented token must hold it), manifest validation, docs versions; release management by an app with its own token (`services.release.manage`); Services' own resource index (`services.resource.read`, first-party). Guarded with openvibe-contracts `requireCapability`. RFC 9457 problems. |
47| Machine | `/api/health`, `/api/ready`, `/release.json`, `/metrics` | Readiness is truthful (db and docs required; Network, JWKS, OAuth client and events relay reported as optional checks). `/metrics` answers loopback callers only. |
48| Telemetry | — | Every HTTP request is aggregated by route template, method and status class, never by raw URL; `/shared` files, static assets, health, readiness and `/metrics` are not counted. Once a minute one `http.request` sample per key, plus the active-requests, p95 latency and event-loop-lag gauges, is written to this service's own `telemetry_samples` table (openvibe-sdk 0.46 telemetry, the `platform.telemetry-sample@1` shape; migration `0006_telemetry.sql`). Rows older than 14 days are deleted daily. `server/app.js` mounts openvibe-sdk's `telemetryMiddleware` (a no-op until init); `server/index.js` calls `telemetry.init()` at production start, runs the 14-day retention and flushes on stop. |
49| Crawl artifacts | `/robots.txt`, `/sitemap.xml`, `/llms.txt`, `/llms-full.txt` | Built with openvibe-shared/seo for search and AI crawlers. robots.txt keeps every disallow (the portal, staff, sign-in and the API are not crawlable) and names the sitemap; the sitemap lists the fixed public pages with STATUS.json's content date and each public app and published release with its real publish time (never "today"); llms.txt maps the site; llms-full.txt lists every fixed public page the sitemap does, one title and one line of text each. |
50
51Everything is server-rendered and usable without JavaScript. The only scripts of Services' own are optional: in-browser webhook verification and a copy button. The coding-harness catalog that used to sit under `/docs/harnesses` is OpenVibe.Codes' (`https://openvibe.codes/harnesses`).
52
53## Who may read the index
54
55- **A Network service token** for audience `openvibe.services`, `svc:` principal, holding `services.resource.read`
56 (first-party): the whole index, over every authority and every project; `?project=` only narrows it. An app, mod
57 or agent token is refused even if it somehow holds the grant — a person reads with their own user token, never
58 through a developer app's credential.
59- **A person's Network user token**: only projects that person owns or belongs to, and only ONE at a time —
60 `?project=prj_…` (or the OVRN's own project) is required, and Network answers the membership question
61 (`GET /api/v1/projects/:project`, the developer projects API) with the caller's own token. A project the person is
62 not a member of is `404 resources.project_not_found`, the same non-disclosure Network itself uses — never a `403`
63 that would confirm the project exists. A person is never accepted unscoped.
64
65`GET /api/v1/authorities` is first-party operational data (loopback origins) and answers only a service token.
66
67## The merged index
68
69The fan-out, each authority's opaque-cursor walk and the merge are `openvibe-sdk/resources`' `createResourceIndex`
70(SDK v0.34.0, `docs/service.md` §"The resource index and the control plane") — Services does not reimplement any of
71it. Services adds:
72
73- **the authority set** — one adapter per authority (`server/authorities/adapter.js`), each calling that authority's
74 loopback origin with its own Network client-credentials token (`server/authorities/tokens.js`: one cached token per
75 audience, scoped to that authority's `resource.read` capability, refreshed 60 s before expiry);
76- **a deterministic total order** over the merged resources (service, OVRN, id) and an opaque keyset cursor into it,
77 so a page walk repeats nothing and skips nothing while the underlying data is stable;
78- **local filters** for `project`, `kind` and `service` on top of the authorities' own answers: a scoped read (a
79 person's, above all) never shows a row outside its scope even if an authority ignores a query parameter;
80- **the partial answer**. `common.resource-list-result@1`'s `partial` array (contracts 1.1.0) names the authorities
81 a page could not read, each with the problem code it answered: a page whose fan-out was incomplete omits those
82 rows and carries `partial: [{ service, code }]`, and a complete page carries no `partial` key at all. The code is
83 the authority's own problem code when it answered one, and Services' own `resources.authority_timeout` /
84 `resources.authority_unavailable` when nothing answered (a timeout, or a network or token-mint failure) — nothing
85 new is registered in contracts.
86
87An authority is never required to answer for the index to answer: a timeout (default 5 s per authority), a refusal,
88a malformed page or a Network that cannot mint its token makes that authority partial and leaves the rest of the page
89intact.
90
91## Control operations
92
93`POST /api/v1/resources/control` (plan T13 step 10) takes one `common.resource-control-request@1` and answers the
94owning authority's `common.resource-control-result@1` (`200`, or `202` when `pending`). Services applies nothing:
95
96- **Who may call.** A first-party `svc:` token holding `services.resource.control` (any project), or a person's own user
97 token for a project they **own or administer** (Network answers the role; not a member is `404`, a member below admin
98 `403`). A person acts for themself only: `on_behalf_of` is absent or names them, and Services sends it on.
99- **Routing.** The body is checked with `contracts.resources.checkControlRequest` (`400 control.bad_request`; the
100 contract ties `project_id` to the resource's own project). The authority is the OVRN's service, or `resource_kind`'s
101 for `create` (`400 resources.unknown_service` otherwise), and the request is forwarded through `openvibe-sdk/resources`
102 `createResourceClient` with Services' token for that authority's own `<id>.resource.control`. An authority whose
103 manifest lists no active `<id>.resource.control`, or whose control path answers 404/405/501, is `failed` with
104 `resources.authority_unsupported`. Today that is every authority (none ships a control endpoint yet), and Services'
105 own resources too. An answer that is not a valid result, or names another project's resource, is `failed` with
106 `resources.authority_bad_response`. An authority's own `refused` (with its `confirmation_required`) passes through.
107- **Idempotency.** `services_control_log` holds one row per `idempotency_key` (UNIQUE). A repeat gets the stored answer
108 and the authority is not asked again. A repeat while the first is still running is `409 control.in_progress`. The key
109 reused by another caller or for another request is `409 control.idempotency_conflict`.
110- **Sensitive actions (WS-Z2).** A request made for an agent (`on_behalf_of: { type: 'agent' }`) that deletes, spends
111 money (a money field in `params`) or publishes is `refused` with `confirmation_required: { confirmation_id, reason }`.
112 Network's confirmations (`/internal/confirmations`, `network.confirmation.manage`) mint the id and ask the agent's
113 owner. The retry carries `confirmation_id`. Services spends the approval once, for exactly that request (its
114 digest), before the authority is asked. A standing rule's approval proceeds at once. A dry run, and a person acting
115 with their own token, are not gated here.
116- **Record.** One `services_audit` row per accepted request (a replay too), written with the log row's answer
117 (migration `0004_control.sql`). Neither table stores `params` (only their digest) or a token.
118
119## The authority registry
120
121`server/authorities/index.js` builds the registry at boot from the **pinned openvibe-contracts service manifests**:
122every service whose manifest lists `<id>.resource.read` **and** whose capability manifest says `status: active` is an
123authority. With contracts v0.110.0 that is **network, events, host, codes and media** (services.resource.read is
124Services' own id, and Services is never its own authority); the moment a service's manifest turns that on, it is in
125the registry on the next boot, and nothing here is hand-maintained. Each authority's loopback origin comes from its
126manifest (`internalOrigin`); `SERVICES_<ID>_URL` overrides one for the operator. The audience Services mints for is
127`openvibe.<id>`.
128
129There is no polling loop: the index reads the authorities on each request (step 6). The rebuildable read model and
130`*.resource.*` ingestion are step 7.
131
132## Depends on
133
134- **OpenVibe.Network** — SSO (OAuth client `services`, PKCE S256), JWKS, `/api/v1/projects` (called server-side with the person's Network access token), the registry (`/api/v1/registry/services`, `/.well-known/openvibe`), client-credentials tokens (Services' own for the events relay; the app's own in playgrounds), and the WS-Z2 confirmations (`/internal/confirmations`) for sensitive control operations.
135- **OpenVibe.Events** — the outbox relay publishes `services.app.*` with Services' service token (`events.event.publish`); the Events playground calls it with the app's token (`events.app.publish`); the project archive pulls the project's app events with a Network export token (`events.app.read`).
136- **OpenVibe.Media** — the Media playground uploads with the app's token into the project's namespace; the project archive lists the project's objects, namespaces and download URLs with a Network export token (`media.object.list`, `media.object.read`).
137- **PostgreSQL 18 and Valkey 9** (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through `openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional).
138- **openvibe-contracts v0.154.0**, **openvibe-sdk v0.161.0**, **openvibe-shared v4.33.0** (pinned tag tarballs; the docs show the tag and the package version, and say so when they differ).
139
140No path in Services sends or accepts a shared loopback key (tested by grep and at runtime).
141
142## Grants and registration the lead must add
143
144Implemented here (the service manifest's `capabilities`): `services.release.read` (public release reads;
145no token needed, a presented token must hold it) and `services.release.manage` (release management by an
146app with its own token), both guarded with openvibe-contracts `requireCapability`; and
147`services.resource.read` (the authority resource index, ADR-048: `GET /api/v1/resources` and
148`GET /api/v1/resources/:ovrn`), first-party — the capability is served here, so a caller presents a
149service token holding it.
150
151Called elsewhere: as the service principal `services`, `events.event.publish` at Events (the outbox relay);
152as the signed-in person, with their own Network token, Network's `/api/v1/projects`; as the app, with
153its own token, `media.object.upload` (Media) and `events.app.publish` (Events) in the playgrounds; and,
154for the project archive, Network's read-only export tokens holding `events.app.read`,
155`media.object.list` and `media.object.read`.
156
157In Network (`server/identity/principals.js` / `server/db/database.js`, as for coupons and host):
158
159- OAuth client `services`, name `OpenVibe.Services`, redirect URI `https://openvibe.services/auth/callback`.
160- Service grant `['services', 'events.event.publish', 'openvibe.events', []]` (the outbox relay).
161- Control operations (plan T13 step 10): `['services', 'network.confirmation.manage', 'openvibe.network', []]` (the
162 sensitive-action gate; until it is granted, a gated request fails with `confirmations.unavailable` and nothing is
163 applied), and `['services', '<id>.resource.control', 'openvibe.<id>', []]` for each authority once it ships a control
164 endpoint and registers that capability.
165- Account export and deletion (ADR-033): `['services', 'events.subscription.manage', 'openvibe.events', []]` (the two
166 account subscriptions, created at boot), then, last and once the release is live,
167 `network.account.export.contribute` and `network.account.deletion.confirm` for audience `openvibe.network`.
168
169### Account export and deletion (ADR-033)
170
171`network.account.export_requested` and `network.account.deleted` arrive at `POST /internal/events`. The route is
172loopback-only (nginx answers 404 for `/internal/`, and the handler refuses a forwarded request) and signed with
173`SERVICES_EVENTS_SECRET`. They are answered by `server/domain/account-data.js` over `openvibe-sdk/account-data`, with
174one receipt per export and deletion in `account_data_events` (migration 0003).
175
176- **Export:** the playground and recipe runs the person started, and the manifests and releases they created.
177- **Deletion:**
178 - Their runs are deleted.
179 - Manifests and releases belong to their project and stay: `created_by` becomes `deleted` and the publisher,
180 deprecator and reviser become NULL.
181 - The resource index's owner becomes NULL.
182 - The append-only release log, the control log and the staff trust tiers stay attributed, for accountability.
183
184The Services service manifest, `services.release.manage|read`, `services.resource.read` and `services.app-manifest@1` are released in openvibe-contracts v0.150.0 (they replaced the `codes.*` forms, retired that release); the CI contracts check is blocking.
185
186For the playgrounds to succeed end to end (not Services' code; configuration elsewhere): Network `DEV_SANDBOX_AUDIENCES` including `openvibe.media` and `openvibe.events`, a staff-set allowance containing `media.object.upload` and `events.app.publish` for the project, a Media tenant keyed by the project id, and OpenVibe.Events serving `events.app.publish` for app tokens. On 2026-09-23 these were in place on production: a sandbox app uploaded to Media (tenant `prj_…-sandbox`) and published and read app events through public endpoints. That run used curl, not the playgrounds, and it is not yet a committed, repeatable check.
187
188## Configuration
189
190See [.env.example](.env.example). Required in production: `OV_OAUTH_CLIENT_SECRET`, `SERVICES_FORM_SECRET`, `BASE_URL`, `DATABASE_URL` (and `DATABASE_DIRECT_URL` for the boot migrations, run as the owner). Optional: `EVENTS_URL` (relay), `INDEXNOW_KEY` (see below), `SERVICES_STAFF_SUBJECTS`, `SERVICES_PLAYGROUND_*`, `SERVICES_REGISTRY_TTL_MS`, `SERVICES_LIMITS_MINUTE` / `SERVICES_LIMITS_HOUR`.
191
192### IndexNow (openvibe-shared/indexnow)
193
194With `INDEXNOW_KEY` set (8–128 hex or alphanumeric characters, what IndexNow's own tools
195generate), the key file is served at `/<key>.txt` as `text/plain` and every public release
196transition tells the engines: publish, deprecate and revoke of a published or deprecated release
197ping `api.indexnow.org` with the release page (while it is published), the app page, `/updates`
198and `/sitemap.xml`; the module batches and debounces these. Nothing pings for a draft, and a
199release that was never public stays silent. Unset: the feature is off — no key file, nothing sent
200(what tests and drills do). The key is not a secret in the credential sense: engines fetch it by
201design. `test/indexnow.test.js`.
202
203### Per-actor limits
204
205`/api/v1`, the console (`/projects`), release actions, the tools' forms and the staff trust form also
206limit who calls them, once the caller is known and before any work (before a form or upload is read,
207before Network is asked): `server/http/actor-limits.js`, openvibe-sdk/limits, roadmap WS-R task 4. An
208app token counts as its app (`app:app_…`), a signed-in person as `user:usr_…`, anyone else by address.
209Past a limit: `429` problem+json `rate_limited` with `Retry-After`, one `[Limits]` log line and
210`services_rate_limited_total{limit,window}`. The per-address limits (pages 300 a minute, console writes 60,
211tools 30, API writes 60, sign-in) and the playground's runs an hour stay. Services hosts no git remotes, so
212there are no clone or push routes to leave out.
213
214| Routes | Per caller, a minute / an hour |
215|---|---|
216| API reads; console pages (each asks Network with the person's token) | `SERVICES_LIMITS_MINUTE` / `SERVICES_LIMITS_HOUR` (120 / 3000) |
217| Project create | 5 / 30 |
218| Members, roles, archive, delete | 20 / 200 |
219| Project export (JSON and the full archive) | 3 / 20 |
220| App create | 10 / 60 |
221| Redirect URIs, grants, app revoke | 30 / 300 |
222| Credential rotate and revoke | 10 / 60 |
223| Playground runs (above the 60 an hour per person) | 10 / 120 |
224| Release create and "validate only" (console and API) | 20 / 200 |
225| Release publish, deprecate, revoke (console and API) | 20 / 200 |
226| Manifest validate (form and API); webhook verify and sample | 30 / 600 each |
227| Staff trust tier | 30 / 300 |
228
229Never limited per actor: `/api/health`, `/api/ready`, `/release.json`, `/metrics`, sign-in, and the
230public pages and docs. `test/actor-limits.test.js`; the other tests boot with the per-actor limits off,
231as they raise the per-address limit.
232
233## Deploy (for the lead)
234
235Production deploys with `sudo ovhost deploy services` on the host (strategy `git-checkout`: fetch,
236fast-forward `/opt/openvibe.services`, install on a lockfile change, restart, wait for `/api/ready`).
237The unit is `openvibe-services.service` on `127.0.0.1:4930`, the env file `/etc/openvibe/services.env`. The database is
238`ov_services` on the host's data role (`sudo /opt/openvibe.host/roles/data/add-service.sh services` writes its settings); the
239release migrates it at boot. nginx serves `openvibe.services` from
240[deploy/nginx/openvibe.services.conf](deploy/nginx/openvibe.services.conf).
241Rollback: ovhost puts the previous sha back by itself when `/api/ready` does not answer 2xx after the
242restart; afterwards `sudo ovhost rollback services --to <sha>`. A release from before the console (the bare index,
243before 2026-10-08) never reads migration 0002's tables, so a rollback across it serves the index again and the console
244pages answer 404 until the next deploy.
245
246First install (done once; kept for a rebuild):
247
2481. `git clone` to `/opt/openvibe.services`; `npm ci --omit=dev` with Node 22.
2492. `/etc/openvibe/services.env` from `.env.example` (secrets from the Network client registration).
2503. `deploy/systemd/openvibe-services.service` → `/etc/systemd/system/`; `systemctl enable --now openvibe-services`.
2514. `deploy/nginx/openvibe.services.conf` → `/etc/nginx/sites-available/`, certificate for `openvibe.services` + `www`, reload nginx.
2525. Check `curl -s http://127.0.0.1:4930/api/ready`.
2536. Only when the launch rule below holds: remove `openvibe.services` from `OpenVibe.Sites/sites.json` and switch routing (done on 2026-10-08, when the console moved here).
254
255## Development
256
257```bash
258npm install
259fnm exec --using=22.22.1 npm test # every test/*.test.js on temp PGlite databases with mock Network, Events, Media
260fnm exec --using=22.22.1 npm run dev # http://localhost:4930
261npx openvibe-contracts-check --service services --src server
262```
263
264Tests: the crawl artifacts (robots.txt, sitemap.xml, llms.txt and llms-full.txt — the public pages only, llms-full.txt listing every page the sitemap does, and lastmod from STATUS.json, never the clock); ADR-013 trust-tier migration; secrets never persisted or re-displayed; Network errors surfaced honestly (404/403/409/422/503, unreachable, expired session refresh); scope editor offers only grantable capabilities and refuses forged requests before Network sees them; generated docs match the pinned versions (every contract, capability, event type, SDK module); webhook verification, v2 only (tampering, lengths, malformed headers, the ±300 s window both ways, timestamp header mismatch, a v1-only delivery refused, samples without v1, constant-time, the in-browser verifier agreeing); playgrounds refuse without the grant and call nothing; the usage page (owner, admins and staff only, Network not asked for anyone else, filters forwarded as offered, a Network failure shown as it answered); the resources pages (your resources: only the signed-in person's own rows in no project, even from an authority that ignores `?owner=`, never another subject named in a query; a project's: any member, a stranger refused before any authority is asked, nothing of another project, malformed filters refused, unreadable authorities named, names escaped, a cursor walk that never repeats or skips a resource); project export (complete, no secrets, audit for admin+ only, never partial) and delete (owner only, Network first, drafts and runs removed, public releases revoked with an event); manifest validation; release lifecycle and events; the authority resource index (auth matrix — no token 401, a token without `services.resource.read` 403, with it 200 — every page and read valid against `common.resource-list-result@1`/`common.resource-summary@1`, `?project=` never returning another project's rows, `?kind=`, cursor paging, `GET /:ovrn` found and 404, a bad query 400); no internal key anywhere; PKCE sign-in; readiness, release.json, loopback metrics; the released services manifest matching the code; IndexNow (off without a key, the key file served with a key, a publish pinging the page and the sitemap, a draft never pinging); and the home page's size budgets (`test/perf-budget.test.js`, openvibe-shared/perf-budget).
265
266## Acceptance (must be true before "done")
267
268- A new external developer goes from account to a working Media, event and capability integration with only public docs, the SDK and scoped credentials — no loopback key. **Partly shown: one production run on 2026-09-23 covered account, project, sandbox app, auto-approved grants, Media upload and app event publish/read with public endpoints (curl, not the SDK or the playgrounds). It is not committed as a repeatable check, and it did not cover webhook delivery to an external endpoint, publishing a release or revoking credentials.**
269- Credentials can be inspected, scoped, rotated and revoked. **Yes, through Network's API (tested against a stand-in; not yet used on production).**
270- The playground cannot exceed its project's grants. **Yes (tested).**
271- A published app or mod release carries compatibility and trust metadata. **Yes (tested).**
272
273## Launch rule
274
275The domain kept its placeholder page on [OpenVibers/OpenVibe.Sites](https://github.com/OpenVibers/OpenVibe.Sites) until all of the following existed (plan §12.12). **The portal left Sites as openvibe.codes on 2026-09-23 and moved to `openvibe.services` on 2026-10-08.** The items: an owning runtime with health/readiness and observability (**done**); canonical identity/auth integration (**done; the OAuth client `services` carries the redirect URI `https://openvibe.services/auth/callback`**); server-rendered public routes useful without JavaScript (**done**); real persistence and end-to-end workflows (**persistence done; the external-developer path ran once on production with curl, not yet through the console**); capability and event registration against OpenVibe.Contracts (**released**); a migration/seed strategy (none needed: no data is imported; raw old developer keys are never imported), a security review and sitemap/robots behaviour (**sitemap and robots done; the security review is the threat notes below, self-authored**); acceptance tests proving the advertised functionality (**against stand-ins**). Roadmap binding: do not present Services as a working developer platform where these paths are mocked.
276
277## Security (threat notes)
278
279Reporting a vulnerability: [SECURITY.md](SECURITY.md).
280
281- Session tokens are httpOnly (unlike the navbar-readable `ov_token` elsewhere) because this site displays secrets; forms carry an HMAC form token and require a same-site `Origin`.
282- Secrets never reach logs: errors are logged without request bodies; playground failure details are scrubbed of the credential before they are stored or shown.
283- Services never fetches a URL a developer types (the webhook tester produces a `curl` for their own endpoint instead).
284- Grant checks happen twice: Services refuses to forward non-grantable capabilities, and Network decides. Playgrounds verify the app token's signature, subject, project, environment, audience and capability before calling anything.
285
286---
287
288Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers).
289
290<!-- versions:start -->
291- openvibe-contracts: v0.419.0
292- openvibe-sdk: v0.161.0
293- openvibe-shared: v4.241.0
294<!-- versions:end -->
295