openvibers/openvibe.events Commits
Events: a product home for openvibe.events (browsers get the page, API clients the text index) and its vhost. (#16)
Changes
@@@ -10,6 +10,8 @@ PORT=4300HOST=127.0.0.1NODE_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.++#EVENTS_SITE_URL=https://openvibe.events# Identity — OpenVibe.Network signs service tokens (client_credentials, audience# openvibe.events) and user JWTs. The public key is fetched from@@@ -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://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)++**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.**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).@@@ -346,7 +346,9 @@ 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 `events.openvibe.network` from--[deploy/nginx/events.openvibe.network.conf](deploy/nginx/events.openvibe.network.conf). ovhost treats open++[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 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.@@@ -20,7 +20,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",-- "updated": "2026-10-05",++ "updated": "2026-10-07","features": ["durable event store with the canonical envelope","subscriptions with signed webhook delivery (v1 and v2), retry, dead letters and replay",nnew file mode 100644@@@ -0,0 +1,115 @@++## ═══════════════════════════════════════════════════════════════++## openvibe.events — Nginx Configuration (REFERENCE)++##++## 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.++##++## The Node service (port 4300, systemd unit openvibe-events) 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)++## /metrics host-local scrapers only (http://127.0.0.1:4300/metrics, never proxied)++## /internal/ host-local service endpoints, refused here++## ═══════════════════════════════════════════════════════════════++++# ── HTTP → HTTPS redirect ──────────────────────────────────++server {++ listen 80;++ listen [::]:80;++ server_name openvibe.events www.openvibe.events;++++ location /.well-known/acme-challenge/ { root /var/www/certbot; }++ location / { return 301 https://openvibe.events$request_uri; }++}++++# ── www → apex ─────────────────────────────────────────────++server {++ listen 443 ssl;++ listen [::]:443 ssl;++ http2 on;++ server_name www.openvibe.events;++++ ssl_certificate /etc/letsencrypt/live/openvibe.events/fullchain.pem;++ ssl_certificate_key /etc/letsencrypt/live/openvibe.events/privkey.pem;++ ssl_protocols TLSv1.2 TLSv1.3;++++ return 301 https://openvibe.events$request_uri;++}++++# ── HTTPS server ───────────────────────────────────────────++server {++ listen 443 ssl;++ listen [::]:443 ssl;++ http2 on;++ server_name openvibe.events;++++ ssl_certificate /etc/letsencrypt/live/openvibe.events/fullchain.pem;++ ssl_certificate_key /etc/letsencrypt/live/openvibe.events/privkey.pem;++ ssl_protocols TLSv1.2 TLSv1.3;++++ access_log /var/log/nginx/openvibe.events.access.log;++ error_log /var/log/nginx/openvibe.events.error.log;++++ 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;++++ # 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.++ location /internal/ { return 404; }++++ # ── 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;++ 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 ^~ /shared/ {++ 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 = /robots.txt {++ 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 = /sitemap.xml {++ 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 = /llms.txt {++ 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;++ }++++ # ── Everything else is the API host: the JSON API, realtime and health live there ──++ location / { return 301 https://events.openvibe.network$request_uri; }++}@@@ -14,7 +14,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.94.2","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.5.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.10.1","pg": "^8.23.0"},"devDependencies": {@@@ -89,65 +89,6 @@"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==","license": "MIT"},-- "node_modules/base64-js": {-- "version": "1.5.1",-- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",-- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",-- "funding": [-- {-- "type": "github",-- "url": "https://github.com/sponsors/feross"-- },-- {-- "type": "patreon",-- "url": "https://www.patreon.com/feross"-- },-- {-- "type": "consulting",-- "url": "https://feross.org/support"-- }-- ],-- "license": "MIT",-- "optional": true,-- "peer": true-- },-- "node_modules/better-sqlite3": {-- "version": "11.10.0",-- "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",-- "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",-- "hasInstallScript": true,-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "bindings": "^1.5.0",-- "prebuild-install": "^7.1.1"-- }-- },-- "node_modules/bindings": {-- "version": "1.5.0",-- "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",-- "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "file-uri-to-path": "1.0.0"-- }-- },-- "node_modules/bl": {-- "version": "4.1.0",-- "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",-- "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "buffer": "^5.5.0",-- "inherits": "^2.0.4",-- "readable-stream": "^3.4.0"-- }-- },"node_modules/body-parser": {"version": "1.20.8","resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz",@@@ -172,32 +113,6 @@"npm": "1.2.8000 || >= 1.4.16"}},-- "node_modules/buffer": {-- "version": "5.7.1",-- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",-- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",-- "funding": [-- {-- "type": "github",-- "url": "https://github.com/sponsors/feross"-- },-- {-- "type": "patreon",-- "url": "https://www.patreon.com/feross"-- },-- {-- "type": "consulting",-- "url": "https://feross.org/support"-- }-- ],-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "base64-js": "^1.3.1",-- "ieee754": "^1.1.13"-- }-- },"node_modules/buffer-equal-constant-time": {"version": "1.0.1","resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",@@@ -242,14 +157,6 @@"url": "https://github.com/sponsors/ljharb"}},-- "node_modules/chownr": {-- "version": "1.1.4",-- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",-- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",-- "license": "ISC",-- "optional": true,-- "peer": true-- },"node_modules/cluster-key-slot": {"version": "1.1.2","resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",@@@ -304,34 +211,6 @@"ms": "2.0.0"}},-- "node_modules/decompress-response": {-- "version": "6.0.0",-- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",-- "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "mimic-response": "^3.1.0"-- },-- "engines": {-- "node": ">=10"-- },-- "funding": {-- "url": "https://github.com/sponsors/sindresorhus"-- }-- },-- "node_modules/deep-extend": {-- "version": "0.6.0",-- "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",-- "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "engines": {-- "node": ">=4.0.0"-- }-- },"node_modules/denque": {"version": "2.1.0","resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",@@@ -360,17 +239,6 @@"npm": "1.2.8000 || >= 1.4.16"}},-- "node_modules/detect-libc": {-- "version": "2.1.2",-- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",-- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",-- "license": "Apache-2.0",-- "optional": true,-- "peer": true,-- "engines": {-- "node": ">=8"-- }-- },"node_modules/dotenv": {"version": "16.6.1","resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",@@@ -421,17 +289,6 @@"node": ">= 0.8"}},-- "node_modules/end-of-stream": {-- "version": "1.4.5",-- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",-- "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "once": "^1.4.0"-- }-- },"node_modules/es-define-property": {"version": "1.0.1","resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",@@@ -477,17 +334,6 @@"node": ">= 0.6"}},-- "node_modules/expand-template": {-- "version": "2.0.3",-- "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",-- "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",-- "license": "(MIT OR WTFPL)",-- "optional": true,-- "peer": true,-- "engines": {-- "node": ">=6"-- }-- },"node_modules/express": {"version": "4.22.3","resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz",@@@ -556,14 +402,6 @@],"license": "BSD-3-Clause"},-- "node_modules/file-uri-to-path": {-- "version": "1.0.0",-- "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",-- "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",-- "license": "MIT",-- "optional": true,-- "peer": true-- },"node_modules/finalhandler": {"version": "1.3.2","resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",@@@ -600,14 +438,6 @@"node": ">= 0.6"}},-- "node_modules/fs-constants": {-- "version": "1.0.0",-- "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",-- "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",-- "license": "MIT",-- "optional": true,-- "peer": true-- },"node_modules/function-bind": {"version": "1.1.2","resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",@@@ -654,14 +484,6 @@"node": ">= 0.4"}},-- "node_modules/github-from-package": {-- "version": "0.0.0",-- "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",-- "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",-- "license": "MIT",-- "optional": true,-- "peer": true-- },"node_modules/gopd": {"version": "1.2.0","resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",@@@ -730,42 +552,12 @@"node": ">=0.10.0"}},-- "node_modules/ieee754": {-- "version": "1.2.1",-- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",-- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",-- "funding": [-- {-- "type": "github",-- "url": "https://github.com/sponsors/feross"-- },-- {-- "type": "patreon",-- "url": "https://www.patreon.com/feross"-- },-- {-- "type": "consulting",-- "url": "https://feross.org/support"-- }-- ],-- "license": "BSD-3-Clause",-- "optional": true,-- "peer": true-- },"node_modules/inherits": {"version": "2.0.4","resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz","integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==","license": "ISC"},-- "node_modules/ini": {-- "version": "1.3.8",-- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",-- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",-- "license": "ISC",-- "optional": true,-- "peer": true-- },"node_modules/iovalkey": {"version": "0.4.0","resolved": "https://registry.npmjs.org/iovalkey/-/iovalkey-0.4.0.tgz",@@@ -996,53 +788,12 @@"node": ">= 0.6"}},-- "node_modules/mimic-response": {-- "version": "3.1.0",-- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",-- "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "engines": {-- "node": ">=10"-- },-- "funding": {-- "url": "https://github.com/sponsors/sindresorhus"-- }-- },-- "node_modules/minimist": {-- "version": "1.2.8",-- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",-- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "funding": {-- "url": "https://github.com/sponsors/ljharb"-- }-- },-- "node_modules/mkdirp-classic": {-- "version": "0.5.3",-- "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",-- "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",-- "license": "MIT",-- "optional": true,-- "peer": true-- },"node_modules/ms": {"version": "2.0.0","resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz","integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==","license": "MIT"},-- "node_modules/napi-build-utils": {-- "version": "2.0.0",-- "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",-- "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",-- "license": "MIT",-- "optional": true,-- "peer": true-- },"node_modules/negotiator": {"version": "0.6.3","resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",@@@ -1052,20 +803,6 @@"node": ">= 0.6"}},-- "node_modules/node-abi": {-- "version": "3.96.0",-- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz",-- "integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "semver": "^7.3.5"-- },-- "engines": {-- "node": ">=10"-- }-- },"node_modules/object-inspect": {"version": "1.13.4","resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",@@@ -1090,17 +827,6 @@"node": ">= 0.8"}},-- "node_modules/once": {-- "version": "1.4.0",-- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",-- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",-- "license": "ISC",-- "optional": true,-- "peer": true,-- "dependencies": {-- "wrappy": "1"-- }-- },"node_modules/openvibe-contracts": {"version": "0.94.2","resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.94.2",@@@ -1147,9 +873,9 @@}},"node_modules/openvibe-shared": {-- "version": "2.5.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.5.0",-- "integrity": "sha512-fHuUS6SXM7ezatfAyqLm1V8B6puV9YXSnz6aG6XQbWx28qghRCCHeUF3lr4nxiCGdn9SyikYtIyLGL5r1ycp9Q==",++ "version": "2.10.1",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.10.1",++ "integrity": "sha512-6fUOw22bSvxg0K33BefAyAkNRkETe9euLNT0CHs2mLIvzYR1q38KiHRUQIWj8FESbVANF0pXMojhBz5TeqpMtg==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -1301,35 +1027,6 @@"node": ">=0.10.0"}},-- "node_modules/prebuild-install": {-- "version": "7.1.3",-- "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",-- "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",-- "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "detect-libc": "^2.0.0",-- "expand-template": "^2.0.3",-- "github-from-package": "0.0.0",-- "minimist": "^1.2.3",-- "mkdirp-classic": "^0.5.3",-- "napi-build-utils": "^2.0.0",-- "node-abi": "^3.3.0",-- "pump": "^3.0.0",-- "rc": "^1.2.7",-- "simple-get": "^4.0.0",-- "tar-fs": "^2.0.0",-- "tunnel-agent": "^0.6.0"-- },-- "bin": {-- "prebuild-install": "bin.js"-- },-- "engines": {-- "node": ">=10"-- }-- },"node_modules/proxy-addr": {"version": "2.0.8","resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",@@@ -1347,18 +1044,6 @@"url": "https://opencollective.com/express"}},-- "node_modules/pump": {-- "version": "3.0.4",-- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",-- "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "end-of-stream": "^1.1.0",-- "once": "^1.3.1"-- }-- },"node_modules/qs": {"version": "6.16.0","resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",@@@ -1399,39 +1084,6 @@"node": ">= 0.8"}},-- "node_modules/rc": {-- "version": "1.2.8",-- "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",-- "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",-- "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",-- "optional": true,-- "peer": true,-- "dependencies": {-- "deep-extend": "^0.6.0",-- "ini": "~1.3.0",-- "minimist": "^1.2.0",-- "strip-json-comments": "~2.0.1"-- },-- "bin": {-- "rc": "cli.js"-- }-- },-- "node_modules/readable-stream": {-- "version": "3.6.2",-- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",-- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "inherits": "^2.0.3",-- "string_decoder": "^1.1.1",-- "util-deprecate": "^1.0.1"-- },-- "engines": {-- "node": ">= 6"-- }-- },"node_modules/redis-errors": {"version": "1.2.0","resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",@@@ -1623,55 +1275,6 @@"url": "https://github.com/sponsors/ljharb"}},-- "node_modules/simple-concat": {-- "version": "1.0.1",-- "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",-- "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",-- "funding": [-- {-- "type": "github",-- "url": "https://github.com/sponsors/feross"-- },-- {-- "type": "patreon",-- "url": "https://www.patreon.com/feross"-- },-- {-- "type": "consulting",-- "url": "https://feross.org/support"-- }-- ],-- "license": "MIT",-- "optional": true,-- "peer": true-- },-- "node_modules/simple-get": {-- "version": "4.0.1",-- "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",-- "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",-- "funding": [-- {-- "type": "github",-- "url": "https://github.com/sponsors/feross"-- },-- {-- "type": "patreon",-- "url": "https://www.patreon.com/feross"-- },-- {-- "type": "consulting",-- "url": "https://feross.org/support"-- }-- ],-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "decompress-response": "^6.0.0",-- "once": "^1.3.1",-- "simple-concat": "^1.0.0"-- }-- },"node_modules/split2": {"version": "4.2.0","resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",@@@ -1696,60 +1299,6 @@"node": ">= 0.8"}},-- "node_modules/string_decoder": {-- "version": "1.3.0",-- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",-- "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "safe-buffer": "~5.2.0"-- }-- },-- "node_modules/strip-json-comments": {-- "version": "2.0.1",-- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",-- "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "engines": {-- "node": ">=0.10.0"-- }-- },-- "node_modules/tar-fs": {-- "version": "2.1.5",-- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",-- "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "chownr": "^1.1.1",-- "mkdirp-classic": "^0.5.2",-- "pump": "^3.0.0",-- "tar-stream": "^2.1.4"-- }-- },-- "node_modules/tar-stream": {-- "version": "2.2.0",-- "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",-- "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",-- "license": "MIT",-- "optional": true,-- "peer": true,-- "dependencies": {-- "bl": "^4.0.3",-- "end-of-stream": "^1.4.1",-- "fs-constants": "^1.0.0",-- "inherits": "^2.0.3",-- "readable-stream": "^3.1.1"-- },-- "engines": {-- "node": ">=6"-- }-- },"node_modules/toidentifier": {"version": "1.0.1","resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",@@@ -1759,20 +1308,6 @@"node": ">=0.6"}},-- "node_modules/tunnel-agent": {-- "version": "0.6.0",-- "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",-- "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",-- "license": "Apache-2.0",-- "optional": true,-- "peer": true,-- "dependencies": {-- "safe-buffer": "^5.0.1"-- },-- "engines": {-- "node": "*"-- }-- },"node_modules/type-is": {"version": "1.6.18","resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",@@@ -1795,14 +1330,6 @@"node": ">= 0.8"}},-- "node_modules/util-deprecate": {-- "version": "1.0.2",-- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",-- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",-- "license": "MIT",-- "optional": true,-- "peer": true-- },"node_modules/utils-merge": {"version": "1.0.1","resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",@@@ -1821,14 +1348,6 @@"node": ">= 0.8"}},-- "node_modules/wrappy": {-- "version": "1.0.2",-- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",-- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",-- "license": "ISC",-- "optional": true,-- "peer": true-- },"node_modules/xtend": {"version": "4.0.2","resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",@@@ -26,7 +26,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.94.2","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.5.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.10.1","pg": "^8.23.0"},"engines": {@@@ -13,6 +13,9 @@ const { policiesRouter } = require('./api/policies');const { placementRouter } = require('./api/placement');const { mountLimits } = require('./limits');const { createLimits } = require('./actor-limits');++const { renderHome, HOME_CSP } = require('./home');++const { createDiscoveryRoutes } = require('./discovery');++const ovServe = require('openvibe-shared/serve');const pkg = require('../package.json');function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLookup, clock, log = console, valkey = null, policies = null, planner = null, carriers = null }) {@@@ -33,6 +36,11 @@ function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLonext();});++ // The product home's own browser files (the OpenVibe Frame, openvibe-shared/serve) under++ // content-addressed /shared/* URLs, from this repo's pinned openvibe-shared. Mounted before the++ // routers and outside the per-actor limiter: it is static and unauthenticated.++ app.use('/shared', ovServe.handler());++// Realtime first: it answers CORS preflights and must not go through the JSON body parser.app.options('/realtime/stream', realtime.cors);app.get('/realtime/stream', realtime.cors, realtime.handler);@@@ -83,7 +91,22 @@ function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLoif (policies) app.use(policiesRouter({ policies, auth, limits }));if (planner) app.use(placementRouter({ planner, policies, auth, limits, store }));-- app.get('/', (_req, res) => {++ // Discovery for the product domain (openvibe.events): robots.txt, sitemap.xml, llms.txt.++ app.use(createDiscoveryRoutes({ config }));++++ // GET / on the product domain: the home page for a browser (server/home.js, openvibe.events), the++ // API index for curl and API clients (`*/*`, no Accept, or any non-HTML Accept). Vary: Accept so a++ // shared cache never serves one to the other. The home carries its own CSP (HOME_CSP); the only++ // global security header is X-Content-Type-Options above, kept on every route.++ app.get('/', (req, res) => {++ res.vary('Accept');++ if (req.accepts(['text/plain', 'text/html']) === 'text/html') {++ return res.type('html')++ .set('Content-Security-Policy', HOME_CSP)++ // private: Cloudflare caches by URL and ignores Vary: Accept, so a shared copy could reach an API client.++ .set('Cache-Control', 'private, max-age=300')++ .send(renderHome({ siteUrl: config.siteUrl, apiUrl: config.baseUrl }));++ }res.type('text/plain').send(['OpenVibe.Events: durable events, subscriptions, signed delivery, dead letters and replay.','',@@@ -73,12 +73,16 @@ 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(/\/$/, '');return {port,host: env.HOST || '127.0.0.1',nodeEnv,isProduction,-- baseUrl: (env.BASE_URL || (isProduction ? 'https://events.openvibe.network' : `http://localhost:${port}`)).replace(/\/$/, ''),++ 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(/\/$/, ''),// Identity: OpenVibe.Network issues service tokens and user JWTs (RS256).networkUrl: (env.OV_NETWORK_URL || 'https://openvibe.network').replace(/\/$/, ''),nnew file mode 100644@@@ -0,0 +1,71 @@++'use strict';++/**++ * Crawl and machine-readability artifacts for openvibe.events, the product domain, built from++ * openvibe-shared/seo the same way every other OpenVibe site builds them:++ *++ * 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 /llms.txt what OpenVibe.Events does today, in plain language: the README's Purpose,++ * Publishing, Subscriptions and delivery, Developer apps and Realtime sections++ *++ * Read-only and viewer-independent: built from the config (the product site URL and the API origin).++ */++const express = require('express');++const cache = require('openvibe-shared/cache-policy');++const seo = require('openvibe-shared/seo');++const { DESCRIPTION } = require('./home');++++const SOURCE = 'https://github.com/OpenVibers/OpenVibe.Events';++const GUIDE = `${SOURCE}#readme`;++++/** The README's Purpose, Publishing, Subscriptions and delivery, Developer apps and Realtime in plain sentences. */++function details({ siteUrl, apiUrl }) {++ return [++ 'OpenVibe.Events is the durable event backbone of the OpenVibe network. Every authoritative service writes its domain mutation and its outbox event in one transaction, a relay publishes them here, consumers keep inbox/idempotency receipts, and browsers get authorized, resumable projections over the realtime stream; an event exists if and only if the change it describes committed.',++ '',++ `Publishing: POST ${apiUrl}/api/v1/events takes one event envelope or up to a hundred at a time, stored atomically and given one global order. A service may only publish the event types its own namespace owns, a repeat of an event id is answered 200 with duplicate: true and is never stored twice, and a producer can redact what it published: the named events become tombstones that every read path serves from then on.`,++ '',++ `Subscriptions and delivery: a subscription is a topic pattern such as media.vod.* and an endpoint (http on 127.0.0.1 or an OpenVibe host). Each delivery is an HMAC-signed POST (X-OpenVibe-Signature-V2, over the timestamp and the raw body, inside a five-minute window) and answers 2xx once the event is safely stored; anything else is retried with backoff and, after the last attempt, waits in a dead-letter queue that operators replay. Consumers can also pull in order instead, with an opaque cursor that crosses the retention tiers without a gap.`,++ '',++ 'Developer apps: an app signs in through OpenVibe.Network (client credentials, audience openvibe.events) and publishes, reads and subscribes on the same routes under the events.app.* capabilities, scoped to its own project and environment. Sandbox events never reach production, app webhook endpoints must be public HTTPS addresses, and per-project quotas are enforced here.',++ '',++ `Realtime: browsers follow topics over server-sent events at ${apiUrl}/realtime/stream with a one-use ticket from OpenVibe.Network; a reconnect resumes from its last cursor, or is told about the gap when the position is too old to replay. Subject-visible events reach only the person they are about, and internal events never reach a browser.`,++ '',++ `The guide is ${GUIDE}. The API and the realtime gateway are at ${apiUrl}; the product home is ${siteUrl}/.`,++ ].join('\n');++}++++function createDiscoveryRoutes({ config }) {++ const router = express.Router();++ const abs = (p) => seo.absolute(p, config.siteUrl);++++ router.get('/robots.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders())++ .send(seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: ['/api/', '/realtime/'] })));++++ router.get('/sitemap.xml', (_req, res) => res.type('application/xml').set('Cache-Control', cache.htmlHeaders())++ .send(seo.sitemapXml([{ loc: abs('/') }])));++++ router.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders())++ .send(seo.llmsTxt({++ name: 'OpenVibe.Events',++ summary: DESCRIPTION,++ details: details({ siteUrl: config.siteUrl, apiUrl: config.baseUrl }),++ sections: [++ { title: 'Start here', links: [++ { title: 'OpenVibe.Events home', url: abs('/') },++ { title: 'Guide', url: GUIDE, note: 'the README: publishing, subscriptions, developer apps and realtime' },++ { title: 'Source code', url: SOURCE },++ ] },++ { title: 'Machine-readable', links: [++ { title: 'Sitemap', url: abs('/sitemap.xml') },++ { title: 'robots.txt', url: abs('/robots.txt') },++ { title: 'API and realtime', url: config.baseUrl, note: 'the JSON API and the realtime gateway' },++ ] },++ ],++ })));++++ return router;++}++++module.exports = { createDiscoveryRoutes };nnew file mode 100644@@@ -0,0 +1,121 @@++'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++ * for developers. Every claim restates the README (Publishing, Subscriptions and delivery, Developer apps,++ * Realtime) and the code; nothing here reads data.++ *++ * renderHome({ siteUrl, apiUrl }) the whole document (siteUrl: the canonical product origin; apiUrl: the API origin)++ * HOME_CSP its Content-Security-Policy (the Frame's scripts and calls to the Network)++ */++const shell = require('openvibe-shared/shell');++const showcase = require('openvibe-shared/showcase');++const ovServe = require('openvibe-shared/serve');++const appIcon = require('openvibe-shared/app-icon');++++const SITE_NAME = 'OpenVibe.Events';++const NETWORK_URL = 'https://openvibe.network';++const SOURCE = 'https://github.com/OpenVibers/OpenVibe.Events';++const DESCRIPTION = 'Durable events for the OpenVibe network: publish once, and every subscriber gets it signed, retried and replayable. '++ + 'Webhooks, pull with a cursor, dead letters, replay and realtime streams for browsers.';++++const HOME_CSP = [++ "default-src 'self'",++ "script-src 'self' 'unsafe-inline' https://openvibe.network",++ "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",++ "frame-src 'self' https://openvibe.network",++ "frame-ancestors 'none'",++ "object-src 'none'",++ "base-uri 'self'",++ "form-action 'self' https://openvibe.network",++].join('; ');++++function sections({ apiUrl }) {++ const publish = [++ `curl -X POST ${apiUrl}/api/v1/events \\`,++ ' -H "Authorization: Bearer $TOKEN" -H \'Content-Type: application/json\' \\',++ ' -d \'{ "event_id": "evt_01JAB2C3D4E5F6G7H8J9K0MNPQ", "event_type": "app.<project_key>.order.created",',++ ' "version": 1, "source": "app-<app id>", "actor": { "type": "app", "id": "app_<ULID>" },',++ ' "timestamp": "2026-10-07T12:00:00Z", "subject": { "type": "order", "id": "42" },',++ ' "payload": { "total": 3 } }\'',++ ].join('\n');++ const subscribe = [++ `curl -X POST ${apiUrl}/api/v1/subscriptions \\`,++ ' -H "Authorization: Bearer $TOKEN" -H \'Content-Type: application/json\' \\',++ ' -d \'{ "topic_pattern": "app.<project_key>.*", "endpoint": "https://hooks.example.com/openvibe" }\'',++ '# → { "id": "sub_…", "secret": "whsec_…" } the secret is shown once',++ ].join('\n');++ const pull = [++ `curl "${apiUrl}/api/v1/events?topic=app.<project_key>.*&limit=100" -H "Authorization: Bearer $TOKEN"`,++ '# then the same with &after=<next_cursor> from the previous page',++ ].join('\n');++ return showcase.hero({++ eyebrow: 'OpenVibe.Events · alpha',++ title: 'Events your services', accent: 'can count on.',++ 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.',++ }) + showcase.features({++ title: 'What you get',++ items: [++ { icon: 'ov:upload', title: 'Publish once', text: 'One event or up to a hundred at a time, stored atomically and numbered in order. Sending the same event id twice never stores it twice.' },++ { icon: 'ov:bell', title: 'Signed webhooks', text: 'Subscribe with a topic pattern such as media.vod.*. Every delivery is HMAC-signed with its timestamp and retried with backoff.' },++ { icon: 'ov:history', title: 'Dead letters and replay', text: 'Deliveries that keep failing wait in a dead-letter queue. Replay them, or catch a new subscription up from a point in its history.' },++ { icon: 'ov:download', title: 'Pull with a cursor', text: 'Read events in order with an opaque cursor when you would rather pull than receive webhooks.' },++ { icon: 'ov:live', title: 'Realtime for browsers', text: 'Server-sent events, authorized per topic with a one-use ticket from OpenVibe.Network, and resumable after a reconnect.' },++ { icon: 'ov:code', title: 'Developer apps', text: 'An app publishes under its own project\'s names and keeps sandbox and production apart: sandbox events never reach production.' },++ ],++ }) + showcase.code({++ id: 'examples',++ title: 'Publish, subscribe, pull',++ lede: 'With an app token from OpenVibe.Network (client credentials, audience openvibe.events).',++ samples: [++ { label: 'Publish', lang: 'bash', code: publish },++ { label: 'Subscribe', lang: 'bash', code: subscribe },++ { label: 'Pull', lang: 'bash', code: pull },++ ],++ }) + showcase.steps({++ title: 'Receive a webhook safely',++ items: [++ { title: 'Keep the secret', text: 'The subscription\'s secret is shown once, when you create it or rotate it.' },++ { title: 'Check the signature', text: 'Verify X-OpenVibe-Signature-V2 over the timestamp and the raw body, and refuse anything older than five minutes.' },++ { title: 'Answer quickly', text: 'Reply 2xx once the event is safely stored; anything else is retried, then dead-lettered.' },++ { title: 'Expect repeats', text: 'A retry can bring the same event again: keep its event id and handle it once.' },++ ],++ });++}++++function renderHome({ siteUrl, apiUrl }) {++ const nav = {++ service: 'events',++ apiBase: NETWORK_URL,++ links: [{ label: 'Guide', href: `${SOURCE}#readme` }],++ history: { type: 'page', title: SITE_NAME },++ };++ const footer = { service: 'events', variant: 'full', mount: '#ov-footer', brandName: SITE_NAME };++ return shell.page({++ name: SITE_NAME, service: 'events', lang: 'en',++ title: `${SITE_NAME}: durable events, signed webhooks and replay`,++ siteName: SITE_NAME,++ description: DESCRIPTION,++ summary: DESCRIPTION,++ canonical: `${siteUrl}/`,++ robots: 'index, follow',++ navbar: nav, footer, home: '/', navLinks: [{ label: 'Guide', href: `${SOURCE}#readme` }],++ head: [++ appIcon.headTags({ site: 'events' }),++ `<link rel="stylesheet" href="${ovServe.url(showcase.STYLESHEET)}">`,++ ].join('\n'),++ body: `<div id="navbar-mount"></div>++<main id="main" class="page">++${sections({ apiUrl })}++</main>`,++ });++}++++module.exports = { renderHome, HOME_CSP, DESCRIPTION, sections };nnew file mode 100644@@@ -0,0 +1,126 @@++'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.++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const nodeHttp = require('http');++const { boot, suite } = require('./helpers');++const { HOME_CSP } = require('../server/home');++++const SITE = 'https://openvibe.events';++++// GET / without an Accept a browser would send: the API index, byte for byte, as it was before the home.++const INDEX = [++ 'OpenVibe.Events: durable events, subscriptions, signed delivery, dead letters and replay.',++ '',++ 'POST /api/v1/events publish (service token, events.event.publish; app token, events.app.publish)',++ 'GET /api/v1/events pull with a cursor (events.event.read; app token, events.app.read)',++ ' /api/v1/subscriptions webhook subscriptions (events.subscription.manage; app token, events.app.subscribe)',++ ' /api/v1/deliveries DLQ inspect and replay (events.delivery.admin)',++ 'GET /realtime/stream?topics=... server-sent events for browsers',++ 'GET /api/health, /api/ready, /release.json, /limits.json',++ '',++ 'Source: https://github.com/OpenVibers/OpenVibe.Events',++ '',++].join('\n');++++const t = suite('home');++let h;++++/** A raw GET with exactly the headers given: node:http sends no Accept unless the caller sets one. */++function raw(base, p, headers = {}) {++ return new Promise((resolve, reject) => {++ const url = new URL(base + p);++ nodeHttp.get({ hostname: url.hostname, port: url.port, path: url.pathname + url.search, headers }, (res) => {++ let body = '';++ res.setEncoding('utf8');++ res.on('data', (c) => { body += c; });++ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));++ }).on('error', reject);++ });++}++++t('boot with the product domain configured', async () => { h = await boot({ env: { EVENTS_SITE_URL: SITE } }); });++++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' });++ assert.strictEqual(r.status, 200);++ assert.match(r.headers['content-type'], /^text\/html/);++ assert.strictEqual(r.headers['content-security-policy'], HOME_CSP);++ assert.strictEqual(r.headers['cache-control'], 'private, max-age=300');++ assert.strictEqual(r.headers.vary, 'Accept');++ 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');++});++++t('the home links the pinned Frame stylesheet, and /shared serves it', async () => {++ const r = await raw(h.base, '/', { Accept: 'text/html' });++ const href = /<link rel="stylesheet" href="([^"]+)"/.exec(r.body);++ assert.ok(href && href[1].startsWith('/shared/showcase.css?v='), `showcase.css link, got ${href && href[1]}`);++ const sheet = await raw(h.base, href[1]);++ assert.strictEqual(sheet.status, 200);++ assert.match(sheet.headers['content-type'], /^text\/css/);++ assert.ok(sheet.body.includes('.ov-'), 'the showcase stylesheet');++});++++t('GET / for curl and API clients: the text/plain index, byte for byte', async () => {++ // `*/*`, no Accept at all, and a JSON client: all three get the API index.++ for (const headers of [{ Accept: '*/*' }, {}, { Accept: 'application/json' }]) {++ const r = await raw(h.base, '/', headers);++ assert.strictEqual(r.status, 200, JSON.stringify(headers));++ assert.match(r.headers['content-type'], /^text\/plain/, JSON.stringify(headers));++ assert.strictEqual(r.headers.vary, 'Accept', JSON.stringify(headers));++ assert.strictEqual(r.body, INDEX, JSON.stringify(headers));++ }++});++++t('robots.txt welcomes the home and keeps the API out, and names the sitemap', async () => {++ const r = await raw(h.base, '/robots.txt');++ assert.strictEqual(r.status, 200);++ assert.match(r.headers['content-type'], /^text\/plain/);++ assert.ok(r.body.includes('Allow: /'));++ assert.ok(r.body.includes('Disallow: /api/'));++ assert.ok(r.body.includes('Disallow: /realtime/'));++ assert.ok(r.body.includes(`Sitemap: ${SITE}/sitemap.xml`));++});++++t('sitemap.xml lists the product home only', async () => {++ const r = await raw(h.base, '/sitemap.xml');++ assert.strictEqual(r.status, 200);++ assert.match(r.headers['content-type'], /xml/);++ assert.deepStrictEqual([...r.body.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]), [`${SITE}/`]);++});++++t('llms.txt is a plain-text map that starts with the site name', async () => {++ const r = await raw(h.base, '/llms.txt');++ assert.strictEqual(r.status, 200);++ assert.match(r.headers['content-type'], /^text\/plain/);++ 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');++});++++t('deploy/nginx/openvibe.events.conf proxies the product surface and nothing else', () => {++ 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 {']) {++ assert.ok(conf.includes(loc), `${loc} is proxied`);++ }++ 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('return 301 https://openvibe.events$request_uri;'), ':80 and www reach the apex');++});++++t('stop', async () => { await h.stop(); });++++t.run();
@@ -10,6 +10,8 @@ PORT=4300
@@ -10,6 +10,8 @@ PORT=4300
HOST=127.0.0.1
HOST=127.0.0.1
NODE_ENV=development
NODE_ENV=development
#BASE_URL=https://events.openvibe.network
#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.
+#EVENTS_SITE_URL=https://openvibe.events
# Identity — OpenVibe.Network signs service tokens (client_credentials, audience
# Identity — OpenVibe.Network signs service tokens (client_credentials, audience
# openvibe.events) and user JWTs. The public key is fetched from
# openvibe.events) and user JWTs. The public key is fetched from
@@ -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://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.
**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)
+**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.
**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).
@@ -346,7 +346,9 @@ fast-forward `/opt/openvibe.events`, install on a lockfile change, restart, wait
@@ -346,7 +346,9 @@ 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 `events.openvibe.network` from
release migrates it at boot. nginx serves `events.openvibe.network` from
-[deploy/nginx/events.openvibe.network.conf](deploy/nginx/events.openvibe.network.conf). ovhost treats open
+[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
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.
@@ -20,7 +20,7 @@
@@ -20,7 +20,7 @@
"capabilitiesRegistered": true,
"capabilitiesRegistered": true,
"deployed": 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://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",
- "updated": "2026-10-05",
+ "updated": "2026-10-07",
"features": [
"features": [
"durable event store with the canonical envelope",
"durable event store with the canonical envelope",
"subscriptions with signed webhook delivery (v1 and v2), retry, dead letters and replay",
"subscriptions with signed webhook delivery (v1 and v2), retry, dead letters and replay",
new file mode 100644
new file mode 100644
@@ -0,0 +1,115 @@
@@ -0,0 +1,115 @@
+## ═══════════════════════════════════════════════════════════════
+## openvibe.events — Nginx Configuration (REFERENCE)
+##
+## 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.
+##
+## The Node service (port 4300, systemd unit openvibe-events) 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)
+## /metrics host-local scrapers only (http://127.0.0.1:4300/metrics, never proxied)
+## /internal/ host-local service endpoints, refused here
+## ═══════════════════════════════════════════════════════════════
+
+# ── HTTP → HTTPS redirect ──────────────────────────────────
+server {
+ listen 80;
+ listen [::]:80;
+ server_name openvibe.events www.openvibe.events;
+
+ location /.well-known/acme-challenge/ { root /var/www/certbot; }
+ location / { return 301 https://openvibe.events$request_uri; }
+}
+
+# ── www → apex ─────────────────────────────────────────────
+server {
+ listen 443 ssl;
+ listen [::]:443 ssl;
+ http2 on;
+ server_name www.openvibe.events;
+
+ ssl_certificate /etc/letsencrypt/live/openvibe.events/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/openvibe.events/privkey.pem;
+ ssl_protocols TLSv1.2 TLSv1.3;
+
+ return 301 https://openvibe.events$request_uri;
+}
+
+# ── HTTPS server ───────────────────────────────────────────
+server {
+ listen 443 ssl;
+ listen [::]:443 ssl;
+ http2 on;
+ server_name openvibe.events;
+
+ ssl_certificate /etc/letsencrypt/live/openvibe.events/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/openvibe.events/privkey.pem;
+ ssl_protocols TLSv1.2 TLSv1.3;
+
+ access_log /var/log/nginx/openvibe.events.access.log;
+ error_log /var/log/nginx/openvibe.events.error.log;
+
+ 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;
+
+ # 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.
+ location /internal/ { return 404; }
+
+ # ── 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;
+ 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 ^~ /shared/ {
+ 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 = /robots.txt {
+ 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 = /sitemap.xml {
+ 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 = /llms.txt {
+ 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;
+ }
+
+ # ── Everything else is the API host: the JSON API, realtime and health live there ──
+ location / { return 301 https://events.openvibe.network$request_uri; }
+}
@@ -14,7 +14,7 @@
@@ -14,7 +14,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.94.2",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.94.2",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.5.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.10.1",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"devDependencies": {
"devDependencies": {
@@ -89,65 +89,6 @@
@@ -89,65 +89,6 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
"license": "MIT"
},
},
- "node_modules/base64-js": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "optional": true,
- "peer": true
- },
- "node_modules/better-sqlite3": {
- "version": "11.10.0",
- "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
- "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "bindings": "^1.5.0",
- "prebuild-install": "^7.1.1"
- }
- },
- "node_modules/bindings": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
- "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "file-uri-to-path": "1.0.0"
- }
- },
- "node_modules/bl": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
- "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "buffer": "^5.5.0",
- "inherits": "^2.0.4",
- "readable-stream": "^3.4.0"
- }
- },
"node_modules/body-parser": {
"node_modules/body-parser": {
"version": "1.20.8",
"version": "1.20.8",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz",
@@ -172,32 +113,6 @@
@@ -172,32 +113,6 @@
"npm": "1.2.8000 || >= 1.4.16"
"npm": "1.2.8000 || >= 1.4.16"
}
}
},
},
- "node_modules/buffer": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.1.13"
- }
- },
"node_modules/buffer-equal-constant-time": {
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -242,14 +157,6 @@
@@ -242,14 +157,6 @@
"url": "https://github.com/sponsors/ljharb"
"url": "https://github.com/sponsors/ljharb"
}
}
},
},
- "node_modules/chownr": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
- "license": "ISC",
- "optional": true,
- "peer": true
- },
"node_modules/cluster-key-slot": {
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
@@ -304,34 +211,6 @@
@@ -304,34 +211,6 @@
"ms": "2.0.0"
"ms": "2.0.0"
}
}
},
},
- "node_modules/decompress-response": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
- "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "mimic-response": "^3.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/deep-extend": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
- "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=4.0.0"
- }
- },
"node_modules/denque": {
"node_modules/denque": {
"version": "2.1.0",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@@ -360,17 +239,6 @@
@@ -360,17 +239,6 @@
"npm": "1.2.8000 || >= 1.4.16"
"npm": "1.2.8000 || >= 1.4.16"
}
}
},
},
- "node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/dotenv": {
"node_modules/dotenv": {
"version": "16.6.1",
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -421,17 +289,6 @@
@@ -421,17 +289,6 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
- "node_modules/end-of-stream": {
- "version": "1.4.5",
- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
- "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "once": "^1.4.0"
- }
- },
"node_modules/es-define-property": {
"node_modules/es-define-property": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -477,17 +334,6 @@
@@ -477,17 +334,6 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
- "node_modules/expand-template": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
- "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
- "license": "(MIT OR WTFPL)",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/express": {
"node_modules/express": {
"version": "4.22.3",
"version": "4.22.3",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz",
@@ -556,14 +402,6 @@
@@ -556,14 +402,6 @@
],
],
"license": "BSD-3-Clause"
"license": "BSD-3-Clause"
},
},
- "node_modules/file-uri-to-path": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
- "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/finalhandler": {
"node_modules/finalhandler": {
"version": "1.3.2",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
@@ -600,14 +438,6 @@
@@ -600,14 +438,6 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
- "node_modules/fs-constants": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
- "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/function-bind": {
"node_modules/function-bind": {
"version": "1.1.2",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -654,14 +484,6 @@
@@ -654,14 +484,6 @@
"node": ">= 0.4"
"node": ">= 0.4"
}
}
},
},
- "node_modules/github-from-package": {
- "version": "0.0.0",
- "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
- "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/gopd": {
"node_modules/gopd": {
"version": "1.2.0",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -730,42 +552,12 @@
@@ -730,42 +552,12 @@
"node": ">=0.10.0"
"node": ">=0.10.0"
}
}
},
},
- "node_modules/ieee754": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "BSD-3-Clause",
- "optional": true,
- "peer": true
- },
"node_modules/inherits": {
"node_modules/inherits": {
"version": "2.0.4",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
"license": "ISC"
},
},
- "node_modules/ini": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "license": "ISC",
- "optional": true,
- "peer": true
- },
"node_modules/iovalkey": {
"node_modules/iovalkey": {
"version": "0.4.0",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/iovalkey/-/iovalkey-0.4.0.tgz",
"resolved": "https://registry.npmjs.org/iovalkey/-/iovalkey-0.4.0.tgz",
@@ -996,53 +788,12 @@
@@ -996,53 +788,12 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
- "node_modules/mimic-response": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
- "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/minimist": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/mkdirp-classic": {
- "version": "0.5.3",
- "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
- "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/ms": {
"node_modules/ms": {
"version": "2.0.0",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
"license": "MIT"
},
},
- "node_modules/napi-build-utils": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
- "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/negotiator": {
"node_modules/negotiator": {
"version": "0.6.3",
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -1052,20 +803,6 @@
@@ -1052,20 +803,6 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
- "node_modules/node-abi": {
- "version": "3.96.0",
- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz",
- "integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "semver": "^7.3.5"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/object-inspect": {
"node_modules/object-inspect": {
"version": "1.13.4",
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -1090,17 +827,6 @@
@@ -1090,17 +827,6 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
- "node_modules/once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "license": "ISC",
- "optional": true,
- "peer": true,
- "dependencies": {
- "wrappy": "1"
- }
- },
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
"version": "0.94.2",
"version": "0.94.2",
"resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.94.2",
"resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.94.2",
@@ -1147,9 +873,9 @@
@@ -1147,9 +873,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "2.5.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.5.0",
- "integrity": "sha512-fHuUS6SXM7ezatfAyqLm1V8B6puV9YXSnz6aG6XQbWx28qghRCCHeUF3lr4nxiCGdn9SyikYtIyLGL5r1ycp9Q==",
+ "version": "2.10.1",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.10.1",
+ "integrity": "sha512-6fUOw22bSvxg0K33BefAyAkNRkETe9euLNT0CHs2mLIvzYR1q38KiHRUQIWj8FESbVANF0pXMojhBz5TeqpMtg==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -1301,35 +1027,6 @@
@@ -1301,35 +1027,6 @@
"node": ">=0.10.0"
"node": ">=0.10.0"
}
}
},
},
- "node_modules/prebuild-install": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
- "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
- "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "detect-libc": "^2.0.0",
- "expand-template": "^2.0.3",
- "github-from-package": "0.0.0",
- "minimist": "^1.2.3",
- "mkdirp-classic": "^0.5.3",
- "napi-build-utils": "^2.0.0",
- "node-abi": "^3.3.0",
- "pump": "^3.0.0",
- "rc": "^1.2.7",
- "simple-get": "^4.0.0",
- "tar-fs": "^2.0.0",
- "tunnel-agent": "^0.6.0"
- },
- "bin": {
- "prebuild-install": "bin.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/proxy-addr": {
"node_modules/proxy-addr": {
"version": "2.0.8",
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
@@ -1347,18 +1044,6 @@
@@ -1347,18 +1044,6 @@
"url": "https://opencollective.com/express"
"url": "https://opencollective.com/express"
}
}
},
},
- "node_modules/pump": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
- "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "end-of-stream": "^1.1.0",
- "once": "^1.3.1"
- }
- },
"node_modules/qs": {
"node_modules/qs": {
"version": "6.16.0",
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
@@ -1399,39 +1084,6 @@
@@ -1399,39 +1084,6 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
- "node_modules/rc": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
- "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
- "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
- "optional": true,
- "peer": true,
- "dependencies": {
- "deep-extend": "^0.6.0",
- "ini": "~1.3.0",
- "minimist": "^1.2.0",
- "strip-json-comments": "~2.0.1"
- },
- "bin": {
- "rc": "cli.js"
- }
- },
- "node_modules/readable-stream": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/redis-errors": {
"node_modules/redis-errors": {
"version": "1.2.0",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
@@ -1623,55 +1275,6 @@
@@ -1623,55 +1275,6 @@
"url": "https://github.com/sponsors/ljharb"
"url": "https://github.com/sponsors/ljharb"
}
}
},
},
- "node_modules/simple-concat": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
- "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "optional": true,
- "peer": true
- },
- "node_modules/simple-get": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
- "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "decompress-response": "^6.0.0",
- "once": "^1.3.1",
- "simple-concat": "^1.0.0"
- }
- },
"node_modules/split2": {
"node_modules/split2": {
"version": "4.2.0",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@@ -1696,60 +1299,6 @@
@@ -1696,60 +1299,6 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
- "node_modules/string_decoder": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
- "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "safe-buffer": "~5.2.0"
- }
- },
- "node_modules/strip-json-comments": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
- "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/tar-fs": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
- "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "chownr": "^1.1.1",
- "mkdirp-classic": "^0.5.2",
- "pump": "^3.0.0",
- "tar-stream": "^2.1.4"
- }
- },
- "node_modules/tar-stream": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
- "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "bl": "^4.0.3",
- "end-of-stream": "^1.4.1",
- "fs-constants": "^1.0.0",
- "inherits": "^2.0.3",
- "readable-stream": "^3.1.1"
- },
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/toidentifier": {
"node_modules/toidentifier": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -1759,20 +1308,6 @@
@@ -1759,20 +1308,6 @@
"node": ">=0.6"
"node": ">=0.6"
}
}
},
},
- "node_modules/tunnel-agent": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
- "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "safe-buffer": "^5.0.1"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/type-is": {
"node_modules/type-is": {
"version": "1.6.18",
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -1795,14 +1330,6 @@
@@ -1795,14 +1330,6 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
- "node_modules/util-deprecate": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/utils-merge": {
"node_modules/utils-merge": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -1821,14 +1348,6 @@
@@ -1821,14 +1348,6 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
- "node_modules/wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "license": "ISC",
- "optional": true,
- "peer": true
- },
"node_modules/xtend": {
"node_modules/xtend": {
"version": "4.0.2",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -26,7 +26,7 @@
@@ -26,7 +26,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.94.2",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.94.2",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.5.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.10.1",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"engines": {
"engines": {
@@ -13,6 +13,9 @@ const { policiesRouter } = require('./api/policies');
@@ -13,6 +13,9 @@ const { policiesRouter } = require('./api/policies');
const { placementRouter } = require('./api/placement');
const { placementRouter } = require('./api/placement');
const { mountLimits } = require('./limits');
const { mountLimits } = require('./limits');
const { createLimits } = require('./actor-limits');
const { createLimits } = require('./actor-limits');
+const { renderHome, HOME_CSP } = require('./home');
+const { createDiscoveryRoutes } = require('./discovery');
+const ovServe = require('openvibe-shared/serve');
const pkg = require('../package.json');
const pkg = require('../package.json');
function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLookup, clock, log = console, valkey = null, policies = null, planner = null, carriers = null }) {
function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLookup, clock, log = console, valkey = null, policies = null, planner = null, carriers = null }) {
@@ -33,6 +36,11 @@ function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLo
@@ -33,6 +36,11 @@ function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLo
next();
next();
});
});
+ // The product home's own browser files (the OpenVibe Frame, openvibe-shared/serve) under
+ // content-addressed /shared/* URLs, from this repo's pinned openvibe-shared. Mounted before the
+ // routers and outside the per-actor limiter: it is static and unauthenticated.
+ app.use('/shared', ovServe.handler());
+
// Realtime first: it answers CORS preflights and must not go through the JSON body parser.
// Realtime first: it answers CORS preflights and must not go through the JSON body parser.
app.options('/realtime/stream', realtime.cors);
app.options('/realtime/stream', realtime.cors);
app.get('/realtime/stream', realtime.cors, realtime.handler);
app.get('/realtime/stream', realtime.cors, realtime.handler);
@@ -83,7 +91,22 @@ function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLo
@@ -83,7 +91,22 @@ function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLo
if (policies) app.use(policiesRouter({ policies, auth, limits }));
if (policies) app.use(policiesRouter({ policies, auth, limits }));
if (planner) app.use(placementRouter({ planner, policies, auth, limits, store }));
if (planner) app.use(placementRouter({ planner, policies, auth, limits, store }));
- app.get('/', (_req, res) => {
+ // Discovery for the product domain (openvibe.events): robots.txt, sitemap.xml, llms.txt.
+ app.use(createDiscoveryRoutes({ config }));
+
+ // GET / on the product domain: the home page for a browser (server/home.js, openvibe.events), the
+ // API index for curl and API clients (`*/*`, no Accept, or any non-HTML Accept). Vary: Accept so a
+ // shared cache never serves one to the other. The home carries its own CSP (HOME_CSP); the only
+ // global security header is X-Content-Type-Options above, kept on every route.
+ app.get('/', (req, res) => {
+ res.vary('Accept');
+ if (req.accepts(['text/plain', 'text/html']) === 'text/html') {
+ return res.type('html')
+ .set('Content-Security-Policy', HOME_CSP)
+ // private: Cloudflare caches by URL and ignores Vary: Accept, so a shared copy could reach an API client.
+ .set('Cache-Control', 'private, max-age=300')
+ .send(renderHome({ siteUrl: config.siteUrl, apiUrl: config.baseUrl }));
+ }
res.type('text/plain').send([
res.type('text/plain').send([
'OpenVibe.Events: durable events, subscriptions, signed delivery, dead letters and replay.',
'OpenVibe.Events: durable events, subscriptions, signed delivery, dead letters and replay.',
'',
'',
@@ -73,12 +73,16 @@ function load(env = process.env) {
@@ -73,12 +73,16 @@ function load(env = process.env) {
const port = int(env.PORT, 4300);
const port = int(env.PORT, 4300);
const maxInflight = int(env.EVENTS_MAX_INFLIGHT, 20);
const maxInflight = int(env.EVENTS_MAX_INFLIGHT, 20);
const prefixes = sourcePrefixes(env);
const prefixes = sourcePrefixes(env);
+ const baseUrl = (env.BASE_URL || (isProduction ? 'https://events.openvibe.network' : `http://localhost:${port}`)).replace(/\/$/, '');
return {
return {
port,
port,
host: env.HOST || '127.0.0.1',
host: env.HOST || '127.0.0.1',
nodeEnv,
nodeEnv,
isProduction,
isProduction,
- baseUrl: (env.BASE_URL || (isProduction ? 'https://events.openvibe.network' : `http://localhost:${port}`)).replace(/\/$/, ''),
+ 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(/\/$/, ''),
// Identity: OpenVibe.Network issues service tokens and user JWTs (RS256).
// Identity: OpenVibe.Network issues service tokens and user JWTs (RS256).
networkUrl: (env.OV_NETWORK_URL || 'https://openvibe.network').replace(/\/$/, ''),
networkUrl: (env.OV_NETWORK_URL || 'https://openvibe.network').replace(/\/$/, ''),
new file mode 100644
new file mode 100644
@@ -0,0 +1,71 @@
@@ -0,0 +1,71 @@
+'use strict';
+/**
+ * Crawl and machine-readability artifacts for openvibe.events, the product domain, built from
+ * openvibe-shared/seo the same way every other OpenVibe site builds them:
+ *
+ * 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 /llms.txt what OpenVibe.Events does today, in plain language: the README's Purpose,
+ * Publishing, Subscriptions and delivery, Developer apps and Realtime sections
+ *
+ * Read-only and viewer-independent: built from the config (the product site URL and the API origin).
+ */
+const express = require('express');
+const cache = require('openvibe-shared/cache-policy');
+const seo = require('openvibe-shared/seo');
+const { DESCRIPTION } = require('./home');
+
+const SOURCE = 'https://github.com/OpenVibers/OpenVibe.Events';
+const GUIDE = `${SOURCE}#readme`;
+
+/** The README's Purpose, Publishing, Subscriptions and delivery, Developer apps and Realtime in plain sentences. */
+function details({ siteUrl, apiUrl }) {
+ return [
+ 'OpenVibe.Events is the durable event backbone of the OpenVibe network. Every authoritative service writes its domain mutation and its outbox event in one transaction, a relay publishes them here, consumers keep inbox/idempotency receipts, and browsers get authorized, resumable projections over the realtime stream; an event exists if and only if the change it describes committed.',
+ '',
+ `Publishing: POST ${apiUrl}/api/v1/events takes one event envelope or up to a hundred at a time, stored atomically and given one global order. A service may only publish the event types its own namespace owns, a repeat of an event id is answered 200 with duplicate: true and is never stored twice, and a producer can redact what it published: the named events become tombstones that every read path serves from then on.`,
+ '',
+ `Subscriptions and delivery: a subscription is a topic pattern such as media.vod.* and an endpoint (http on 127.0.0.1 or an OpenVibe host). Each delivery is an HMAC-signed POST (X-OpenVibe-Signature-V2, over the timestamp and the raw body, inside a five-minute window) and answers 2xx once the event is safely stored; anything else is retried with backoff and, after the last attempt, waits in a dead-letter queue that operators replay. Consumers can also pull in order instead, with an opaque cursor that crosses the retention tiers without a gap.`,
+ '',
+ 'Developer apps: an app signs in through OpenVibe.Network (client credentials, audience openvibe.events) and publishes, reads and subscribes on the same routes under the events.app.* capabilities, scoped to its own project and environment. Sandbox events never reach production, app webhook endpoints must be public HTTPS addresses, and per-project quotas are enforced here.',
+ '',
+ `Realtime: browsers follow topics over server-sent events at ${apiUrl}/realtime/stream with a one-use ticket from OpenVibe.Network; a reconnect resumes from its last cursor, or is told about the gap when the position is too old to replay. Subject-visible events reach only the person they are about, and internal events never reach a browser.`,
+ '',
+ `The guide is ${GUIDE}. The API and the realtime gateway are at ${apiUrl}; the product home is ${siteUrl}/.`,
+ ].join('\n');
+}
+
+function createDiscoveryRoutes({ config }) {
+ const router = express.Router();
+ const abs = (p) => seo.absolute(p, config.siteUrl);
+
+ router.get('/robots.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders())
+ .send(seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: ['/api/', '/realtime/'] })));
+
+ router.get('/sitemap.xml', (_req, res) => res.type('application/xml').set('Cache-Control', cache.htmlHeaders())
+ .send(seo.sitemapXml([{ loc: abs('/') }])));
+
+ router.get('/llms.txt', (_req, res) => res.type('text/plain').set('Cache-Control', cache.htmlHeaders())
+ .send(seo.llmsTxt({
+ name: 'OpenVibe.Events',
+ summary: DESCRIPTION,
+ details: details({ siteUrl: config.siteUrl, apiUrl: config.baseUrl }),
+ sections: [
+ { title: 'Start here', links: [
+ { title: 'OpenVibe.Events home', url: abs('/') },
+ { title: 'Guide', url: GUIDE, note: 'the README: publishing, subscriptions, developer apps and realtime' },
+ { title: 'Source code', url: SOURCE },
+ ] },
+ { title: 'Machine-readable', links: [
+ { title: 'Sitemap', url: abs('/sitemap.xml') },
+ { title: 'robots.txt', url: abs('/robots.txt') },
+ { title: 'API and realtime', url: config.baseUrl, note: 'the JSON API and the realtime gateway' },
+ ] },
+ ],
+ })));
+
+ return router;
+}
+
+module.exports = { createDiscoveryRoutes };
new file mode 100644
new file mode 100644
@@ -0,0 +1,121 @@
@@ -0,0 +1,121 @@
+'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
+ * for developers. Every claim restates the README (Publishing, Subscriptions and delivery, Developer apps,
+ * Realtime) and the code; nothing here reads data.
+ *
+ * renderHome({ siteUrl, apiUrl }) the whole document (siteUrl: the canonical product origin; apiUrl: the API origin)
+ * HOME_CSP its Content-Security-Policy (the Frame's scripts and calls to the Network)
+ */
+const shell = require('openvibe-shared/shell');
+const showcase = require('openvibe-shared/showcase');
+const ovServe = require('openvibe-shared/serve');
+const appIcon = require('openvibe-shared/app-icon');
+
+const SITE_NAME = 'OpenVibe.Events';
+const NETWORK_URL = 'https://openvibe.network';
+const SOURCE = 'https://github.com/OpenVibers/OpenVibe.Events';
+const DESCRIPTION = 'Durable events for the OpenVibe network: publish once, and every subscriber gets it signed, retried and replayable. '
+ + 'Webhooks, pull with a cursor, dead letters, replay and realtime streams for browsers.';
+
+const HOME_CSP = [
+ "default-src 'self'",
+ "script-src 'self' 'unsafe-inline' https://openvibe.network",
+ "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",
+ "frame-src 'self' https://openvibe.network",
+ "frame-ancestors 'none'",
+ "object-src 'none'",
+ "base-uri 'self'",
+ "form-action 'self' https://openvibe.network",
+].join('; ');
+
+function sections({ apiUrl }) {
+ const publish = [
+ `curl -X POST ${apiUrl}/api/v1/events \\`,
+ ' -H "Authorization: Bearer $TOKEN" -H \'Content-Type: application/json\' \\',
+ ' -d \'{ "event_id": "evt_01JAB2C3D4E5F6G7H8J9K0MNPQ", "event_type": "app.<project_key>.order.created",',
+ ' "version": 1, "source": "app-<app id>", "actor": { "type": "app", "id": "app_<ULID>" },',
+ ' "timestamp": "2026-10-07T12:00:00Z", "subject": { "type": "order", "id": "42" },',
+ ' "payload": { "total": 3 } }\'',
+ ].join('\n');
+ const subscribe = [
+ `curl -X POST ${apiUrl}/api/v1/subscriptions \\`,
+ ' -H "Authorization: Bearer $TOKEN" -H \'Content-Type: application/json\' \\',
+ ' -d \'{ "topic_pattern": "app.<project_key>.*", "endpoint": "https://hooks.example.com/openvibe" }\'',
+ '# → { "id": "sub_…", "secret": "whsec_…" } the secret is shown once',
+ ].join('\n');
+ const pull = [
+ `curl "${apiUrl}/api/v1/events?topic=app.<project_key>.*&limit=100" -H "Authorization: Bearer $TOKEN"`,
+ '# then the same with &after=<next_cursor> from the previous page',
+ ].join('\n');
+ return showcase.hero({
+ eyebrow: 'OpenVibe.Events · alpha',
+ title: 'Events your services', accent: 'can count on.',
+ 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.',
+ }) + showcase.features({
+ title: 'What you get',
+ items: [
+ { icon: 'ov:upload', title: 'Publish once', text: 'One event or up to a hundred at a time, stored atomically and numbered in order. Sending the same event id twice never stores it twice.' },
+ { icon: 'ov:bell', title: 'Signed webhooks', text: 'Subscribe with a topic pattern such as media.vod.*. Every delivery is HMAC-signed with its timestamp and retried with backoff.' },
+ { icon: 'ov:history', title: 'Dead letters and replay', text: 'Deliveries that keep failing wait in a dead-letter queue. Replay them, or catch a new subscription up from a point in its history.' },
+ { icon: 'ov:download', title: 'Pull with a cursor', text: 'Read events in order with an opaque cursor when you would rather pull than receive webhooks.' },
+ { icon: 'ov:live', title: 'Realtime for browsers', text: 'Server-sent events, authorized per topic with a one-use ticket from OpenVibe.Network, and resumable after a reconnect.' },
+ { icon: 'ov:code', title: 'Developer apps', text: 'An app publishes under its own project\'s names and keeps sandbox and production apart: sandbox events never reach production.' },
+ ],
+ }) + showcase.code({
+ id: 'examples',
+ title: 'Publish, subscribe, pull',
+ lede: 'With an app token from OpenVibe.Network (client credentials, audience openvibe.events).',
+ samples: [
+ { label: 'Publish', lang: 'bash', code: publish },
+ { label: 'Subscribe', lang: 'bash', code: subscribe },
+ { label: 'Pull', lang: 'bash', code: pull },
+ ],
+ }) + showcase.steps({
+ title: 'Receive a webhook safely',
+ items: [
+ { title: 'Keep the secret', text: 'The subscription\'s secret is shown once, when you create it or rotate it.' },
+ { title: 'Check the signature', text: 'Verify X-OpenVibe-Signature-V2 over the timestamp and the raw body, and refuse anything older than five minutes.' },
+ { title: 'Answer quickly', text: 'Reply 2xx once the event is safely stored; anything else is retried, then dead-lettered.' },
+ { title: 'Expect repeats', text: 'A retry can bring the same event again: keep its event id and handle it once.' },
+ ],
+ });
+}
+
+function renderHome({ siteUrl, apiUrl }) {
+ const nav = {
+ service: 'events',
+ apiBase: NETWORK_URL,
+ links: [{ label: 'Guide', href: `${SOURCE}#readme` }],
+ history: { type: 'page', title: SITE_NAME },
+ };
+ const footer = { service: 'events', variant: 'full', mount: '#ov-footer', brandName: SITE_NAME };
+ return shell.page({
+ name: SITE_NAME, service: 'events', lang: 'en',
+ title: `${SITE_NAME}: durable events, signed webhooks and replay`,
+ siteName: SITE_NAME,
+ description: DESCRIPTION,
+ summary: DESCRIPTION,
+ canonical: `${siteUrl}/`,
+ robots: 'index, follow',
+ navbar: nav, footer, home: '/', navLinks: [{ label: 'Guide', href: `${SOURCE}#readme` }],
+ head: [
+ appIcon.headTags({ site: 'events' }),
+ `<link rel="stylesheet" href="${ovServe.url(showcase.STYLESHEET)}">`,
+ ].join('\n'),
+ body: `<div id="navbar-mount"></div>
+<main id="main" class="page">
+${sections({ apiUrl })}
+</main>`,
+ });
+}
+
+module.exports = { renderHome, HOME_CSP, DESCRIPTION, sections };
new file mode 100644
new file mode 100644
@@ -0,0 +1,126 @@
@@ -0,0 +1,126 @@
+'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.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const nodeHttp = require('http');
+const { boot, suite } = require('./helpers');
+const { HOME_CSP } = require('../server/home');
+
+const SITE = 'https://openvibe.events';
+
+// GET / without an Accept a browser would send: the API index, byte for byte, as it was before the home.
+const INDEX = [
+ 'OpenVibe.Events: durable events, subscriptions, signed delivery, dead letters and replay.',
+ '',
+ 'POST /api/v1/events publish (service token, events.event.publish; app token, events.app.publish)',
+ 'GET /api/v1/events pull with a cursor (events.event.read; app token, events.app.read)',
+ ' /api/v1/subscriptions webhook subscriptions (events.subscription.manage; app token, events.app.subscribe)',
+ ' /api/v1/deliveries DLQ inspect and replay (events.delivery.admin)',
+ 'GET /realtime/stream?topics=... server-sent events for browsers',
+ 'GET /api/health, /api/ready, /release.json, /limits.json',
+ '',
+ 'Source: https://github.com/OpenVibers/OpenVibe.Events',
+ '',
+].join('\n');
+
+const t = suite('home');
+let h;
+
+/** A raw GET with exactly the headers given: node:http sends no Accept unless the caller sets one. */
+function raw(base, p, headers = {}) {
+ return new Promise((resolve, reject) => {
+ const url = new URL(base + p);
+ nodeHttp.get({ hostname: url.hostname, port: url.port, path: url.pathname + url.search, headers }, (res) => {
+ let body = '';
+ res.setEncoding('utf8');
+ res.on('data', (c) => { body += c; });
+ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));
+ }).on('error', reject);
+ });
+}
+
+t('boot with the product domain configured', async () => { h = await boot({ env: { EVENTS_SITE_URL: SITE } }); });
+
+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' });
+ assert.strictEqual(r.status, 200);
+ assert.match(r.headers['content-type'], /^text\/html/);
+ assert.strictEqual(r.headers['content-security-policy'], HOME_CSP);
+ assert.strictEqual(r.headers['cache-control'], 'private, max-age=300');
+ assert.strictEqual(r.headers.vary, 'Accept');
+ 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');
+});
+
+t('the home links the pinned Frame stylesheet, and /shared serves it', async () => {
+ const r = await raw(h.base, '/', { Accept: 'text/html' });
+ const href = /<link rel="stylesheet" href="([^"]+)"/.exec(r.body);
+ assert.ok(href && href[1].startsWith('/shared/showcase.css?v='), `showcase.css link, got ${href && href[1]}`);
+ const sheet = await raw(h.base, href[1]);
+ assert.strictEqual(sheet.status, 200);
+ assert.match(sheet.headers['content-type'], /^text\/css/);
+ assert.ok(sheet.body.includes('.ov-'), 'the showcase stylesheet');
+});
+
+t('GET / for curl and API clients: the text/plain index, byte for byte', async () => {
+ // `*/*`, no Accept at all, and a JSON client: all three get the API index.
+ for (const headers of [{ Accept: '*/*' }, {}, { Accept: 'application/json' }]) {
+ const r = await raw(h.base, '/', headers);
+ assert.strictEqual(r.status, 200, JSON.stringify(headers));
+ assert.match(r.headers['content-type'], /^text\/plain/, JSON.stringify(headers));
+ assert.strictEqual(r.headers.vary, 'Accept', JSON.stringify(headers));
+ assert.strictEqual(r.body, INDEX, JSON.stringify(headers));
+ }
+});
+
+t('robots.txt welcomes the home and keeps the API out, and names the sitemap', async () => {
+ const r = await raw(h.base, '/robots.txt');
+ assert.strictEqual(r.status, 200);
+ assert.match(r.headers['content-type'], /^text\/plain/);
+ assert.ok(r.body.includes('Allow: /'));
+ assert.ok(r.body.includes('Disallow: /api/'));
+ assert.ok(r.body.includes('Disallow: /realtime/'));
+ assert.ok(r.body.includes(`Sitemap: ${SITE}/sitemap.xml`));
+});
+
+t('sitemap.xml lists the product home only', async () => {
+ const r = await raw(h.base, '/sitemap.xml');
+ assert.strictEqual(r.status, 200);
+ assert.match(r.headers['content-type'], /xml/);
+ assert.deepStrictEqual([...r.body.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]), [`${SITE}/`]);
+});
+
+t('llms.txt is a plain-text map that starts with the site name', async () => {
+ const r = await raw(h.base, '/llms.txt');
+ assert.strictEqual(r.status, 200);
+ assert.match(r.headers['content-type'], /^text\/plain/);
+ 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');
+});
+
+t('deploy/nginx/openvibe.events.conf proxies the product surface and nothing else', () => {
+ 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 {']) {
+ assert.ok(conf.includes(loc), `${loc} is proxied`);
+ }
+ 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('return 301 https://openvibe.events$request_uri;'), ':80 and www reach the apex');
+});
+
+t('stop', async () => { await h.stop(); });
+
+t.run();