openvibers/openvibe.events Commits
Events: events.openvibe.network answers 308 to openvibe.events (plan T7 layer 3) (#22)
Changes
@@@ -3,7 +3,7 @@> 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://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).++**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 308 to the same path on openvibe.events.**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).@@@ -357,9 +357,8 @@ fast-forward `/opt/openvibe.events`, install on a lockfile change, restart, waitThe 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); therelease 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++[deploy/nginx/openvibe.events.conf](deploy/nginx/openvibe.events.conf); `events.openvibe.network` answers 308 to the++same path there ([deploy/nginx/events.openvibe.network.conf](deploy/nginx/events.openvibe.network.conf)). ovhost treats openrealtime 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 therestart; afterwards `sudo ovhost rollback events --to <sha>`. Migrations only add tables and columns.@@@ -4,32 +4,21 @@## Deployed to /etc/nginx/sites-available/events.openvibe.network.conf — keep them in sync,## and reload nginx after changing it.##--## The Node service (port 4300, systemd unit openvibe-events) serves:--## /realtime/stream browser Server-Sent Events (long-lived; must not be buffered)--## /api/health, /api/ready, /release.json--## /metrics Prometheus text, answered only to direct loopback callers (never proxied)--## /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.++## OpenVibe.Events' API, realtime stream and product home are on https://openvibe.events++## (deploy/nginx/openvibe.events.conf, plan T7). This old name answers 308 to the same path and query++## there: a 308 keeps the method and the body, so a POST to /api/v1/events is replayed as a POST.++## Services on this host never come through here: they call 127.0.0.1:4300 directly.## ═══════════════════════════════════════════════════════════════--limit_req_zone $binary_remote_addr zone=ovevents_connect:10m rate=2r/s;--limit_conn_zone $binary_remote_addr zone=ovevents_streams:10m;--limit_req_zone $binary_remote_addr zone=ovevents_api:10m rate=20r/s;----# ── HTTP → HTTPS redirect ──────────────────────────────────server {listen 80;listen [::]:80;server_name events.openvibe.network;location /.well-known/acme-challenge/ { root /var/www/certbot; }-- location / { return 301 https://events.openvibe.network$request_uri; }++ location / { return 308 https://openvibe.events$request_uri; }}--# ── HTTPS server ───────────────────────────────────────────server {listen 443 ssl;listen [::]:443 ssl;@@@ -43,83 +32,7 @@ server {access_log /var/log/nginx/events.openvibe.network.access.log;error_log /var/log/nginx/events.openvibe.network.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/events.openvibe.network.access.log ov_events_noquery; # never the ?ticket= (log_format_events_noquery.conf)-- limit_req zone=ovevents_connect burst=20 nodelay;-- limit_conn ovevents_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;-- }---- # X-Forwarded-For on every proxied 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; }---- # ── 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_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;-- }---- location = / {-- 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 / {-- return 404;-- }++ location / { return 308 https://openvibe.events$request_uri; }}@@@ -136,6 +136,10 @@ t('deploy/nginx/openvibe.events.conf is the API and product origin, with no rediconst 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');assert.ok(!conf.includes('events.openvibe.network$request_uri'), 'no catch-all redirect to the old API host');++ const old = fs.readFileSync(path.join(__dirname, '..', 'deploy', 'nginx', 'events.openvibe.network.conf'), 'utf8')++ .split('\n').filter((l) => !/^\s*#/.test(l)).join('\n');++ assert.ok(!old.includes('proxy_pass'), 'events.openvibe.network serves nothing itself');++ assert.strictEqual((old.match(/return 308 https:\/\/openvibe\.events\$request_uri;/g) || []).length, 2, 'HTTP and HTTPS answer 308 to openvibe.events');assert.ok(conf.includes('return 301 https://openvibe.events$request_uri;'), ':80 and www reach the apex');});@@@ -29,7 +29,7 @@ t('realtime example gets a new ticket and resumes from the last cursor', async (assert.strictEqual(options.headers.Authorization, 'Bearer test-user-token');return { ok: true, json: async () => ({ticket: `ticket-${requests}`,-- stream_url: 'https://events.openvibe.network/realtime/stream',++ stream_url: 'https://openvibe.events/realtime/stream',topics: ['network.notification.*'],}) };},
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
> Durable events, subscriptions, delivery, retry, dead letters and replay for the network.
> 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://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.
**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).
+**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 308 to the same path on openvibe.events.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §6 and §6.3.
**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).
**License:** AGPL-3.0 (same as every OpenVibe service).
@@ -357,9 +357,8 @@ fast-forward `/opt/openvibe.events`, install on a lockfile change, restart, wait
@@ -357,9 +357,8 @@ fast-forward `/opt/openvibe.events`, install on a lockfile change, restart, wait
The unit is `openvibe-events.service` on `127.0.0.1:4300`, the env file `/etc/openvibe/events.env`. The database is
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
`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 `openvibe.events`, the API and product origin, from
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
+[deploy/nginx/openvibe.events.conf](deploy/nginx/openvibe.events.conf); `events.openvibe.network` answers 308 to the
+same path there ([deploy/nginx/events.openvibe.network.conf](deploy/nginx/events.openvibe.network.conf)). ovhost treats open
realtime connections as a report-only drain, so `--wait-idle` waits for them.
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
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.
restart; afterwards `sudo ovhost rollback events --to <sha>`. Migrations only add tables and columns.
@@ -4,32 +4,21 @@
@@ -4,32 +4,21 @@
## Deployed to /etc/nginx/sites-available/events.openvibe.network.conf — keep them in sync,
## Deployed to /etc/nginx/sites-available/events.openvibe.network.conf — keep them in sync,
## and reload nginx after changing it.
## and reload nginx after changing it.
##
##
-## The Node service (port 4300, systemd unit openvibe-events) serves:
-## /realtime/stream browser Server-Sent Events (long-lived; must not be buffered)
-## /api/health, /api/ready, /release.json
-## /metrics Prometheus text, answered only to direct loopback callers (never proxied)
-## /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.
+## OpenVibe.Events' API, realtime stream and product home are on https://openvibe.events
+## (deploy/nginx/openvibe.events.conf, plan T7). This old name answers 308 to the same path and query
+## there: a 308 keeps the method and the body, so a POST to /api/v1/events is replayed as a POST.
+## Services on this host never come through here: they call 127.0.0.1:4300 directly.
## ═══════════════════════════════════════════════════════════════
## ═══════════════════════════════════════════════════════════════
-limit_req_zone $binary_remote_addr zone=ovevents_connect:10m rate=2r/s;
-limit_conn_zone $binary_remote_addr zone=ovevents_streams:10m;
-limit_req_zone $binary_remote_addr zone=ovevents_api:10m rate=20r/s;
-
-# ── HTTP → HTTPS redirect ──────────────────────────────────
server {
server {
listen 80;
listen 80;
listen [::]:80;
listen [::]:80;
server_name events.openvibe.network;
server_name events.openvibe.network;
location /.well-known/acme-challenge/ { root /var/www/certbot; }
location /.well-known/acme-challenge/ { root /var/www/certbot; }
- location / { return 301 https://events.openvibe.network$request_uri; }
+ location / { return 308 https://openvibe.events$request_uri; }
}
}
-# ── HTTPS server ───────────────────────────────────────────
server {
server {
listen 443 ssl;
listen 443 ssl;
listen [::]:443 ssl;
listen [::]:443 ssl;
@@ -43,83 +32,7 @@ server {
@@ -43,83 +32,7 @@ server {
access_log /var/log/nginx/events.openvibe.network.access.log;
access_log /var/log/nginx/events.openvibe.network.access.log;
error_log /var/log/nginx/events.openvibe.network.error.log;
error_log /var/log/nginx/events.openvibe.network.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;
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/events.openvibe.network.access.log ov_events_noquery; # never the ?ticket= (log_format_events_noquery.conf)
- limit_req zone=ovevents_connect burst=20 nodelay;
- limit_conn ovevents_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;
- }
-
- # X-Forwarded-For on every proxied 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; }
-
- # ── 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_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;
- }
-
- location = / {
- 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 / {
- return 404;
- }
+ location / { return 308 https://openvibe.events$request_uri; }
}
}
@@ -136,6 +136,10 @@ t('deploy/nginx/openvibe.events.conf is the API and product origin, with no redi
@@ -136,6 +136,10 @@ t('deploy/nginx/openvibe.events.conf is the API and product origin, with no redi
const refusals = [conf.indexOf('location = /metrics { return 404; }'), conf.indexOf('location /internal/ { return 404; }')];
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');
assert.ok(refusals.every((i) => i >= 0), 'metrics and /internal/ are refused');
assert.ok(!conf.includes('events.openvibe.network$request_uri'), 'no catch-all redirect to the old API host');
assert.ok(!conf.includes('events.openvibe.network$request_uri'), 'no catch-all redirect to the old API host');
+ const old = fs.readFileSync(path.join(__dirname, '..', 'deploy', 'nginx', 'events.openvibe.network.conf'), 'utf8')
+ .split('\n').filter((l) => !/^\s*#/.test(l)).join('\n');
+ assert.ok(!old.includes('proxy_pass'), 'events.openvibe.network serves nothing itself');
+ assert.strictEqual((old.match(/return 308 https:\/\/openvibe\.events\$request_uri;/g) || []).length, 2, 'HTTP and HTTPS answer 308 to openvibe.events');
assert.ok(conf.includes('return 301 https://openvibe.events$request_uri;'), ':80 and www reach the apex');
assert.ok(conf.includes('return 301 https://openvibe.events$request_uri;'), ':80 and www reach the apex');
});
});
@@ -29,7 +29,7 @@ t('realtime example gets a new ticket and resumes from the last cursor', async (
@@ -29,7 +29,7 @@ t('realtime example gets a new ticket and resumes from the last cursor', async (
assert.strictEqual(options.headers.Authorization, 'Bearer test-user-token');
assert.strictEqual(options.headers.Authorization, 'Bearer test-user-token');
return { ok: true, json: async () => ({
return { ok: true, json: async () => ({
ticket: `ticket-${requests}`,
ticket: `ticket-${requests}`,
- stream_url: 'https://events.openvibe.network/realtime/stream',
+ stream_url: 'https://openvibe.events/realtime/stream',
topics: ['network.notification.*'],
topics: ['network.notification.*'],
}) };
}) };
},
},