openvibers/openvibe.events Commits

Events: openvibe.events serves the API and the realtime stream too (plan T7 layer 1) (#20)

OP OpenVibers committed 7 Oct 2026 · 5e955dbaf2 · 9 files, +139 −43

**Changes (file:line)**

- `deploy/nginx/openvibe.events.conf:30-32` — new `ovevents_apex_{connect,streams,api}` limit zones (distinct from the other vhost's `ovevents_*`, since both load in one nginx config).
- `deploy/nginx/openvibe.events.conf:81-104` — `/realtime/stream` SSE block copied byte-for-byte from the API vhost (buffering/cache/gzip off, 1h timeouts, `Last-Event-ID`, `ov_events_noquery` log), zones renamed.
- `deploy/nginx/openvibe.events.conf:106-117` — `/api/health`, `/api/ready`, `/release.json`, `/limits.json` proxied; `/metrics` and `/internal/` 404.
- `deploy/nginx/openvibe.events.conf:119-141` — `/api/v1/(events|subscriptions|checkpoints)` rate-limited block and the `/api/` 403 block.
- `deploy/nginx/openvibe.events.conf:143-186` — kept home, `/shared/`, robots, sitemap, llms; removed the `return 301 …events.openvibe.network` catch-all (now a plain 404); `client_max_body_size 2m` for parity; every proxied location uses `$remote_addr` only.
- `server/config.js:82` — production default API origin → `https://openvibe.events`.
- `server/config.js:91` — `siteUrl` defaults to `baseUrl` (same origin, plan T7); `EVENTS_SITE_URL` still overrides.
- `server/home.js:4,29,62` — docstring, CSP `connect-src` now `https://openvibe.events`, hero note interpolates `${apiUrl}`.
- `server/discovery.js:8` — docstring: openvibe.events is the API origin too.
- `README.md:5,6,23,183,287,359-362` — Status/domain, nginx link, `EVENTS=`, ticket-cookie note, Deploy paragraph all name openvibe.events; events.openvibe.network "answers until every client moves, then redirects (308)".
- `.env.example:12-18` — `BASE_URL`/`EVENTS_SITE_URL` documented as the openvibe.events API+product origin.
- `docs/cutover-12.md:24` — verification `curl` uses `https://openvibe.events/limits.json`.
- `STATUS.json:9,22` — manifest `domain`/`deployment` name openvibe.events.
- `test/home.test.js:47-55,67-69,111,120-140` — production-origin config test, home/llms origin assertions, and the vhost parse test now checks `/realtime/stream`, the `/api/v1/…` block, `/api/health`, `/api/` and no `events.openvibe.network$request_uri` catch-all.
- Deletions: `docs/replay-window-rollout.md` does not exist, and `grep -i sqlite` finds no stale SQLite store docstring in `server/` or `docs/` (only historical migrations/README/lib notes, still true) — nothing deleted.

**Tests** — `nice -n 15 ov test --no-cache test/home.test.js test/readme-examples.test.js`: 2 passed in 4 s (home 10/10). `ov` reports 2 of 30 files cover the 9 changed files.

**For Opus**
- `deploy/nginx/events.openvibe.network.conf` is untouched; both vhosts now declare the same server_names on different hosts and load together, which is why the new zone names must differ — worth a config-review eye, and I could not run `nginx -t` here (parse-level test only).
- `test/readme-examples.test.js:32` still stubs Network's ticket reply with `stream_url: https://events.openvibe.network/...`; deliberately left, since that value comes from Network's response and the README example only consumes it (a client-side/layer-2 move). Say the word if you want it switched to the new origin.
- I added a small `STATUS.json` manifest update beyond the four numbered points (the T7 note says manifests move too); drop it if you want the PR scoped strictly to the listed files.

Changes

ddiff --git a/.env.example b/.env.example
iindex 897e1ff..d819904 100644
---- a/.env.example
++++ b/.env.example
@@@ -9,8 +9,12 @@
PORT=4300
HOST=127.0.0.1
NODE_ENV=development
--#BASE_URL=https://events.openvibe.network
--# Product domain (openvibe.events): canonical origin of the home page and its robots.txt/sitemap.xml/llms.txt; defaults to https://openvibe.events in production, to BASE_URL otherwise.
++# The API origin (plan T7): everything Events names about itself — Location headers, realtime URLs,
++# limits.json links, llms.txt/sitemap, the home's examples — uses it; defaults to https://openvibe.events
++# in production, to http://localhost:PORT otherwise. events.openvibe.network answers until every client
++# moves, then redirects (308).
++#BASE_URL=https://openvibe.events
++# The product origin; defaults to BASE_URL (openvibe.events is the API and product origin, plan T7).
#EVENTS_SITE_URL=https://openvibe.events
# Identity — OpenVibe.Network signs service tokens (client_credentials, audience
ddiff --git a/README.md b/README.md
iindex 9cb172e..46fbd64 100644
---- a/README.md
++++ b/README.md
@@@ -2,8 +2,8 @@
> Durable events, subscriptions, delivery, retry, dead letters and replay for the network.
--**Status:** alpha (roadmap Wave 3), deployed on `openvibe-ovh` (unit `openvibe-events`, 127.0.0.1:4300, public at `https://events.openvibe.network`). Live (`live.stream.*`), Media, Chat, Games, Network and OpenRe publish to it in production, and every consumer webhook requires signature v2. Media's completion events reach Live through Events (`/internal/media-events`; Live keeps the older `/internal/media-webhook` during the transition), and the shared notification bell can follow `network.notification.created` over the realtime stream with a Network ticket.
--**Domain:** `events.openvibe.network` (the realtime gateway is served here too: ADR-005 put Realtime inside Events, and `realtime.openvibe.network` has no runtime of its own). The product domain `openvibe.events` serves only the home page and its `robots.txt`/`sitemap.xml`/`llms.txt`, and redirects everything else here.
++**Status:** alpha (roadmap Wave 3), deployed on `openvibe-ovh` (unit `openvibe-events`, 127.0.0.1:4300, public at `https://openvibe.events`). Live (`live.stream.*`), Media, Chat, Games, Network and OpenRe publish to it in production, and every consumer webhook requires signature v2. Media's completion events reach Live through Events (`/internal/media-events`; Live keeps the older `/internal/media-webhook` during the transition), and the shared notification bell can follow `network.notification.created` over the realtime stream with a Network ticket.
++**Domain:** `openvibe.events` is the API and product origin (plan T7): the JSON API, the realtime gateway (ADR-005 put Realtime inside Events; `realtime.openvibe.network` has no runtime of its own), the home page and its `robots.txt`/`sitemap.xml`/`llms.txt`. `events.openvibe.network` answers the same surface until every client moves, then redirects (308).
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §6 and §6.3.
**License:** AGPL-3.0 (same as every OpenVibe service).
@@@ -20,7 +20,7 @@ npm run dev # http://127.0.0.1:4300
npm test # every test/*.test.js, temp databases, no network needed
```
--Node 22 in production (`fnm exec --using=22.22.1 npm test`). Production: `/opt/openvibe.events`, env `/etc/openvibe/events.env`, unit [deploy/systemd/openvibe-events.service](deploy/systemd/openvibe-events.service), store PostgreSQL (`ov_events` on the host's data role), nginx [deploy/nginx/events.openvibe.network.conf](deploy/nginx/events.openvibe.network.conf) (public vhost exposes `/realtime/stream`, health, and the token-guarded `/api/v1/events`, `/api/v1/subscriptions` and `/api/v1/checkpoints` for developer apps; `/api/v1/deliveries` stays host-local; services on the host call `127.0.0.1:4300`).
++Node 22 in production (`fnm exec --using=22.22.1 npm test`). Production: `/opt/openvibe.events`, env `/etc/openvibe/events.env`, unit [deploy/systemd/openvibe-events.service](deploy/systemd/openvibe-events.service), store PostgreSQL (`ov_events` on the host's data role), nginx [deploy/nginx/openvibe.events.conf](deploy/nginx/openvibe.events.conf) (the public vhost, on the API and product origin: it exposes the home and its discovery files, `/realtime/stream`, health, and the token-guarded `/api/v1/events`, `/api/v1/subscriptions` and `/api/v1/checkpoints` for developer apps; `/api/v1/deliveries` stays host-local; services on the host call `127.0.0.1:4300`).
`GET /api/health` is liveness. `GET /api/ready` (openvibe-shared/ready) is 200 only when every required check passes — `db` (a real query), `network_jwks` (the Network signing key has loaded; it retries every 30 s while Network boots) and `delivery_worker` (when `EVENTS_WORKER` is on) — and 503 otherwise, with `failed: [...]`. The optional `dlq` check fails once more than `EVENTS_DLQ_DEGRADED_AT` (default 100) deliveries are dead: the service stays ready (200) and reports `status: "degraded"`, `degraded: ["dlq"]`. Each check carries `status`, `required`, `latency_ms`, `checked_at` and, for `dlq`, `detail: { depth, threshold }`; `latest_seq`, `deliveries`, `worker` and `realtime_connections` are still in the body. **Shape change (Track O):** `checks` used to be booleans (`{ db, worker, key }`); they are now objects keyed `db`, `network_jwks`, `delivery_worker`, `dlq`.
@@@ -180,7 +180,7 @@ Roadmap Wave 20, ADR-014. A developer app gets a token from OpenVibe.Network (`P
**Names.** `project_key` is `p` followed by the project's ULID in lowercase: `prj_01JAB2C3D4E5F6G7H8J9K0MNPQ` → `p01jab2c3d4e5f6g7h8j9k0mnpq`. An app event's `source` is `app-` followed by the app's ULID in lowercase: `app:app_01JAB…` → `app-01jab…`. Both fit the existing `events.event-envelope@1` patterns, so the envelope contract did not change. `actor` is `{ "type": "app", "id": "app_<ULID>" }`, or the user in the token's `on_behalf_of`. First-party services can never publish `app.*` (the source names `app` and `app-*` are reserved and refused in `EVENTS_SOURCE_PREFIXES`).
```bash
--EVENTS=https://events.openvibe.network
++EVENTS=https://openvibe.events
PK=p$(echo "${PRJ#prj_}" | tr 'A-Z' 'a-z') # project_key
SRC=app-$(echo "${APP#app_}" | tr 'A-Z' 'a-z') # source
curl -s -X POST "$EVENTS/api/v1/events" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{
@@@ -284,7 +284,7 @@ void connect();
```
- Auth: a **realtime ticket** (`?ticket=`), the Network `ov_token` cookie or a Bearer user JWT (the `ov_token` cookie is scoped to its issuing host, and an EventSource cannot set an Authorization header, so a client on another site uses a ticket); a service token with `events.event.read`; or nobody (public events only, `REALTIME_ALLOW_ANONYMOUS`). An expired cookie degrades to anonymous; a bad Bearer is a 401.
--- Realtime tickets (ADR-005 amendment 2): a page on any OpenVibe site cannot count on a cookie of events.openvibe.network (third-party there), and an EventSource cannot send a header. So it asks Network for a ticket (`POST https://openvibe.network/api/v1/realtime/ticket`, answering `network.realtime-ticket-result@1`) and opens `/realtime/stream?topics=network.notification.*&ticket=<ticket>` without credentials. The ticket is an RS256 JWT signed with Network's key (`identity.realtime-ticket-claims@1`): `iss <OV_NETWORK_ISSUER>/realtime`, `sub <usr_>`, `aud [openvibe.events]`, `typ` and `purpose` `realtime`, a lifetime of at most 300 s (Network mints 120 s) and `jti rtk_…`.
++- Realtime tickets (ADR-005 amendment 2): a page on any OpenVibe site cannot count on a cookie of openvibe.events (third-party there), and an EventSource cannot send a header. So it asks Network for a ticket (`POST https://openvibe.network/api/v1/realtime/ticket`, answering `network.realtime-ticket-result@1`) and opens `/realtime/stream?topics=network.notification.*&ticket=<ticket>` without credentials. The ticket is an RS256 JWT signed with Network's key (`identity.realtime-ticket-claims@1`): `iss <OV_NETWORK_ISSUER>/realtime`, `sub <usr_>`, `aud [openvibe.events]`, `typ` and `purpose` `realtime`, a lifetime of at most 300 s (Network mints 120 s) and `jti rtk_…`.
- Events accepts each ticket once, never as Bearer or cookie, and never logs it. The refusals are 401: `ticket.invalid`, `ticket.expired` and `ticket.used`.
- A reconnect asks for a new ticket and resumes with `last_event_id` (the cursor from the last message's SSE `id`).
- Conversely, a user JWT that carries `typ` or `purpose` (a ticket, a FedCM assertion) is not a session here.
@@@ -356,10 +356,10 @@ Production deploys with `sudo ovhost deploy events` on the host (strategy `git-c
fast-forward `/opt/openvibe.events`, install on a lockfile change, restart, wait for `/api/ready`).
The unit is `openvibe-events.service` on `127.0.0.1:4300`, the env file `/etc/openvibe/events.env`. The database is
`ov_events` on the host's data role (`sudo /opt/openvibe.host/roles/data/add-service.sh events` writes its settings); the
--release migrates it at boot. nginx serves `events.openvibe.network` from
--[deploy/nginx/events.openvibe.network.conf](deploy/nginx/events.openvibe.network.conf); the product domain `openvibe.events` gets
--[deploy/nginx/openvibe.events.conf](deploy/nginx/openvibe.events.conf) instead, installed when openvibe.events is cut over from
--its OpenVibe.Sites placeholder (the Sites vhost of the same name is replaced; nothing else changes on `events.openvibe.network`). ovhost treats open
++release migrates it at boot. nginx serves `openvibe.events`, the API and product origin, from
++[deploy/nginx/openvibe.events.conf](deploy/nginx/openvibe.events.conf); `events.openvibe.network` answers the same
++surface from [deploy/nginx/events.openvibe.network.conf](deploy/nginx/events.openvibe.network.conf) until every client
++moves, then redirects (308). ovhost treats open
realtime connections as a report-only drain, so `--wait-idle` waits for them.
Rollback: ovhost puts the previous sha back by itself when `/api/ready` does not answer 2xx after the
restart; afterwards `sudo ovhost rollback events --to <sha>`. Migrations only add tables and columns.
ddiff --git a/STATUS.json b/STATUS.json
iindex a3ba795..486f17d 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -6,7 +6,7 @@
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"port": 4300,
-- "domain": "events.openvibe.network",
++ "domain": "openvibe.events",
"contracts": "openvibe-contracts v0.110.0",
"capabilities": [
"events.event.publish",
@@@ -19,7 +19,7 @@
],
"capabilitiesRegistered": true,
"deployed": true,
-- "deployment": "Deployed with `sudo ovhost deploy events`: /opt/openvibe.events, unit openvibe-events, 127.0.0.1:4300; public at https://events.openvibe.network (realtime SSE, health, token-guarded /api/v1/events, /api/v1/subscriptions, /api/v1/checkpoints; /api/v1/deliveries host-local); real traffic since 2026-09-23 (Live, Media, Chat, Games, Network and OpenRe publish; on 2026-09-23 8 subscriptions, held by Search, Tips, VIP, News and Reviews); restore drill passed 2026-09-23",
++ "deployment": "Deployed with `sudo ovhost deploy events`: /opt/openvibe.events, unit openvibe-events, 127.0.0.1:4300; public at https://openvibe.events (plan T7: the API and product origin; realtime SSE, health, token-guarded /api/v1/events, /api/v1/subscriptions, /api/v1/checkpoints; /api/v1/deliveries host-local; events.openvibe.network answers the same surface until every client moves, then redirects 308); real traffic since 2026-09-23 (Live, Media, Chat, Games, Network and OpenRe publish; on 2026-09-23 8 subscriptions, held by Search, Tips, VIP, News and Reviews); restore drill passed 2026-09-23",
"updated": "2026-10-07",
"features": [
"durable event store with the canonical envelope",
ddiff --git a/deploy/nginx/openvibe.events.conf b/deploy/nginx/openvibe.events.conf
iindex 0f9825b..013445a 100644
---- a/deploy/nginx/openvibe.events.conf
++++ b/deploy/nginx/openvibe.events.conf
@@@ -4,21 +4,33 @@
## Deployed to /etc/nginx/sites-available/openvibe.events.conf — keep them in sync,
## and reload nginx after changing it.
##
--## The product domain (plan expansion II): openvibe.events serves the product home page and its
--## discovery files. The API, the realtime gateway and health stay at events.openvibe.network (the
--## vhost of the same name, deploy/nginx/events.openvibe.network.conf), and every other path here
--## redirects there. Installed when openvibe.events is cut over from its OpenVibe.Sites placeholder:
--## the Sites vhost of the same name is replaced; nothing else changes on events.openvibe.network.
++## openvibe.events is the API and product origin (plan T7): the same Node service (port 4300,
++## systemd unit openvibe-events) answers the product home and its discovery files, the
++## token-guarded JSON API, the realtime gateway and health, all here. events.openvibe.network
++## keeps answering the same surface unchanged (deploy/nginx/events.openvibe.network.conf) until
++## every client has moved; a later change turns it into a redirect (308) to this host.
##
--## The Node service (port 4300, systemd unit openvibe-events) answers here:
++## The Node service (port 4300) answers here:
## / the product home (server/home.js) for a browser, the text/plain API index otherwise
## /shared/ the OpenVibe Frame's pinned browser files (openvibe-shared/serve)
## /robots.txt, /sitemap.xml, /llms.txt
## discovery for the product domain (server/discovery.js)
++## /realtime/stream browser Server-Sent Events (long-lived; must not be buffered)
++## /api/health, /api/ready, /release.json, /limits.json
++## /api/v1/events, /api/v1/subscriptions, /api/v1/checkpoints
++## public, token-guarded: developer apps (events.app.* capabilities, ADR-014)
++## and off-host services. Every request needs a Network token.
++## /api/v1/deliveries operators only (events.delivery.admin): host-local, refused here.
++## Services on this host call 127.0.0.1:4300 directly.
## /metrics host-local scrapers only (http://127.0.0.1:4300/metrics, never proxied)
## /internal/ host-local service endpoints, refused here
## ═══════════════════════════════════════════════════════════════
++# Distinct zone names: events.openvibe.network.conf declares its own and both files load in one config.
++limit_req_zone $binary_remote_addr zone=ovevents_apex_connect:10m rate=2r/s;
++limit_conn_zone $binary_remote_addr zone=ovevents_apex_streams:10m;
++limit_req_zone $binary_remote_addr zone=ovevents_apex_api:10m rate=20r/s;
++
# ── HTTP → HTTPS redirect ──────────────────────────────────
server {
listen 80;
@@@ -57,19 +69,79 @@ server {
access_log /var/log/nginx/openvibe.events.access.log;
error_log /var/log/nginx/openvibe.events.error.log;
++ client_max_body_size 2m;
++
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
++ # ── Realtime (SSE): no buffering, no compression, long reads ──
++ # The service sends a heartbeat comment every 25 s, well inside these timeouts and
++ # Cloudflare's 100 s idle limit. CORS is answered by the service (https://*.openvibe.* only).
++ location = /realtime/stream {
++ access_log /var/log/nginx/openvibe.events.access.log ov_events_noquery; # never the ?ticket= (log_format_events_noquery.conf)
++ limit_req zone=ovevents_apex_connect burst=20 nodelay;
++ limit_conn ovevents_apex_streams 20;
++ proxy_pass http://127.0.0.1:4300;
++ proxy_http_version 1.1;
++ proxy_set_header Connection "";
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $remote_addr;
++ proxy_set_header CF-Connecting-IP $remote_addr;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ proxy_set_header Last-Event-ID $http_last_event_id;
++ proxy_buffering off;
++ proxy_cache off;
++ gzip off;
++ chunked_transfer_encoding on;
++ proxy_read_timeout 1h;
++ proxy_send_timeout 1h;
++ send_timeout 1h;
++ }
++
++ # The client address comes from $remote_addr only on every proxied location: never appended to,
++ # never passed through. X-Forwarded-For on every API location: the service treats a request without it from
++ # 127.0.0.1 as a local caller (GET /metrics answers only those).
++ location = /api/health { proxy_pass http://127.0.0.1:4300; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
++ location = /api/ready { proxy_pass http://127.0.0.1:4300; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
++ location = /release.json { proxy_pass http://127.0.0.1:4300; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
++ location = /limits.json { proxy_pass http://127.0.0.1:4300; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
++
# Metrics are for host-local scrapers only (http://127.0.0.1:4300/metrics).
location = /metrics { return 404; }
-- # Internal service endpoints (Live's /internal/media-events, ...) live on the API host.
++ # Internal service endpoints (Live's /internal/media-events, ...) are host-local.
location /internal/ { return 404; }
++ # ── Token-guarded API for developer apps (see header) ──
++ # Per-project quotas are enforced by the service; this only caps per-IP request rates.
++ location ~ ^/api/v1/(events|subscriptions|checkpoints)(/|$) {
++ limit_req zone=ovevents_apex_api burst=40 nodelay;
++ proxy_pass http://127.0.0.1:4300;
++ proxy_http_version 1.1;
++ proxy_set_header Connection "";
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $remote_addr;
++ proxy_set_header CF-Connecting-IP $remote_addr;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ }
++
++ # ── Everything else under /api/: host-local only (see header) ──
++ location /api/ {
++ return 403;
++ # To expose it (token-guarded) to off-host services instead:
++ # proxy_pass http://127.0.0.1:4300;
++ # proxy_http_version 1.1;
++ # proxy_set_header Host $host;
++ # proxy_set_header X-Real-IP $remote_addr; # client address from $remote_addr only (realip),
++ # proxy_set_header X-Forwarded-For $remote_addr; # never appended to or passed through
++ # proxy_set_header CF-Connecting-IP $remote_addr;
++ # proxy_set_header X-Forwarded-Proto $scheme;
++ }
++
# ── The product surface: the home, its Frame files and its discovery files ──
-- # Client address from $remote_addr only on every proxied location (never appended to, never
-- # passed through), exactly as on events.openvibe.network.
location = / {
proxy_pass http://127.0.0.1:4300;
proxy_set_header Host $host;
@@@ -110,6 +182,6 @@ server {
proxy_set_header CF-Connecting-IP $remote_addr;
}
-- # ── Everything else is the API host: the JSON API, realtime and health live there ──
-- location / { return 301 https://events.openvibe.network$request_uri; }
++ # ── Everything else: no route here ──
++ location / { return 404; }
}
ddiff --git a/docs/cutover-12.md b/docs/cutover-12.md
iindex 2f38b88..e398edd 100644
---- a/docs/cutover-12.md
++++ b/docs/cutover-12.md
@@@ -21,7 +21,7 @@ reads span both tables. `EVENTS_REPLAY_RETENTION_DAYS` defaults to 365; `0` is t
takes milliseconds and holds no lock on `events`.
2. `/etc/openvibe/events.env`: nothing to add for the default (365 days of replay). To keep today's behaviour, set
`EVENTS_REPLAY_RETENTION_DAYS=0` and restart.
--3. Check: `curl -s https://events.openvibe.network/limits.json` lists `replay_retention_days`; after the first prune
++3. Check: `curl -s https://openvibe.events/limits.json` lists `replay_retention_days`; after the first prune
(start + up to `EVENTS_PRUNE_INTERVAL_MS`) the journal (`ov access run openvibe-ovh journal openvibe-events.service 200`)
shows `[retention] pruned N hot events (N moved to replay), …` when anything was due, and
`node scripts/events-archive.js status` (on the host, with the service env) lists the replay rows per month.
ddiff --git a/server/config.js b/server/config.js
iindex 5ac14fa..73967f2 100644
---- a/server/config.js
++++ b/server/config.js
@@@ -77,16 +77,18 @@ function load(env = process.env) {
const port = int(env.PORT, 4300);
const maxInflight = int(env.EVENTS_MAX_INFLIGHT, 20);
const prefixes = sourcePrefixes(env);
-- const baseUrl = (env.BASE_URL || (isProduction ? 'https://events.openvibe.network' : `http://localhost:${port}`)).replace(/\/$/, '');
++ // openvibe.events is the API origin and the product origin (plan T7); events.openvibe.network answers
++ // until every client moves, then redirects (308). Everything Events names about itself uses this.
++ const baseUrl = (env.BASE_URL || (isProduction ? 'https://openvibe.events' : `http://localhost:${port}`)).replace(/\/$/, '');
return {
port,
host: env.HOST || '127.0.0.1',
nodeEnv,
isProduction,
baseUrl,
-- // The product domain (openvibe.events, plan expansion II): the canonical origin of the home page
-- // and its robots.txt/sitemap.xml/llms.txt. The API stays at baseUrl. Local development follows baseUrl.
-- siteUrl: (env.EVENTS_SITE_URL || (isProduction ? 'https://openvibe.events' : baseUrl)).replace(/\/$/, ''),
++ // The product origin is the API origin (plan T7): the home page and its robots.txt/sitemap.xml/
++ // llms.txt are served from baseUrl. EVENTS_SITE_URL overrides it for a deployment that serves them apart.
++ siteUrl: (env.EVENTS_SITE_URL || baseUrl).replace(/\/$/, ''),
// Identity: OpenVibe.Network issues service tokens and user JWTs (RS256).
networkUrl: (env.OV_NETWORK_URL || 'https://openvibe.network').replace(/\/$/, ''),
ddiff --git a/server/discovery.js b/server/discovery.js
iindex a3a55e6..50fd942 100644
---- a/server/discovery.js
++++ b/server/discovery.js
@@@ -5,7 +5,7 @@
*
* GET /robots.txt welcomes search and AI crawlers to the home page, keeps the API and the
* realtime gateway out of the index, and always names the sitemap
-- * GET /sitemap.xml the product home only (the API and realtime live at events.openvibe.network)
++ * GET /sitemap.xml the product home only (openvibe.events is the API origin too, plan T7)
* GET /llms.txt what OpenVibe.Events does today, in plain language: the README's Purpose,
* Publishing, Subscriptions and delivery, Developer apps and Realtime sections
*
ddiff --git a/server/home.js b/server/home.js
iindex 0e057e2..1e605b2 100644
---- a/server/home.js
++++ b/server/home.js
@@@ -1,8 +1,8 @@
'use strict';
/**
-- * The product home of OpenVibe.Events (openvibe.events, plan expansion II: the product domain; the API stays at
-- * events.openvibe.network). The OpenVibe Frame (openvibe-shared/shell) around openvibe-shared/showcase sections
++ * The product home of OpenVibe.Events (openvibe.events, plan T7: the API origin is the product origin too).
++ * The OpenVibe Frame (openvibe-shared/shell) around openvibe-shared/showcase sections
* for developers. Every claim restates the README (Publishing, Subscriptions and delivery, Developer apps,
* Realtime) and the code; nothing here reads data.
*
@@@ -26,7 +26,7 @@ const HOME_CSP = [
"style-src 'self' 'unsafe-inline' https://openvibe.network https://fonts.googleapis.com https://cdnjs.cloudflare.com",
"font-src 'self' data: https://fonts.gstatic.com https://cdnjs.cloudflare.com",
"img-src 'self' data: https:",
-- "connect-src 'self' https://openvibe.network https://events.openvibe.network",
++ "connect-src 'self' https://openvibe.network https://openvibe.events",
"frame-src 'self' https://openvibe.network",
"frame-ancestors 'none'",
"object-src 'none'",
@@@ -59,7 +59,7 @@ function sections({ apiUrl }) {
lede: 'The event backbone of the OpenVibe network. Publish an event once and every subscriber gets it: stored durably, signed, '
+ 'retried with backoff, and replayable after a failure. Live, Media, Chat, Games, Network and OpenRe.Stream publish to it in production.',
actions: [{ label: 'Read the guide', href: `${SOURCE}#readme`, primary: true }, { label: 'See an example', href: '#examples' }],
-- note: 'Alpha. The API is at events.openvibe.network; developer apps sign in through OpenVibe.Network.',
++ note: `Alpha. The API is at ${apiUrl}; developer apps sign in through OpenVibe.Network.`,
}) + showcase.features({
title: 'What you get',
items: [
ddiff --git a/test/home.test.js b/test/home.test.js
iindex 39bd1a3..0bcb1ea 100644
---- a/test/home.test.js
++++ b/test/home.test.js
@@@ -1,12 +1,14 @@
'use strict';
--// The product home (plan expansion II): openvibe.events renders server/home.js for a browser and keeps
--// the text/plain API index for curl and API clients, serves the pinned OpenVibe Frame at /shared, and
--// answers robots.txt, sitemap.xml and llms.txt. A parse-only check of the new nginx vhost too.
++// The product home (plan T7: openvibe.events is the API and product origin): it renders server/home.js
++// for a browser and keeps the text/plain API index for curl and API clients, serves the pinned OpenVibe
++// Frame at /shared, answers robots.txt, sitemap.xml and llms.txt, and names its own origin everywhere.
++// A parse-only check of the nginx vhost too.
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const nodeHttp = require('http');
const { boot, suite } = require('./helpers');
++const { load } = require('../server/config');
const { HOME_CSP } = require('../server/home');
const SITE = 'https://openvibe.events';
@@@ -42,7 +44,15 @@ function raw(base, p, headers = {}) {
});
}
--t('boot with the product domain configured', async () => { h = await boot({ env: { EVENTS_SITE_URL: SITE } }); });
++t('in production the API origin and the product origin are the same host', () => {
++ const production = load({ NODE_ENV: 'production' });
++ assert.strictEqual(production.baseUrl, SITE, 'the API origin defaults to openvibe.events');
++ assert.strictEqual(production.siteUrl, SITE, 'the product origin is the same origin');
++});
++
++t('boot with openvibe.events as both the API and the product origin', async () => {
++ h = await boot({ env: { BASE_URL: SITE } }); // siteUrl follows baseUrl
++});
t('GET / as a browser: the home page, its CSP and its cache policy', async () => {
const r = await raw(h.base, '/', { Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' });
@@@ -54,6 +64,9 @@ t('GET / as a browser: the home page, its CSP and its cache policy', async () =>
assert.strictEqual((r.body.match(/<h1\b/g) || []).length, 1, 'exactly one h1');
assert.ok(r.body.includes('Events your services'), 'the hero text');
assert.ok(r.body.includes(`<link rel="canonical" href="${SITE}/">`), 'the canonical product origin');
++ assert.ok(r.body.includes(`The API is at ${SITE}`), 'the hero note names the API origin');
++ assert.ok(r.body.includes(`${SITE}/api/v1/events`), 'the API examples use the API origin');
++ assert.ok(!r.body.includes('events.openvibe.network'), 'the home names no other origin');
});
t('the home links the pinned Frame stylesheet, and /shared serves it', async () => {
@@@ -101,23 +114,28 @@ t('llms.txt is a plain-text map that starts with the site name', async () => {
assert.ok(r.body.startsWith('# OpenVibe.Events'), r.body.slice(0, 40));
assert.ok(r.body.includes('> Durable events for the OpenVibe network'), 'the home summary');
assert.ok(r.body.includes('https://github.com/OpenVibers/OpenVibe.Events#readme'), 'the guide');
++ assert.ok(r.body.includes(`The API and the realtime gateway are at ${SITE}`), 'the discovery names the API origin');
});
--t('deploy/nginx/openvibe.events.conf proxies the product surface and nothing else', () => {
++t('deploy/nginx/openvibe.events.conf is the API and product origin, with no redirect to the old host', () => {
const conf = fs.readFileSync(path.join(__dirname, '..', 'deploy', 'nginx', 'openvibe.events.conf'), 'utf8');
assert.match(conf, /server_name openvibe\.events;/);
assert.match(conf, /server_name www\.openvibe\.events;/);
assert.ok(conf.includes('/etc/letsencrypt/live/openvibe.events/fullchain.pem'));
assert.ok(conf.includes('/etc/letsencrypt/live/openvibe.events/privkey.pem'));
assert.ok(!conf.includes('$proxy_add_x_forwarded_for'), 'the client address is never appended to');
-- for (const loc of ['location = / {', 'location ^~ /shared/ {', 'location = /robots.txt {', 'location = /sitemap.xml {', 'location = /llms.txt {']) {
++ // The product surface (plan expansion II) and everything the API host proxies (plan T7).
++ for (const loc of ['location = / {', 'location ^~ /shared/ {', 'location = /robots.txt {', 'location = /sitemap.xml {', 'location = /llms.txt {',
++ 'location = /realtime/stream {', 'location = /api/health', 'location = /api/ready', 'location = /release.json', 'location = /limits.json']) {
assert.ok(conf.includes(loc), `${loc} is proxied`);
}
++ assert.ok(conf.includes('location ~ ^/api/v1/(events|subscriptions|checkpoints)(/|$)'), 'the token-guarded API block');
++ assert.ok(conf.includes('location /api/ {'), 'the /api/ refusal block');
++ assert.ok(conf.includes('proxy_buffering off;'), 'SSE is not buffered');
++ assert.ok(conf.includes('proxy_read_timeout 1h;'), 'SSE keeps its long read timeout');
const refusals = [conf.indexOf('location = /metrics { return 404; }'), conf.indexOf('location /internal/ { return 404; }')];
assert.ok(refusals.every((i) => i >= 0), 'metrics and /internal/ are refused');
-- const catchAll = conf.indexOf('location / { return 301 https://events.openvibe.network$request_uri; }');
-- assert.ok(catchAll >= 0, 'everything else goes to the API host');
-- assert.ok(Math.max(...refusals) < catchAll, 'the refusals come before the catch-all');
++ assert.ok(!conf.includes('events.openvibe.network$request_uri'), 'no catch-all redirect to the old API host');
assert.ok(conf.includes('return 301 https://openvibe.events$request_uri;'), ':80 and www reach the apex');
});