openvibers/openvibe.network README.md

README.md

main 70.2 KB · 1043 lines · markdown

1![OpenVibe.Network](https://openvibe.network/shared/social/network.jpg)
2
3# OpenVibe.Network
4
5**One account for every OpenVibe site** — [openvibe.network](https://openvibe.network). Sign-in, profiles, follows, OpenCoins and your machines.
6
7**Live site:** https://openvibe.network · **API docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt
8**Run it:** `npm ci && npm run dev` · **Tests:** `npm test`
9
10Part of the [OpenVibe network](https://openvibe.network); [help](https://openvibe.help), the [SDK](https://github.com/OpenVibers/OpenVibe.SDK) and the [developer API](https://openvibe.services).
11
12## Purpose
13
14Identity and account service for the OpenVibe network. Manages user accounts, OAuth2 authorization, theme preferences, notifications, email alerts, anonymous identities, the network-wide OpenCoins wallet, and internal APIs that connect all OpenVibe services together.
15
16**Part of the [OpenVibe network](https://openvibe.network)** — `https://openvibe.network`
17
18---
19
20## What it does
21
22- **SSO Identity Provider** — "One Account. All of OpenVibe." Central registration and login, OAuth2 Authorization Code flow for OpenVibe.Live, OpenVibe.Tools, OpenVibe.Games, and OpenVibe.Media.
23- **Refresh tokens** — stored only as a SHA-256, rotated on every use as the next `generation` of the sign-in's `family_id`; presenting an already rotated token again (after a 10-second grace for two tabs refreshing at once) revokes the whole family and is audited (`oauth_refresh_reuse`). Rows from before this change still work: a raw row is hashed the first time it is presented. `server/auth/refresh-tokens.js`.
24- **OpenID Connect** — discovery at the issuer's root, `/.well-known/openid-configuration` (also RFC 8414's `/.well-known/oauth-authorization-server`; the older `/oauth/...` alias was retired in plan T2). With scope `openid` the code grant adds an RS256 `id_token` (aud = client, the authorize `nonce` echoed, `sub` = the Network id as a string, `subject_id` = the canonical usr_ id); `/oauth/userinfo` returns the same claims for an access token (`server/auth/oidc.js`).
25- **Unified Notification System** — Cross-service notifications with priority levels, category filtering, toast popups, bell badge, sounds, and rich content (buttons, inputs, media). All services push notifications to the central API; clients poll every 15 seconds.
26- **Email Alerts** — Critical notifications (moderation actions, system alerts) can be delivered by the built-in email service. Configuration is managed through the admin panel.
27- **Anonymous Users** — Browse and interact without an account. Anon users receive a unique number, can accumulate stats, and optionally link to a registered account later.
28- **Multi-Account Switching** — Google-style account management. Users can add multiple accounts and switch instantly, including an anonymous mode.
29- **Theme Catalog** — Shared theme system with ~30 built-in themes and community submissions. Theme preferences sync across all services.
30- **Per-site theme overrides** — one theme follows a person to every site, and an override is their exception for one service ("Games in Neon Tokyo", "Live in Daylight"). `GET /api/themes/me/active?service=<id>` answers that service's theme (`override: true`, with the `service` named) or the person's global theme (`override: false`); a missing or unknown service answers exactly as before. The person owns their own list at `GET /api/themes/me/overrides`, `PUT /api/themes/me/overrides/:service` and `DELETE /api/themes/me/overrides/:service` (the theme and the custom colours are validated as `PUT /api/themes/me` does), at most 50 per person, and every change is kept in the theme history with the service recorded.
31- **Admin Panel** — email configuration, user management (role changes, bans), broadcast notifications, system health dashboard, and audit log.
32- **Setup** (`/admin#setup`, owner only) — one checklist of everything only the owner can give the network: keys, accounts, caps and a few registrations. It lists every field a service's config namespace marks `x-owner-input` (openvibe-shared 4.125.0), reached through the owner settings proxy (`server/admin/ai-config-proxy.js`, seventeen services), Needed first, then Not working, Connected and Optional, with a counted summary. A key is written once through the proxy (fresh sign-in, the service's vault, audit, alert) and never shown again; **Test** asks the service to try it and keeps the one-line answer; Network's own to-dos (legal registrations, DNSSEC at the registrar, a top-up) are its `network.setup` namespace with **Mark done** (`server/admin/setup.js`). The AI tab and every admin page explain their words; `/admin#glossary` lists them.
33- **Reviewer queue** (`/admin/review`) — one staff-only page for every Wiki page, Blog draft, AI brief and Trade company page waiting for a person, grouped by site with the publishing gate's noindex reason in words. Approve (indexable) or keep hidden calls the owning service's own review route and writes its own review log.
34- **Internal API** — Server-to-server endpoints for token verification, user lookup, notification push, account linking, and audit logging. Every route takes a capability-scoped service token; the deployment-wide key they used to accept was retired on 2026-09-29 (plan T2, [docs/retirement.md](docs/retirement.md)) and opens nothing.
35- **Developer projects (foundation)** — projects, members, apps (OAuth clients with `app_` subjects), hashed client secrets with rotation and revocation, capability grants within a staff-set allowance (sandbox apps also get a default sandbox allowance of public Media, Events-app and Tools-job capabilities, so a new project works without staff), recorded quotas, per-project usage from the services' hourly rollups (`GET /api/v1/projects/:project/usage`) and an append-only audit at `/api/v1/projects`. Its events can be relayed to OpenVibe.Events through an outbox when `OV_EVENTS_INTERNAL_URL` is set. Bearer user tokens only; the console over this API is OpenVibe.Services at openvibe.services (it moved there from OpenVibe.Codes on 2026-10-08). See [docs/developer-projects.md](docs/developer-projects.md).
36- **Public discovery** — `/.well-known/openvibe`, `/api/v1/registry/*` and `/contracts/*.json` (services, capabilities, contract schemas) answer any origin with `Access-Control-Allow-Origin: *`, preflight included, so a browser app can discover the platform directly. Every other route keeps the first-party CORS allow-list (`server/public-cors.js`).
37- **Public profiles** — `/@<username>`: one page per person with their picture, bio, when they joined, what they wear and the items they own in [OpenVibe.Inventory](https://inventory.openvibe.network) (plan T21). The same data is at `GET /api/v1/profiles/:username`, open to every origin. An old username answers 301 to the current one. A person can hide their profile from Edit Profile (`users.profile_public`); a hidden one shows only the name and picture and is not indexed. Items are read with the Network's own service token and kept 60 s per person (`server/profile/public.js`).
38- **OpenCoins Wallet** — Network-wide currency. User balance/history at `/api/coins/*`; atomic credit/debit/transfer for services at `/internal/coins/*` with idempotency-key dedupe.
39- **User modules** — versioned per-person preferences and summaries in namespaces owned by services (openvibe-contracts `manifests/namespaces`); see [User modules](#user-modules).
40- **Online status** — each person has a presence of online, away, do not disturb or invisible, with an optional status line and one emoji, carried across every site. `POST /api/v1/me/presence/heartbeat` keeps it fresh (the shared navbar sends it every 60 s while the page is visible), `PUT /api/v1/me/presence` sets the chosen state, and `GET /api/v1/presence?subjects=…` answers only for people the caller follows or has a DM with (and themselves), never for a stranger and never for an invisible person; last seen is never returned. `network.presence@1` and `network.presence.changed@1`, contracts 0.318.0, `server/presence/`. Chat reads it through `GET /internal/presence` (`network.presence.read`) and asks back for the DM half of the rule (`chat.dm.peers.read`). Live streaming makes `live: true`. Every row is in the person's export and erased with the account (ADR-033).
41- **The Dock's state** — the little floating surface every OpenVibe site draws (your agent, your chats, your activity) follows the person across every domain, because `localStorage` is per site and the state lives here instead. `GET /api/v1/me/dock` and `PUT /api/v1/me/dock` (`network.dock-state@1`, `network.dock-result@1`, contracts 0.251.0; `server/dock/`). See [docs/dock.md](docs/dock.md).
42
43## Your account ID
44
45Every person on OpenVibe has one canonical id, `usr_…`: the **account ID**. It is what apps,
46robots and services ask for when they ask for "your OpenVibe account ID", and it is the id every
47OpenVibe service keys a person's data by.
48
49A person sees their own on their account page (`https://openvibe.network/my`, Profile → Account
50ID): the id in a chip with a Copy button. It is nobody else's — the API answers it for the
51signed-in person only.
52
53### API
54
55- `GET /api/auth/me` — the signed-in person's own account. `user.subject_id` is the account ID
56 (`usr_…`); an account that has none gets one minted on its first call.
57
58```bash
59curl -s https://openvibe.network/api/auth/me \
60 -H "Authorization: Bearer $OV_TOKEN" | jq -r '.user.subject_id'
61```
62
63`OV_TOKEN` is that person's own access token (the `ov_token` cookie, an OAuth access token, or an
64`Authorization: Bearer` header). The answer carries the caller's own id and nobody else's.
65
66### With openvibe-sdk
67
68The SDK has no client for `GET /api/auth/me`, so a service that already holds the person's token
69reads the id from the token's claims:
70
71```js
72const { verifyUserToken } = require('openvibe-sdk/auth');
73const claims = await verifyUserToken(token, { jwks: 'https://openvibe.network/api/.well-known/jwks' });
74claims.subject_id; // 'usr_…' — the account ID
75```
76
77`openvibe-sdk/identity` goes the other way: `resolve({ subjectId })` turns an account ID into the
78person's username and picture (a service token, `identity.subject.resolve`).
79
80---
81
82## Owns
83
84- accounts, sessions, anonymous identities, canonical subjects (`usr_…`, `gst_…`)
85- OAuth2/OIDC (the network's token issuer and its signing key), service principals and their grants,
86 developer projects, apps, credentials, grants and quotas (ADR-014), mod principals
87- notifications and email, themes and theme preferences, user modules, platform blocks, the follow
88 graph (ADR-030), creator analytics (ADR-021), the OpenCoins wallet
89- node principals: a person's paired machine (`lnk_…`) with a one-time pairing code, a scoped credential and a
90 token; a revoke — by the pairing service, its owner, or an account deletion — stops the machine and is announced
91 as `network.link.revoked@1`
92- the machines hub (`/api/v1/machines`, plan L34): a person's machines, their roles and what they do right now.
93 Bot stays the registry underneath — Network calls it as the person, so Bot checks the machine is that person's
94 own — and Network publishes what changes (`network.machine.online`, `.offline`, `.paired`, `.roles_changed`,
95 `.revoked`). A token that acts for the person (an app's, or a run's) may read the machines, rename one and
96 revoke one; pairing a machine and choosing its roles come from the person's own session only, because a
97 machine paired that way would carry the person's jobs and their desks.
98- the service registry (`/api/v1/registry/*`, `/.well-known/openvibe`), the status page, the network
99 changelog proxy, and the files at `/shared/*` (the pinned openvibe-shared release)
100
101## Does not own
102
103- money (OpenVibe.Billing), channel points and streams (OpenVibe.Live), each service's data (a user
104 module's namespace belongs to the service that owns it)
105- the contracts it serves (OpenVibe.Contracts) and the Frame's code (OpenVibe.Shared)
106
107## Depends on
108
109- `openvibe-contracts` v0.317.0 (registry manifests, capability checks), `openvibe-sdk` v0.158.0 (PostgreSQL
110 and Valkey adapters, events, per-actor limits, universal telemetry), `openvibe-shared` v4.223.1, pinned by release tarball
111- OpenVibe.Events (Network's outbox relay; the events it consumes), OpenVibe.AI (the `network.site_copy`
112 workflow), OpenVibe.Blog (the changelog feed it proxies)
113- email (Resend), Discord and GitHub when their secrets are set ([Provider secrets](#provider-secrets))
114
115## Capabilities
116
117Implemented here (the service manifest's `capabilities`, audience `openvibe.network`):
118`identity.subject.resolve`, `mods.grant.manage`, `network.account.deletion.confirm`,
119`network.account.export.contribute`, `network.analytics.creator.read`, `network.blocks.read`,
120`network.coins.credit|debit|transfer`, `network.follows.read|write`, `network.presence.read`, `network.integration.github.read`,
121`network.modules.read|write`, `network.notifications.push`, `network.operator.alert`,
122`network.project.manage|read`, `network.secret.use`, `network.staff.read` and `network.status.incident`. The grants other
123services hold are seeded in [server/identity/principals.js](server/identity/principals.js).
124
125The hub's own two, `network.machines.read` and `network.machines.manage` (Contracts 0.261.0), are checked by name
126here, next to the token and the consent: the check is the same one the contracts' own guard makes, and this one also
127holds the app's consent and the person's own session (see [server/registry/machines.js](server/registry/machines.js)).
128
129Called elsewhere: Network is the issuer, so it signs its own `svc:network` tokens: `events.event.publish`
130(and `events.delivery.admin` for the admin Events views) at OpenVibe.Events, `ai.run.create` /
131`ai.run.read` at OpenVibe.AI for the footer's site copy, and `bot.machine.manage` at OpenVibe.Bot for the
132machines hub.
133
134## Architecture
135
136```
137openvibe.network (port 4000)
138├── server/
139│ ├── index.js # Express app, CORS, service init, periodic tasks
140│ ├── config.js # Port, JWT, DB, OAuth, connected services
141│ ├── auth/
142│ │ ├── routes.js # Register, login, profile, anon sessions, multi-account, follows
143│ │ └── oauth-routes.js # OAuth2 authorize, token, OIDC discovery
144│ ├── notifications/
145│ │ ├── notification-service.js # CRUD, preferences, email queue, cleanup
146│ │ ├── routes.js # REST API for notification UI
147│ │ └── email-service.js # Resend email service with HTML templates
148│ ├── admin/
149│ │ └── routes.js # Admin panel API (email settings, users, broadcast, health)
150│ ├── themes/
151│ │ └── routes.js # Theme CRUD, user preferences
152│ ├── internal/
153│ │ └── routes.js # Server-to-server API (verify-token, user sync, notif push)
154│ └── db/
155│ └── database.js # PostgreSQL handle (openvibe-sdk/db), migrations/, boot seeding
156├── public/
157│ ├── index.html # Landing page (hero + service cards)
158│ ├── login.html # Animated login/register page
159│ └── my.html # Account management (profile, sessions, notifications)
160├── deploy/
161│ ├── nginx/ # openvibe.network.conf; network-subdomains.conf (status/themes/admin/auth/api. redirects); parked-domains.conf
162│ ├── scripts/ # deploy.sh (runs `ovhost deploy network`)
163│ └── systemd/ # openvibe-network.service (EnvironmentFile=/etc/openvibe/network.env), .socket + socket.conf drop-in
164└── .env.example
165```
166
167---
168
169## Setup
170
171```bash
172# 1. Install dependencies
173cd OpenVibe.Network && npm install
174
175# 2. Generate RSA keys for JWT signing
176mkdir -p data/keys
177openssl genrsa -out data/keys/private.pem 2048
178openssl rsa -in data/keys/private.pem -pubout -out data/keys/public.pem
179
180# 3. Configure environment
181cp .env.example .env
182# Edit .env — set ADMIN_USERNAME, ADMIN_PASSWORD, and optionally SETUP_TOKEN and BOOTSTRAP_PROFILE.
183# In production also set DATABASE_URL (the runtime role through PgBouncer), DATABASE_DIRECT_URL (the owner
184# role on a direct connection, for migrations) and VALKEY_URL (shared limit counters). Without
185# DATABASE_URL, development uses an embedded PGlite database in data/pglite; production refuses to boot.
186
187# 4. Run
188npm start
189
190After the first run the service will seed the URL registry with safe defaults and create an admin account from environment values. Visit `/api/setup/status` for setup health and use `/api/setup/bootstrap` to apply registry profiles manually.
191```
192
193The server applies `migrations/NNNN_*.sql` at boot (as the owner on `DATABASE_DIRECT_URL`) and seeds OAuth2 clients and site settings on first run into PostgreSQL. Client secrets are logged to console once on creation — copy them to the consuming services' env files (`/etc/openvibe/<svc>.env`). The tests run on PGlite (`npm test`) or the PostgreSQL + PgBouncer containers (`npm run test:pg`, after `eval "$(openvibe-sdk scripts/test-services.sh up)"`).
194
195To rotate a service principal, load **Network's** environment before the script name and pass the target service's file with `--write-env`:
196
197```bash
198sudo node --env-file=/etc/openvibe/network.env server/setup/service-principal.js rotate <id> --write-env /etc/openvibe/<id>.env
199```
200
201The script requires the database named `ov_network` (or an explicit `--database-name <name>` override). Do not use `--env-file` after the script name: Link loads it as environment and can select the target service's database.
202
203---
204
205## Deploying
206
207`sudo /opt/openvibe.network/deploy/scripts/deploy.sh` runs `ovhost deploy network --install-units`
208(OpenVibe.Host, strategy `git-checkout`; roadmap WS-N task 11): a fast-forward pull as the checkout owner,
209`npm install --omit=dev` when the lockfile or dependency fields changed, every dependency checked before
210anything restarts, `deploy/systemd/openvibe-network.service` installed when it differs (daemon-reload), the
211restart, `/api/ready` polled, an automatic rollback if it does not come up, and the release announced.
212`--restart`, `--wait-idle`, `--force` and `--rollback` (`ovhost rollback network`) are passed on; `DRY_RUN=1`
213prints `ovhost plan network`. If ovhost is missing, the wrapper exits with an error.
214
215The unit is `openvibe-network.service` (runs as `ubuntu` from `/opt/openvibe.network`) on
216`127.0.0.1:4000`, the env file `/etc/openvibe/network.env`. The listener belongs to `openvibe-network.socket`
217(with the `openvibe-network.service.d/socket.conf` drop-in), so connections that arrive during a restart wait
218for the new process instead of getting 502s; ovhost restarts the service, never the socket. Check that systemd
219holds it with `sudo ss -ltnp | grep :4000` (systemd listed, the process on fd 3). Rollback: automatic when `/api/ready` does not
220come up; afterwards `sudo ovhost rollback network --to <sha>`. Two blockers: `chrome_cache`/`chrome_hits`
221became `frame_cache`/`frame_hits` on 2026-09-24 (an older release starts with empty caches), and
222`tool_domains` was rebuilt once for the mirror role (an older release does not know mirror rows).
223
224---
225
226## TLS / Certbot Deploy Support
227
228`openvibe.network` includes a built-in deploy subsystem under `server/deploy/` that can manage Let’s Encrypt certificates and preview Nginx config.
229
230### What it does
231
232- wraps local `certbot` calls via `server/deploy/cert-manager.js`
233- supports `cloudflare` mode using `certbot-dns-cloudflare`
234- supports `manual` DNS-01 issuance for wildcard certificates
235- stores certs under `/etc/letsencrypt/live/<domain>/`
236- generates Nginx config templates that reference `/etc/letsencrypt/live/.../fullchain.pem` and `privkey.pem`
237
238### Requirements
239
240- `certbot` installed and available in `PATH`
241- for Cloudflare mode: `certbot-dns-cloudflare` plugin installed
242- `/etc/letsencrypt` writable by the service user
243- `nginx` installed if you want preview/apply support
244
245### Deploy configuration keys
246
247Use the `network` deploy setup API or URL registry to configure:
248
249- `DEPLOY_ACME_EMAIL` — ACME registration email
250- `DEPLOY_CERT_MODE` — `cloudflare`, `manual`, or `none`
251- `DEPLOY_CLOUDFLARE_TOKEN` — Cloudflare API token for DNS-01 challenge
252- `DEPLOY_DOMAINS` — array of domain objects, e.g. `[{domain:'openvibe.network',wildcard:false,certName:'openvibe.network',services:['network']}]`
253- `DEPLOY_NGINX_MODE` — `preview`, `apply`, or `disabled`
254- `DEPLOY_NGINX_SITES_PATH` — path where Nginx site files should be written
255- `DEPLOY_NGINX_BACKUP_PATH` — backup directory for generated Nginx configs
256- `DEPLOY_SERVICE_MAP` — optional service mapping override
257
258### Admin deploy endpoints
259
260The deploy subsystem exposes admin-only APIs at `/api/admin/deploy`:
261
262- `GET /api/admin/deploy/prerequisites` — check `certbot`, plugin, `/etc/letsencrypt`, and `nginx`
263- `GET /api/admin/deploy/config` — read current deploy config
264- `PUT /api/admin/deploy/config` — save deploy config values
265- `GET /api/admin/deploy/certs` — list certificates known to certbot
266- `POST /api/admin/deploy/certs/issue-cloudflare` — issue a wildcard cert via Cloudflare DNS-01
267- `POST /api/admin/deploy/certs/manual-info` — get manual DNS challenge instructions
268- `POST /api/admin/deploy/certs/issue-manual` — run manual DNS issuance after TXT records are in place
269- `POST /api/admin/deploy/certs/renew` — renew all certbot certificates
270- `GET /api/admin/deploy/nginx/preview` — preview generated Nginx configs
271
272### How to use it
273
2741. Install certbot:
275
276```bash
277sudo apt update
278sudo apt install certbot python3-certbot-dns-cloudflare
279```
280
2812. Ensure `nginx` is installed if you want config preview/apply support.
282
2833. Set deploy registry values through the setup API or admin config.
284
2854. For Cloudflare mode, provide a valid Cloudflare token. The wrapper writes `/etc/letsencrypt/cloudflare.ini`.
286
2875. For manual mode, create the required DNS TXT records for `_acme-challenge.<domain>` and `*. <domain>` as instructed by the service.
288
2896. After issuance, Nginx configs reference certs at `/etc/letsencrypt/live/<domain>/fullchain.pem` and `/etc/letsencrypt/live/<domain>/privkey.pem`.
290
2917. Use `POST /api/admin/deploy/certs/renew` to renew existing certs.
292
293### Notes
294
295- The deploy module does not replace a full deployment toolchain; it is a built-in helper for cert issuance and Nginx preview.
296- If `DEPLOY_CERT_MODE` is set to `none`, the service will still generate preview Nginx configs but will leave SSL certificate paths commented out.
297- `certbot` must be installed on the host running openvibe.network, not just in Docker or another container.
298
299---
300
301## Notification System
302
303### How it works
304
3051. **Any service** pushes notifications to openvibe.network via `POST /internal/notifications/push`
3062. **openvibe.network** stores them in PostgreSQL with priority, category, and optional rich content
3073. **Clients** poll `GET /api/notifications` every 15 seconds, rendering toasts and updating the bell badge (or, where a site turned realtime on, hear each new notification over OpenVibe.Events and poll every 2 minutes as a safety net: see *Realtime badge* below)
3084. **Critical notifications** are queued for email delivery via the built-in email service (Resend by default).
3095. **Quiet hours and digests** can hold normal email and browser push until a window ends, or replace per-alert email with an hourly/daily/weekly summary; critical notifications always break through.
310
311### Priorities
312
313| Priority | Behavior |
314|----------|----------|
315| `low` | Silent — badge only, no toast or sound |
316| `normal` | Standard toast with subtle notification sound |
317| `high` | Persistent toast with attention sound, auto-dismiss 8s |
318| `critical` | Sticky toast (must dismiss), urgent sound, **triggers email** |
319
320### Categories
321
322`social`, `chat`, `game`, `stream`, `economy`, `achievement`, `moderation`, `system`, `service`, `admin`
323
324Users can toggle each category's enabled/sound/toast/email preferences at `openvibe.network/my` → Notifications tab.
325
326### Quiet hours and digests
327
328The bell's tabs (All · Mentions · Live · Deals & alerts · System · Agents) are defined once in `server/notifications/tabs.js`: `GET /api/notifications?tab=mentions|live|alerts|system|agents` lists a tab (it combines with `unread_only`, `q` and paging; an unknown tab is All), and `GET /api/notifications/unread-by-tab` answers `{ tabs: { all, mentions, live, alerts, system, agents } }` for the counts on the tabs.
329
330A person can set a do-not-disturb window with `GET`/`PUT /api/notifications/quiet-hours` — `{ enabled, start: 'HH:MM', end: 'HH:MM', tz }`, default off at 22:00–07:00 UTC; the GET also reports `in_quiet`. Normal notification email and browser push wait until the window ends; `critical` notifications still go at once. The notification itself is always stored and appears in the bell.
331
332Each category can choose `digest`: `off` (the default, NULL), `hourly`, `daily` or `weekly`, through `PUT /api/notifications/preferences`. A digested category is not emailed one by one: `runDigests()` sends one summary email after the period is due (daily and weekly at 08:00 or later in the person's quiet-hours zone, never during quiet hours), lists up to 20 items and says "and N more", then marks them emailed. It runs every 15 minutes in `server/index.js` and uses the same daily caps and verified/bounced-address checks as the email queue.
333
334### Cross-Service Push
335
336Other services push notifications to openvibe.network via the internal API:
337
338```bash
339curl -X POST http://127.0.0.1:4000/internal/notifications/push \
340 -H "Authorization: Bearer <service token with network.notifications.push>" \
341 -H "Content-Type: application/json" \
342 -d '{"userId": 42, "type": "new_follower", "data": {"actorName": "someone"}}'
343```
344
345### The Action Center
346
347`GET /api/v1/me/actions` (`server/actions/`, capability `network.actions.read`, contracts 0.229.0) is the ✦ beside the bell:
348the signed-in person's agent work in five buckets (`running`, `waiting`, `scheduled`, `done`, `failed`) as
349`network.me-actions-result@1`. It merges their pending confirmations (`server/developer/confirmations.js`) with
350Actor's, Codes' and AI's `GET /api/v1/actions?subject=usr_…` (`actor.actions.read`, `codes.actions.read`,
351`ai.actions.read`; Network signs its own token for each, `server/identity/self-token.js`; `OV_ACTOR_INTERNAL_URL`,
352`OV_CODES_INTERNAL_URL`, `OV_AI_INTERNAL_URL`). Each source has 2.5 s; a slow or failing one is named in `sources`
353and the answer is `partial`, and a service that has not deployed the route is `not_connected`. Items that are not
354`common.action-item@1`, or claim another service, are dropped. The answer is cached 10 s per person.
355`POST /api/v1/me/actions/:service/:id/approve|decline` decides a Network confirmation, or an Actor approval through
356Actor's `POST /api/v1/approvals/:id/decide` (`actor.approval.decide`, `X-OV-Subject`); anything else is opened on its
357own page.
358
359### Notifications from Events
360
361`POST /internal/events` (`server/notifications/events-consumer.js`) turns OpenVibe.Events deliveries
362into inbox notifications for the person they name, through the same `NotificationService.create()`
363as everything else, so a person who turned the `service` category off gets nothing and email follows
364their per-category email choice.
365
366| Event type | From | Person | Notification |
367|---|---|---|---|
368| `deals.watch.matched` | Deals (`source: deals`) | `payload.recipient` (`usr_…`) | `DEAL_WATCH_MATCH`, `service`, normal |
369| `trade.alert.triggered` | Trade (`source: trade`) | `subject.id` (`subject.type: user`) | `TRADE_ALERT`, `service`, high |
370| `live.stream.started` | Live (`source: live`) | every follower of `payload.channel.subject`, plus everyone with `stream_live_all` on | `STREAM_LIVE`, `stream`, high; the Discord live alert after commit |
371| `today.event.scheduled` | Today (`source: today`) | every follower of `subject.id`, for a public or followers-only stream 15 min to 14 days ahead; at most 3 announced schedules per streamer a day | `STREAM_SCHEDULED`, `stream`, low (the bell, no toast); the time in the stream's own zone |
372| `today.event.cancelled` | Today | everyone told by the matching `today.event.scheduled` | those notices are dismissed |
373
374**Go-lives.** The followers are Network's own follow graph (see "Follow graph"): the active
375`user_follows` rows for `('channel', payload.channel.subject)`, read in the same transaction as the
376notifications. Live is never asked. A channel whose subject has no Network account is `ignored:channel`;
377a follower whose subject has no account is counted as `unresolved`; the streamer never hears about
378themself; the `stream` category's mute and email choice apply. A start more
379than 30 minutes old (a replay, or Events catching up) announces nothing. The per-streamer window (one
380announcement an hour, eight a day; `stream_live_cooldown_min`, `stream_live_daily_cap`) is kept by
381`server/notifications/stream-live.js`. This consumer is the only go-live path (compatibility register C-85): Live
382publishes `live.stream.started` and makes no direct call. Follows have been Network's since 2026-09-26, and the
383cutover flag that held the consumer back is gone.
384
385**Project usage.** `tools.usage.recorded` and `events.usage.recorded` (hourly rollups of a developer
386project's use, `common.usage-recorded@1`) are kept per project and day (`server/developer/usage.js`,
387tables `dev_usage_windows`, `dev_usage_daily`, `dev_usage_errors`) for `GET /api/v1/projects/:project/usage`
388and the project's usage page on openvibe.services; they notify nobody. See
389[docs/developer-projects.md](docs/developer-projects.md#usage).
390
391Coupons publishes no watch event yet: its merchant watches never leave Coupons, and its `coupons.*`
392events name no person. Every other type is acknowledged and ignored. Deliveries are v2-signed only
393(`openvibe-sdk` `parseDelivery` with `requireV2`, ±300 s), and each `event_id` is handled once
394(openvibe-sdk inbox, table `network_event_inbox`, consumer `network-notifications`). Links point at a
395site only when its domain serves the service (the exposure overlay above); until then the URL is kept
396in `rich_content.context.planned_url`.
397
398The route is inert until the operator does both of these:
399
4001. Generate the signing secret and give it to Network: add
401 `NETWORK_EVENTS_SECRET=<openssl rand -hex 32>` to `/etc/openvibe/network.env` (comma-separate a
402 second value to rotate), and make sure `OV_EVENTS_INTERNAL_URL=http://127.0.0.1:4300` is set.
403 Restart `openvibe-network`; the log says `[Events consumer] on`.
4042. Create the subscriptions in Events (consumer `network`, which Events takes from the token's
405 `svc:network`; endpoint `http://127.0.0.1:4000/internal/events`; topic patterns
406 `deals.watch.matched`, `trade.alert.triggered` and `live.stream.started`; secret = the first
407 `NETWORK_EVENTS_SECRET`). On a host that already has the other two, add `live.stream.started` alone with
408 `--topic live.stream.started` (and the project usage rollups with
409 `--topic tools.usage.recorded --topic events.usage.recorded`):
410
411 ```bash
412 cd /opt/openvibe.network
413 sudo node --env-file=/etc/openvibe/network.env scripts/subscribe-events.js --dry-run
414 sudo node --env-file=/etc/openvibe/network.env scripts/subscribe-events.js
415 ```
416
417 The script signs its own 5-minute service token (Network is the issuer: `sub svc:network`,
418 `aud openvibe.events`, `cap events.subscription.manage`) and hands the secret to Events in the
419 subscription body, the way Deals, News and Tips do with theirs. It prints the subscription ids,
420 never the secret, and reports an existing identical subscription instead of duplicating it.
421
422Each go-live's outcome is logged, with counts only:
423
424```
425[Events consumer] live.stream.started evt_…: notified {"followers":3,"unresolved":0,"targets":3,"notified":3}
426```
427
428The consumer is the only go-live path (compatibility register C-85, closed 2026-10-11): Live publishes `live.stream.started` and makes no direct call, and `POST /internal/events/stream-live` is gone. A reconnect inside the per-streamer window logs `skipped:cooldown`.
429
430### Realtime badge (network.notification.created)
431
432Every notification `NotificationService.create()` stores is announced as **`network.notification.created`** (Contracts 0.61.0; ADR-005 amendment 2). The envelope goes into `network_event_outbox` in the same transaction as the notification: both exist or neither. The relay then publishes it to Events.
433- **Envelope:** subject `{ type: user, id: <recipient usr_> }`, visibility `subject` (Events streams it to that person only), actor `system:network`.
434- **Payload:** `notification_id`, `type`, `category`, `priority`, `service`, `created_at` and the recipient's `unread_count`. Never the title, message, link or sender.
435- **Nothing announced:** a muted category, a blocked sender or a go-live dedupe stores nothing, so it announces nothing. Guests and accounts without a `usr_` subject get no event.
436
437The badge on other sites cannot use a cookie of openvibe.events, so it asks here for a **realtime ticket**:
438
439```
440POST /api/v1/realtime/ticket (Bearer Network JWT, or the ov_token cookie here; 60/min per IP)
441→ 200 { ticket, expires_at, expires_in: 120, stream_url, topics: ["network.notification.*"], subject }
442```
443
444It then opens `${stream_url}?topics=network.notification.*&ticket=…[&last_event_id=…]`.
445- **The ticket** (`server/auth/realtime-ticket.js`, `identity.realtime-ticket-claims@1`) is an RS256 JWT: `iss <issuer>/realtime`, `sub <usr_>`, `aud [openvibe.events]`, `typ` and `purpose` `realtime`, 120 s, `jti rtk_…`. Events accepts each one once.
446- **Never a session:** its issuer, `typ` and audience each rule that out, and this session guard refuses it too.
447- **Not stored, logged or audited:** it grants the person only their own stream.
448- **Refusals:** a guest gets 403 `realtime.guest`. `REALTIME_TICKETS=off` in `/etc/openvibe/network.env` answers 503 `realtime.disabled`, and every badge stays on polling. `OV_EVENTS_PUBLIC_URL` overrides the stream origin (default `https://openvibe.events`).
449- **Client:** openvibe-shared `notification-live.js`, on where a site sets `notificationsRealtime: true`.
450
451Quiet hours and per-category digests are built (migration `0027_notification_quiet_digest.sql`, `server/notifications/quiet-hours.js` and `digest.js`); the design note is [docs/notification-digest.md](docs/notification-digest.md).
452
453---
454
455## The Dock's state
456
457The Dock (plan 2026-10-30, K-D1) is one small floating surface on every OpenVibe site: your agent, your chats and
458your activity. It is drawn on many domains and `localStorage` is per site, so the state it follows you with lives in
459the Network (`server/dock/`, migration `0031_dock_states.sql`, contracts 0.251.0).
460
461- `GET /api/v1/me/dock` answers `network.dock-result@1`: the person's `network.dock-state@1` (the stored one, or the
462 default — closed, on the activity tab, not pinned, bottom-right, the site's own size), the `counts` the pill's
463 status dot is drawn from (`running` and `waiting` from the Action Center's aggregator, `unread` from the
464 notification service) and `sources_partial`, true when an authority behind those counts did not answer.
465- `PUT /api/v1/me/dock` takes a partial body — any subset of the state's fields — merges it into the stored state,
466 validates the result against `network.dock-state@1` (an invalid body is `422` with the contract's problem shape)
467 and answers the new `network.dock-result@1`. `place` merges per device class, so a body naming only `place.phone`
468 leaves the desktop where it was.
469- The subject comes from the caller's token and from nowhere else, so one person never reads or writes another's
470 state. The caller's own session only: the `ov_token` cookie on openvibe.network, a Bearer user token elsewhere,
471 with the CORS allow-list and the CSRF rules the other `/api/v1/me/*` routes use.
472- A write is limited to 60 a minute per person (`429 dock.rate_limited` past that); the page debounces anyway.
473- The row is the person's own: it goes into their account export and is erased with them (ADR-033), and a merge
474 with the surviving account.
475
476The full reference is [docs/dock.md](docs/dock.md).
477
478---
479
480## Email Setup
481
482Email is optional — the notification system works without it. Email is only sent for **CRITICAL** priority notifications.
483
484### Quick setup
485
4861. Create a domain or sender identity at <https://resend.com/domains>.
4872. Add the required SPF, DKIM, and MX records to your DNS provider.
4883. Configure the Resend API key and from-addresses in the admin panel.
4894. Verify your domain on Resend before sending production email.
490
491### Admin panel configuration
492
493Configure email at runtime via the admin panel (the API key only when `RESEND_API_KEY` is not set; see below):
494- `GET /api/admin/email` — view current email config (`api_key_source`: `env`, `database` or `unset`)
495- `PUT /api/admin/email` — update API key, default from address, and per-service from addresses
496- `POST /api/admin/email/test` — send a test email
497
498## Provider secrets
499
500Each provider secret has an environment variable, read first; the `site_settings` row is only a
501fallback (`server/secrets.js`, roadmap §18.2(12)). While a variable is set, the admin panel shows the
502secret as "set in the environment" and never saves one into the database. `GET /api/admin/secrets`
503(owner) and the boot line `[Secrets] resend_api_key=env …` say which source each one uses, never a value.
504
505| Setting | Variable | Used for |
506| --- | --- | --- |
507| `resend_api_key` | `RESEND_API_KEY` | email delivery |
508| `resend_webhook_secret` | `RESEND_WEBHOOK_SECRET` | Resend delivery webhooks (Svix `whsec_…`) |
509| `discord_bot_token` | `DISCORD_BOT_TOKEN` | Discord bot alerts |
510| `discord_oauth_client_secret` | `DISCORD_OAUTH_CLIENT_SECRET` | Discord account linking |
511| `vapid_private_key` | `VAPID_PRIVATE_KEY` | web push (the public key is derived from it; `VAPID_PUBLIC_KEY` optional) |
512
513`net.ipinfo_token`, `net.globalping_token` and the old `ses_*` keys are never read by Network (the Tools
514gateway reads `NET_IPINFO_TOKEN` / `NET_GLOBALPING_TOKEN` from its own env file).
515
516Provider secrets are read from the environment only (`/etc/openvibe/network.env`): a `site_settings` row of the same
517key is ignored and no admin form stores one. The database fallback and its copy-out script were retired on 2026-10-10,
518once production held no database copy of any secret.
519
520## Project secrets (ADR-056)
521
522A project keeps its own secrets here, per environment, written once and used by name (`server/developer/secrets.js`).
523These are the project's, not Network's provider secrets above.
524
525- **People** (Bearer user tokens on `/api/v1/projects/:project/secrets`):
526 - admins and owners set a value (`PUT …/:env/:name { value }`; setting it again adds one to its version), delete it,
527 and grant it to a service (`PUT …/:name/grants/:service { purposes, origins }`);
528 - members and staff read metadata only (`network.project-secret@1`): name, version, a keyed fingerprint and grants.
529 No route answers a value to a person.
530- **Services** (`POST /internal/secrets/use`, `network.secret.use`): a service the secret is granted to either signs a
531 message with it (`sign`: HMAC-SHA256, the value never leaves Network) or receives it for an https origin the grant
532 lists (`reveal`). A missing secret, grant, purpose or origin, and an archived project, all answer 404
533 `secret.not_found`. Actor holds the capability by default, to sign webhook deliveries with a task's `secret_ref`.
534- **Storage:** AES-256-GCM under a key derived from `NETWORK_SECRETS_KEY`, with the row's identity as authenticated data,
535 so a ciphertext moved onto another row does not open. Without the key the feature is off (503), never plaintext.
536 Archiving a project, or deleting its owner's account with no heir, shreds its secrets.
537- **Audit (`dev_audit`):** sets, deletions, grant changes, and the first use per secret, service, purpose and day.
538 Values and messages are never logged.
539
540---
541
542## Anonymous Users
543
544Anonymous users can browse and interact without creating an account:
545
546- Each anon user gets a unique number (e.g., "Anonymous #42")
547- Stats and preferences are tracked via a session token
548- Fingerprint matching reconnects returning anonymous visitors
549- Anon identities can be linked to a registered account at any time, merging stats; the guest's user modules move to the account (the account's own record wins in a namespace where it has one)
550- Multi-account switcher includes a "Continue as Anonymous" option
551
552### API
553
554- `POST /api/auth/anon-session` — create anonymous session
555- `GET /api/auth/anon/:token` — get anon user info
556- `PUT /api/auth/anon/:token/preferences` — update anon preferences
557- `POST /api/auth/anon/:token/link` — link anon identity to registered account
558
559---
560
561## User modules
562
563One JSON record per (person, namespace), for portable preferences and summaries (never domain truth).
564Namespaces, schemas, writers, public fields and quotas come from openvibe-contracts
565(`manifests/namespaces`); `server/identity/modules.js` enforces them.
566
567- People: `GET /api/modules` (export), `GET|PUT|DELETE /api/modules/:ns` (`If-Match: <revision>` on PUT).
568 Anyone: `GET /api/modules/:ns/public/:subject` (public fields only).
569- Services: `GET|PUT|DELETE /internal/modules/:ns/:subject`, service token only (`network.modules.read` /
570 `network.modules.write` with the namespace in the grant); only the namespace's owner writes.
571 `chat.preferences` is owned by Chat since the Wave 6 cutover (`OWNER_HANDOFFS`, until Contracts says so).
572- Every change emits `network.module.updated` through `network_event_outbox` (relayed to OpenVibe.Events
573 when `OV_EVENTS_INTERNAL_URL` is set; rows wait there otherwise). The payload names the owner subject,
574 namespace, schema version, revision, change, reason and the changed keys; values only for fields the
575 namespace declares public. Revisions only grow for a (person, namespace), deletes included.
576- Accounts: `onSubjectRemoved` deletes a subject's records with events; guest links move portable preferences to a registered account; triggers
577 refuse deleting a `users`/`anon_users` row, or changing its `subject_id`, while records remain.
578- Owning service retired (its manifest `status: retired`): the namespace turns read-only;
579 `delete-after-retention` namespaces are emptied `retentionDays` after Network first saw it (daily sweep).
580
581---
582
583## Platform blocks
584
585A person blocks someone once, on the network, and every product honours it (roadmap WS-E task 5,
586Contracts 0.49.0; `server/identity/blocks.js`). `user_blocks` is keyed by subjects, one row per
587(blocker, blocked) with `active` and a per-pair `revision` that grows on every change.
588
589- People: `GET /api/v1/me/blocks` (who I blocked, with names and avatars), `PUT|DELETE /api/v1/me/blocks/:subjectOrUsername`.
590 Nobody blocks themselves; guests neither block nor are blocked. Staff can be blocked, but a block never hides a
591 staff action. The account page (Security → Blocked people) lists blocks with unblock buttons.
592- Services: `GET /internal/blocks?subject=usr_…` → `{ subject, blocks, blocked_by }`, service token with
593 `network.blocks.read` only (Chat and Community hold it by default; never the shared key).
594- Every change writes `network.block.changed` into `network_event_outbox` in the same transaction. Chat
595 (DMs, mentions) and Community (replies) keep projections from their own Events subscriptions.
596- Network's notifications: nothing is created from a person the recipient blocked (`sender_id` as a Network
597 id, or `actor_subject`); moderation, system and admin notices always are.
598
599---
600
601## Follow graph (ADR-030)
602
603Network owns follows, keyed by subjects (`server/identity/follows.js`; roadmap WS-E task 4, Contracts 0.65.0).
604A target is a Live channel today (`channel`, named by its owner's subject), and other kinds join as products need them.
605
606- **Public:** `GET /api/v1/follows/:type/:target` gives the follower count, and for a signed-in caller whether
607 they follow and how they are notified.
608- **People:** `GET /api/v1/me/follows` lists what the caller follows. `PUT|DELETE /api/v1/me/follows/:type/:target`
609 follow and unfollow (`:target` is a subject or a username). Both are idempotent, and the PUT body can carry
610 `{ notify_email, notify_push }`.
611- **Who follows a target:** `GET /api/v1/follows/:type/:target/followers` is open to the target's owner, and to
612 services with `network.follows.read` (service token only). Lists are never public.
613- **Events:** every change writes `network.follow.created` or `network.follow.deleted` (subject visibility,
614 growing per-pair revision) to the outbox in the same transaction. Live keeps a projection from them.
615- **Products writing for a person** (ADR-030 step 4): `PUT|DELETE /internal/follows/:type/:target` needs
616 `network.follows.write`, a service token and `network.follow-write-request@1`. It is granted to Live, whose
617 buttons use it.
618- **Go-live source:** go-live notifications read the followers here, and only here (plan T2); there is no
619 switch back to Live.
620
621## Multi-Account Switching
622
623Google-style account management supporting up to 5 accounts:
624
625- Accounts stored client-side in `localStorage` (`openvibe_accounts`)
626- Server tracks active sessions via `user_sessions` table
627- Switch accounts instantly without re-authentication
628- Includes anonymous mode as a switchable identity
629- Shared `account-switcher.js` component renders the switcher UI on all services
630
631### API
632
633- `GET /api/auth/sessions` — list active sessions
634- `POST /api/auth/sessions` — create session record
635- `DELETE /api/auth/sessions/:id` — revoke single session
636- `DELETE /api/auth/sessions` — revoke all sessions
637
638---
639
640## OAuth2 Flow
641
6421. Service redirects user to `https://openvibe.network/oauth/authorize?client_id=...&redirect_uri=...`
6432. If user has `ov_token` cookie → issue authorization code → redirect to service
6443. If no cookie → redirect to `openvibe.network` → user signs in → cookie set → resume flow
6454. Service exchanges code for access + refresh tokens via `POST /token`
6465. Access tokens are RS256 JWT (24h) verified by any service with the public key
6476. Refresh tokens rotate on use (30d)
648
649---
650
651## Observability and status
652
653- `GET /api/ready`: named checks from `openvibe-shared/ready`, each with `status`, `required`,
654 `latency_ms` and `checked_at`. `db` and `signing_key` are required (`signing_key` means an RS256
655 keypair that signs and verifies a probe token; it is only optional outside production).
656 `registry_poll` and `discord_bot` are optional. The answer is 503 only when a required check fails.
657 Otherwise it is 200, with `status: "degraded"` when an optional check has failed.
658- `GET /metrics`: Prometheus text for direct loopback callers only. Any proxied request gets a 404,
659 and `deploy/nginx` blocks the path too. Besides the HTTP golden signals by route template, it
660 exports process metrics, `release_info` and `release_client_updates_total{outcome,reason}` (what
661 open tabs report to `POST /release-metrics`). Network's own counters are
662 `network_tokens_issued_total{grant_type}`, `network_token_failures_total{grant_type,error}` and
663 `network_principal_token_failures_total{code}`.
664- Universal telemetry: requests are aggregated per `route|method|status_class` into a single
665 `platform.telemetry-sample@1` per key per flush (count, sum, max and p95 in `extra`, mean in
666 `latency_ms`), plus the HTTP autoscaling gauges (active requests, p95, event-loop lag) once per flush;
667 health, readiness, metrics, the OpenVibe Frame and static/shared assets are skipped. Buffered by
668 `openvibe-sdk/service`'s telemetry kit and written into the analytics store as `telemetry.*` events
669 (`server/telemetry-sink.js`); `gracefulStop` flushes it once at shutdown.
670- `GET /release.json` is Network's release manifest (ADR-016, `registry.release-manifest@1`, from
671 `openvibe-shared/release`'s `release.mount`): the deployed commit, the library versions and, since
672 openvibe-contracts 0.32.0, the 1.1.0 fields. No components are declared, so every release still
673 prompts open tabs to reload.
674- `GET /status` is the operator page: server-rendered, no JavaScript needed, `noindex`.
675 `GET /api/v1/status` returns the same data as JSON. Each OpenVibe service shows as up, degraded,
676 down, not running (placeholder or no runtime) or unknown, with its release, boot time and
677 `checked_at`. The data comes from the ecosystem registry poll (`server/registry/ecosystem.js`),
678 which reads each service's readiness endpoint and `/release.json` about once a minute. A service
679 not checked yet, or whose last check is more than three intervals old, shows as unknown.
680- Where each service can be reached is Network's exposure overlay (`server/registry/exposure.js`), not
681 the manifest's maturity label: `live` (its public domain serves it), `internal` (loopback only, the
682 domain still serves a placeholder page), `library` (released package), `repository` (code with CI,
683 nothing to run) or `placeholder` (planned). The registry adds `exposure` to each service, the
684 descriptor gives an `origin` only to `live` services (`planned_origin` otherwise), `/status` reads
685 "up (loopback only)" for an internal service, and the Frame's nav lists only `live` sites. Change a
686 row when a domain stops serving its placeholder; `test/registry-exposure.test.js` pins the states.
687- The registry also answers `GET /api/v1/registry/topics` (every event type from the manifests'
688 `eventsProduced`/`eventsConsumed` and the payload contracts, with producers, consumers, the matching
689 consumer pattern and the payload contract; filters `service`, `producer`, `consumer`, `prefix`),
690 `/topics/:topic`, `/releases` (each running service's `/release.json` from the same loopback poll,
691 with its installed openvibe-contracts/sdk/shared versions and drift against the libraries' current
692 releases), `/health` and `/search?q=`. A type a service's code publishes before its manifest lists
693 it is added from `server/registry/topics.js` OBSERVED (marked `observed` on the service; empty since
694 openvibe-contracts 0.32.0 lists Live's, Media's and Network's), and Network's consumed topics come
695 from its own Events consumer.
696 Payload schemas are served at their `$id` (`/contracts/events/payloads/<type>.v1.json`).
697- `GET /api/v1/registry/categories[/:id]` groups every service by what it is, each with its rule:
698 `site` (in the network's site list, `server/frame/sites.js`), `platform` (runs, but other services call
699 it), `library`, `repository`, `planned`. `GET /api/v1/registry/featured` lists the open sites whose last
700 check was up or degraded, in the navigation's usage order (7-day page views plus 14-day signed-in
701 history, recounted every 30 minutes), with `ranked_at` and `stale`; the hub is not listed and nothing
702 is featured by hand.
703- Loopback addresses for the health poll: the built-in ports in `server/registry/ecosystem.js`, each
704 overridable by `OV_<ID>_INTERNAL_URL` (loopback URLs only; anything else is ignored and logged). Site
705 hosts come from each manifest's `publicOrigin`.
706- `node scripts/contracts-drift.js` warns about services that pin an older openvibe-contracts than
707 the latest tag, or a tag that was never published (`--dir ~/OpenVibers` for local checkouts,
708 `--registry https://openvibe.network` for what is running, `--package all` for sdk and shared too).
709 It exits 0 unless `--strict`; CI runs it as a warning-only step.
710- `GET /api/v1/status/slo` returns the proposed SLO categories, as does
711 [docs/slo.md](docs/slo.md) / [docs/slo.json](docs/slo.json).
712
713## Analytics (ADR-021)
714
715**Creator analytics** (roadmap WS-E task 6, Contracts 0.68.0; `server/analytics/creators.js`) are built from
716events, not from Live's tables. Every `live.stream.ended` becomes one `creator_streams` row, with the stream's
717totals from the event's `stats`: counts only, never who watched or chatted, and kept 400 days.
718`GET /api/v1/creators/:creator/analytics?days=` shows everyone streams, minutes and peak viewers. The creator,
719and services with `network.analytics.creator.read` (Live's dashboards), also get average viewers, chatters,
720messages and watch minutes.
721
722Network records one row per finished request in the `analytics_*` tables of its PostgreSQL database and rolls
723them up hourly and daily for the admin analytics pages (`/api/admin/analytics`, which also gathers
724Live, Tools, Games and Media). What a raw row may carry is bound by ADR-021 (OpenVibe.Contracts
725`docs/adr/ADR-021-analytics.md`). The module is `openvibe-shared/analytics` (the same one Live and Tools
726use), on the service database through its PostgreSQL tracker (`AnalyticsTrackerPg`). `server/analytics/network.js`
727holds only Network's wiring: its path options (`paramPrefixes`, `pathRules`) and the prune job.
728
729- **Stored:** event type, service, **route template** (the matched Express route, else the path
730 without its query and with ids, usernames and tokens replaced by `:id` / `:param`), method, status,
731 response time, a **rotating session id**, country (CDN header), **user-agent class**
732 (`chrome/windows/desktop`, `bot:curl`), referer **origin** only, bot flags, a signed-in flag.
733- **Never stored:** IP address, user or subject id, city, the user-agent string, full referer URLs.
734 The `ip`, `user_id` and `city` columns stay for compatibility and are always NULL. Per-IP counters
735 for the bot rate check live in memory only. Unique visitors come from a daily-salted hash kept only
736 until that day's final rollup. "New vs returning visitors" is no longer measured.
737- **Opt-out:** a request with `Sec-GPC: 1` or `DNT: 1` is not recorded at all (no raw row, visitor
738 hash, session id or rate counter), so it is also missing from the rollups.
739- **Admin panel:** the bot tables list user-agent classes and high-volume session ids, not IPs.
740- **Connection:** the tracker buffers and flushes into the service's own PostgreSQL handle (the same
741 database the identity tables live in), so there is no second connection or file of its own.
742- **Retention:** raw events older than 30 days are deleted every night in batches of 5000 (the
743 `analytics-prune` job: first run 5 minutes after boot, then every 24 h). Rollups are kept.
744- **Operator CLI:** `scripts/analytics-prune.js` (over `openvibe-shared/analytics/pg`'s
745 `pruneRawEventsPg`) runs a dry run by default and changes nothing; `--apply` deletes raw events older
746 than `--days` (1..30, default 30). Run it where Network runs, with `DATABASE_URL` set.
747
748## Shared Client Libraries
749
750The `openvibe-shared` package ([OpenVibers/OpenVibe.Shared](https://github.com/OpenVibers/OpenVibe.Shared), pinned in package.json) provides drop-in vanilla JS components served at `/shared/` (and `/shared/v1/`). Only the package's browser files are served; a `?v=` equal to a file's content hash is cached for a year, anything else for five minutes:
751
752| File | Purpose |
753|------|---------|
754| `navbar.js` | Universal top bar with service links, notification bell mount, user dropdown |
755| `nav-icons.js` | Navbar glyphs, loaded by navbar.js |
756| `notification-ui.js` | Toast popups, bell badge, notification panel with category tabs |
757| `account-switcher.js` | Multi-account switcher panel with anonymous mode |
758| `user-card.js` | Right-click context menu + user profile card with name effects |
759| `notifications.js` | Shared constants (priorities, categories, types) — also used server-side |
760
761Include them in any service page:
762```html
763<script src="https://openvibe.network/shared/navbar.js"></script>
764<script src="https://openvibe.network/shared/notification-ui.js"></script>
765<script src="https://openvibe.network/shared/account-switcher.js"></script>
766<script src="https://openvibe.network/shared/user-card.js"></script>
767```
768
769**Boost (plan T11).** `/`, `/status` and `/updates` load `/shared/boost.js` (`data-main="#main"`): a same-site link between two pages with the same external scripts swaps only `<main id="main">` instead of loading the page. Today that is the `/updates?site=` filter links between `/updates` views; `/`, `/status` and `/updates` each load other scripts (the footer, the mark, the shipped log), so a move between them is a normal page load until their script sets match. Each carries `<meta name="ov-boost" content="network@<release>">`, the release id `/release.json` serves (`server/boost.js`), so a page from another deploy is a normal page load too. On `/` the static hero, intent picker and constellation stay outside `#main`, and the release id is part of the page cache key, so a deploy changes the `/` ETag. Pages without the marker (`/my`, `/login`, `/admin/*`, `/verify-email`, `/@:username`, the legal pages) are always a normal page load, by design. Page script that lives outside `#main` and wires something inside it re-wires on `ov:boost:load` (the shipped log on `/updates`, the shipped strip on `/`).
770
771---
772
773## Admin Panel
774
775Accessible to users with `role = 'admin'`. All endpoints under `/api/admin/`.
776
777| Endpoint | Description |
778|----------|-------------|
779| `GET /api/admin/email` | Email configuration status |
780| `PUT /api/admin/email` | Update API key and email settings |
781| `POST /api/admin/email/test` | Send test email |
782| `GET /api/admin/settings` | All site settings (secrets masked) |
783| `PUT /api/admin/settings` | Update site settings |
784| `PUT /api/admin/users/:id/role` | Change user role |
785| `PUT /api/admin/users/:id/ban` | Ban/unban user (sends notification) |
786| `POST /api/admin/broadcast` | Send notification to all non-banned users |
787| `GET /api/admin/health` | System health (counts, memory, uptime, email status) |
788| `GET /api/admin/audit` | Audit log |
789
790---
791
792## Connected Services
793
794| Service | Port | OAuth Client ID |
795|---------|------|-----------------|
796| OpenVibe.Live | 3000 | `live` |
797| OpenVibe.Tools | 4001 | `tools` |
798| OpenVibe.Games | 8000 | `games` |
799| OpenVibe.Media | 4100 | `media` |
800
801---
802
803## Database Tables
804
805| Table | Purpose |
806|-------|---------|
807| `users` | Accounts (username, email, password hash, role, bio, effects) |
808| `oauth_clients` | Registered OAuth2 applications |
809| `oauth_codes` | Authorization codes (5-min TTL) |
810| `refresh_tokens` | Rotating refresh tokens (30-day) |
811| `linked_accounts` | Connected external service accounts |
812| `themes` | Theme catalog entries |
813| `user_theme_prefs` | Per-user theme selections |
814| `site_settings` | Key-value admin settings (email config, etc.) |
815| `audit_log` | Admin action audit trail |
816| `notifications` | All user notifications (UUID PK) |
817| `notification_preferences` | Per-user category preferences |
818| `anon_users` | Anonymous user identities |
819| `user_sessions` | Active session tracking for multi-account |
820| `user_effects` | Equipped name/particle effects |
821| `follows` | User follow relationships |
822| `wallets` | OpenCoins balance per Network user |
823| `coin_transactions` | OpenCoins ledger (idempotency-key deduped) |
824| `dev_projects`, `dev_project_members` | Developer projects and member roles |
825| `dev_apps`, `dev_credentials`, `dev_auth_codes` | Developer apps, hashed client secrets, app authorization codes |
826| `dev_grants`, `dev_quotas` | App capability grants; per-project quotas (enforced by the owning service) |
827| `dev_audit` | Append-only developer audit; rows that are platform events carry an event envelope |
828| `dev_usage_windows`, `dev_usage_daily`, `dev_usage_errors` | Developer projects' usage from the services' hourly rollups (35 days), per day (400 days), sampled failures (30 days) |
829| `analytics_events`, `analytics_hourly`, `analytics_daily` | Request analytics (raw ≤ 30 days) and rollups, within ADR-021; see [Analytics](#analytics-adr-021) |
830| `user_modules`, `user_module_revisions`, `user_module_retirements` | User-module records, the last revision issued per (subject, namespace), retired namespace owners |
831| `network_event_outbox` | Network's events on their way to OpenVibe.Events (developer projects, user modules, blocks) |
832| `user_blocks` | Platform blocks by subject (blocker, blocked, active, per-pair revision) |
833| `locker_keys` | The Locker key directory: a person's public keys, their fingerprint and their self-signature |
834| `locker_key_backups` | The wrapped private keys, one row per person and kind (`passphrase`, `recovery`, `passkey`); opaque to Network |
835| `locker_key_history` | Every Locker key change, append-only: the fingerprint before and after |
836| `user_follows` | The follow graph by subject (follower, target type and id, active, notify flags, per-pair revision, source) |
837| `analytics_visitor_days`, `analytics_day_salts` | The current day's salted visitor hashes and salt, deleted after the day's final rollup |
838
839---
840
841## Acceptance
842
843`npm test` runs the 74 test files listed in `package.json` (temp databases, no network). They
844cover OAuth with PKCE, OIDC discovery, refresh-token rotation and reuse, SSO and FedCM, subjects,
845principals and grants, export and deletion, developer projects, export tokens and usage,
846user modules, blocks, follows and their authority switch, analytics privacy, the registry and its
847exposure, notifications and events, status and incidents, operator alerts, per-actor limits, and the
848security suites (`security-session`, `security-redirects`, `security-secrets`, `security-private`,
849`security-idor`, `security-ssrf`), plus the deploy wrapper.
850
851## Security
852
853Reporting a vulnerability: [SECURITY.md](SECURITY.md). The rules the code keeps:
854
855- **Tokens.** Network signs every token with its RS256 key (`JWT_PRIVATE_KEY` or `data/keys`); refresh
856 tokens are stored as SHA-256 and a reused one revokes its family; `token_valid_after` cutoffs are
857 published so services refuse older tokens; client secrets are stored hashed and shown once.
858- **Redirects.** Post-sign-in targets (`/login?return=`, `/sso/fanout?next=`) never run `javascript:`
859 URLs or leave for a host OpenVibe does not own (`test/security-redirects.test.js`); OAuth redirects
860 must be one of the client's registered URIs.
861- **Private data.** Analytics keep no personal data beyond a day's salted hash; account export and
862 deletion follow ADR-033; the internal routes take a capability-scoped service token only (the shared
863 internal key was retired in plan T2).
864- **CORS.** Only discovery and the registry are open to any origin; every other route uses the
865 first-party allow-list.
866- **Secrets.** Provider secrets come from the environment first, never printed; `SETUP_TOKEN`,
867 `ADMIN_PASSWORD`, `NETWORK_EVENTS_SECRET`, the webhook secrets and `GITHUB_TOKEN` live in
868 `/etc/openvibe/network.env` (0600), by name only.
869
870## Sharing
871
872The sharing system (design 2026-11-06) lets a person share an item on one site with another person.
873The owning service stays the authority on access; Network keeps the index and the list.
874
875### The events
876
877Every share event arrives over OpenVibe.Events at `POST /internal/events` and is handled in
878`server/sharing/events.js`:
879
880| Event | What Network does |
881|-------|-------------------|
882| `share.granted` | the item joins that person's list, and they get the notification `share.added` |
883| `share.revoked` | the item leaves that person's list |
884| `share.transfer.requested` | the offer waits for the recipient, who gets `share.transfer.requested` |
885| `share.transfer.accepted` | the recipient owns it now; the old owner gets `share.transfer.answered` |
886| `share.transfer.declined` | nothing changed; the old owner gets `share.transfer.answered` |
887| `share.access.requested` | the owner gets `share.access_requested` |
888
889The payloads carry ids only (a `{ service, kind, id }` ref and a subject), never a title. The name of
890an item and its own link come from the owning service's share API, `GET /api/v1/<kind>/<id>/share`,
891asked on the person's behalf; when the service does not answer, the item is shown as its site and its
892kind instead.
893
894### The routes
895
896| Route | What it answers |
897|-------|-----------------|
898| `GET /shared` | the page: every item shared with the signed-in person, on every site |
899| `GET /api/v1/shared-with-me` | the same list as JSON, with the pending transfers and their Accept and Decline links |
900| `GET /api/v1/share/suggestions` | the people this person has shared with (the share picker's suggestions): from their own history only, at most 20 |
901| `GET /api/v1/share/name-check?name=` | one @name check for the share picker, thirty a minute for the person |
902
903The event names and the payload shapes are Contracts share.v1, which is still being written; this code
904waits for that release before it ships.
905
906## Locker keys
907
908The Locker (design 2026-11-06, `LOCKER-DESIGN-2026-11-06.md`) is end-to-end encryption for anything on
909OpenVibe: a locked item is stored and served as ciphertext, and the keys never reach our servers. Network
910is the key directory — it holds a person's **public** keys, their self-signature and a copy of the
911**private** keys that the browser sealed, and it can never read a private key. The format and the library
912are `openvibe-sdk/locker` and `openvibe-sdk/locker/server`; the browser seals the private keys, Network
913only checks the shape of what it is given.
914
915### What Network stores
916
917| Table | What it holds |
918|-------|---------------|
919| `locker_keys` | the two P-256 public keys (the ECDH one receives content keys, the ECDSA one signs the fingerprint), the eight-word fingerprint and the self-signature |
920| `locker_key_backups` | the wrapped private keys, one row per kind (`passphrase`, `recovery`, `passkey`), stored exactly as the browser sealed them |
921| `locker_key_history` | every key change, append-only: the fingerprint before and the fingerprint after |
922
923**What Network can read:** the public keys, the fingerprint, when the keys last changed, and the shape of
924a backup (its size, its lock and its PBKDF2 count).
925
926**What Network cannot read:** a private key, in any form. The private keys are wrapped by a key from the
927person's locker passphrase (PBKDF2-SHA-256, 600 000 iterations or more) or by a passkey, and the wrapped
928bytes are opaque here. If the passphrase and the recovery kit are both lost, nobody can open the locked
929items, including OpenVibe — but the person can start again with a **reset** (see below).
930
931### The proof: a change needs the key the person has now
932
933A session cookie on its own must not be able to change a person's keys: a stolen session that swapped in
934somebody else's keys would read everything locked for that person from then on. So a person who **already
935has keys** proves they still hold them: the **current** signing key signs the change, and the signature
936comes as `previous_signature` (base64, ECDSA P-256 over SHA-256). Without it the write is refused with
937`403 locker.proof_required`.
938
939The bytes to sign are these, as UTF-8 (`server/locker/proof.js`, exported so the browser signs the same
940ones):
941
942```
943openvibe-locker key-change v1|<subject>|<new fingerprint>|<backups digest>
944openvibe-locker forget v1|<subject>
945```
946
947- `<subject>` is the person's own `usr_…` id.
948- `<new fingerprint>` is the eight words the **new** keys produce (`fingerprintOf`).
949- `<backups digest>` is the lower-case hex SHA-256 of the **canonical JSON** of the backups that are
950 written: the keys of every object sorted, no spaces, one backup per kind:
951
952 ```json
953 {"passphrase":{"envelope":"<base64>","format":"locker.envelope.v1"}}
954 ```
955
956The same proof covers a backups-only update (the fingerprint does not change).
957
958### A reset, and forgetting
959
960- **A reset** is for the person who lost both their passphrase and their recovery kit: `PUT /api/v1/me/keys`
961 with `{"reset": true}` and no proof, allowed only when the session's sign-in is recent — the session
962 token's `auth_time` within the last ten minutes (the OIDC claim Network's `signToken` puts on every
963 session; the same rule the account deletion uses). Without it the write is refused with
964 `403 locker.reauth_required`. A reset replaces the whole locker: new keys, and the backups that come with
965 them; the old backups wrapped keys that are gone. It is written down as `reset`.
966- **Forgetting** (`DELETE /api/v1/me/keys`, with `{"confirm": "forget my locker"}`) needs the same proof, over
967 `openvibe-locker forget v1|<subject>`, or a recent sign-in.
968
969### The routes
970
971| Route | What it answers |
972|-------|-----------------|
973| `PUT /api/v1/me/keys` | my public keys, my self-signature and my wrapped backups |
974| `GET /api/v1/me/keys/backup` | my own wrapped private keys, for a new device — never anybody else's |
975| `DELETE /api/v1/me/keys` | forget my locker: the keys and the backups go, the history stays |
976| `GET /api/v1/people/:subject/keys` | a person's public keys and fingerprint, by subject |
977| `GET /api/v1/people/@:username/keys` | the same, by exact name |
978
979The lookups need a signed-in person and are rate-limited to sixty a minute for each caller. Every answer
980is `private, no-store`, and `Vary: Authorization, Cookie`.
981
982```bash
983# the public keys and the fingerprint of @maticus
984curl -s -H "Authorization: Bearer $TOKEN" https://openvibe.network/api/v1/people/@maticus/keys
985```
986
987```json
988{
989 "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
990 "ecdh_public": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" },
991 "signing_public": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" },
992 "fingerprint": "alpha bravo charlie delta · echo foxtrot golf hotel",
993 "updated_at": "2026-11-06T09:12:00.000Z",
994 "changed_at": "2026-11-06T09:12:00.000Z"
995}
996```
997
998`changed_at` is the last time the keys changed, so a share sheet can warn before it shares with a key the
999people have not seen before. The fingerprint is computed here, never taken from the caller: a caller that
1000sends one that does not match its own keys is refused, and the self-signature must verify with the
1001signing key.
1002
1003A backup is a Locker envelope with a password lock, base64, at most 16 KB, sealed with a PBKDF2 count
1004between 600 000 and 10 000 000. Network checks that with `openvibe-sdk/locker/server` and stores the bytes
1005as they came; it never opens one.
1006
1007```json
1008{
1009 "ecdh_public": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" },
1010 "signing_public": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" },
1011 "self_signature": "…",
1012 "previous_signature": "…",
1013 "backups": {
1014 "passphrase": { "format": "locker.envelope.v1", "envelope": "…" }
1015 }
1016}
1017```
1018
1019Every key change is announced on OpenVibe.Events as `locker.keys.changed`, with ids and fingerprints
1020only: no key, no signature, no blob ever leaves this service in an event, in a log line or in an answer.
1021The event's reason says why it happened: `created`, `rotated` (new keys), `restored` (the same keys, new
1022backups), `reset` or `forgotten`.
1023
1024Every change also sends the person a Network notification, so a swap nobody asked for is seen:
1025
1026| Change | What the person is told |
1027|--------|------------------------|
1028| `created` | "Your locker was set up: the things you lock are opened with these keys." |
1029| `rotated` | "Your locker keys changed: things locked for your old keys cannot be opened with the new ones." |
1030| `restored` | "Your locker backup was restored: the things you lock are opened with the same keys." |
1031| `reset` | "Your locker was reset: things locked for your old keys cannot be opened" |
1032| `forgotten` | "Your locker was forgotten: your keys and your backups are gone." |
1033
1034## License
1035
1036MIT (`license` in [package.json](package.json)).
1037
1038<!-- versions:start -->
1039- openvibe-contracts: v0.421.0
1040- openvibe-sdk: v0.161.0
1041- openvibe-shared: v4.237.0
1042<!-- versions:end -->
1043