openvibers/openvibe.ai Commits
AI: the public home at ai.openvibe.services, developer-app routes on the public vhost, contracts 0.109.0 (#23)
Changes
@@@ -1,6 +1,6 @@# ═══════════════════════════════════════════════════════════════--# ai.openvibe.network — OpenVibe.AI Environment Configuration--# Target host: ai.openvibe.services (the public home; BASE_URL still defaults to ai.openvibe.network here).++# ai.openvibe.services — OpenVibe.AI Environment Configuration++# The public home and API origin (ai.openvibe.network answers 301 to it). BASE_URL defaults to it in production.# Copy to .env and fill in values. In production this lives at /etc/openvibe/ai.env (0600).# Secrets are read by NAME only (providers store 'env:NAME' references); nothing logs or returns them.# ═══════════════════════════════════════════════════════════════@@@ -35,8 +35,7 @@ OV_NETWORK_INTERNAL_URL=http://127.0.0.1:4000# ── Operator console (/console; README "Operator console") ──# Network SSO as OAuth client `ai` (the client AI's service tokens use). The Network must list--# ${BASE_URL}/auth/callback (https://ai.openvibe.network/auth/callback) as a redirect URI of client `ai`.--# Target host: also list https://ai.openvibe.services/auth/callback when BASE_URL moves there.++# ${BASE_URL}/auth/callback (https://ai.openvibe.services/auth/callback) as a redirect URI of client `ai`.# OV_OAUTH_CLIENT_SECRET is also what the events relay and the user modules authenticate with.#OV_OAUTH_CLIENT_ID=ai#OV_OAUTH_CLIENT_SECRET=@@@ -2,11 +2,11 @@> Providers, models, routing, prompt templates, workflows, runs, citations, cache and quotas for every product.--**Status:** alpha (roadmap Wave 13). Deployed on the host since 2026-09-23 on `127.0.0.1:4700`--(loopback only) and in production use: OpenVibe.Live runs with `AI_SERVICE=remote` and Network's--footer copy runs `network.site_copy` here. It is not a public product: the domain keeps its placeholder--page until the launch rule below is met.--**Domain:** `ai.openvibe.network` (target host `ai.openvibe.services`) · **Port:** 4700 · **Unit:** `openvibe-ai.service`++**Status:** alpha (roadmap Wave 13). Deployed on the host since 2026-09-23 on `127.0.0.1:4700` and in++production use: OpenVibe.Live runs with `AI_SERVICE=remote` and Network's footer copy runs++`network.site_copy` here. Public since 2026-10-07 at [ai.openvibe.services](https://ai.openvibe.services):++the home (`server/home.js`), `/stats` and the developer-app API (`ai.app.run`).++**Domain:** `ai.openvibe.services` (`ai.openvibe.network` answers 301 to it) · **Port:** 4700 · **Unit:** `openvibe-ai.service`**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §12.1, §12.13, §15.14, §33, §34.**License:** AGPL-3.0 (same as every OpenVibe service).@@@ -51,9 +51,8 @@ owns publication truth: Wiki, Blog, News, Live, … decide what to publish.Implemented here (the service manifest's `capabilities`, audience `openvibe.ai`; routes under[API](#api)): `ai.run.create`, `ai.run.read`, `ai.workflow.manage`, `ai.provider.manage`,`ai.usage.read`, `ai.credential.manage` (`/api/v1/credentials/:subject`),--`ai.quota.attribution.manage` (per-streamer quotas) and `ai.app.run` (developer apps, ADR-014; a--public capability proposed in `docs/capabilities-proposal/` until openvibe-contracts registers it —--`server/auth.js` enforces it through its PROPOSED fallback until then). A service token's `ns`++`ai.quota.attribution.manage` (per-streamer quotas) and `ai.app.run` (developer apps, ADR-014; public,++registered since openvibe-contracts 0.109.0). A service token's `ns`claim limits which workflow namespaces it may run; an app token is fixed to its project's.Called elsewhere, as the service principal `ai`: `events.event.publish` (Events, the outbox relay)@@@ -145,10 +144,10 @@ and its rollback lever (`AI_NS_REQUIRED`) were retired on 2026-09-28 (shims C-22Roadmap Wave 20, ADR-014. A developer app gets a token from OpenVibe.Network (`POST /oauth/token`,`grant_type=client_credentials` with its `app_<ULID>` client, `audience=openvibe.ai`): `subapp:app_<ULID>`, `project_id prj_<ULID>`, `env sandbox|production`, ns `[project_id,--app.<project_id>.*]`, five minutes. AI accepts one capability for it, `ai.app.run` (public;--proposed in `docs/capabilities-proposal/ai.app.run.json` until openvibe-contracts registers it —--until then `server/auth.js` enforces it through its PROPOSED fallback, as Events did before--`events.app.*` shipped):++app.<project_id>.*]`, five minutes. AI accepts one capability for it, `ai.app.run` (public). A++sandbox app holds it without asking (Network's default sandbox allowance); a production app holds it++once OpenVibe staff add it to the project's allowance. Create the project and its app at++[openvibe.codes/projects](https://openvibe.codes/projects):| Route | What ||---|---|@@@ -178,7 +177,7 @@ project's runs and never uses a person's BYO provider key: the `byok` provider poffered to an app run, and `credential` on its request is refused.```bash--AI=https://ai.openvibe.network++AI=https://ai.openvibe.servicesAPP=app_01JAB… # the app's client id; SECRET is its client secretTOKEN=$(curl -s -X POST https://openvibe.network/oauth/token \-d grant_type=client_credentials -d client_id=$APP -d client_secret=$SECRET \@@@ -194,7 +193,7 @@ AI takes one on these routes alone, and every other route refuses `env: sandbox`## Operator console--`/console` on ai.openvibe.network (roadmap WS-O task 4) is a server-rendered staff console: no++`/console` on ai.openvibe.services (roadmap WS-O task 4) is a server-rendered staff console: noJavaScript, one inline stylesheet allowed by its CSP hash, `noindex` (meta and `X-Robots-Tag`),`no-store`, disallowed in `robots.txt`, in no sitemap. It does not load the OpenVibe Frame (the Frameneeds scripts and inline styles), like Billing's staff console. Code: `server/console/`.@@@ -440,19 +439,15 @@ runs as `openvibe-llm.service` on 127.0.0.1:8090 ([Local model](#local-model)).Rollback: ovhost puts the previous sha back by itself when `/api/ready` does not answer 2xx after therestart; afterwards `sudo ovhost rollback ai --to <sha>`. Migrations only add tables and columns.--Deployed: `/opt/openvibe.ai`, env `/etc/openvibe/ai.env` (0600), unit `deploy/systemd/openvibe-ai.service`--(`StateDirectory=openvibe-ai`; database `ov_ai` on the host's data role), principal `ai`. The nginx vhost--`deploy/nginx/ai.openvibe.network.conf` (health/ready, `/`, `/robots.txt`, the operator console--`/console` and its sign-in `/auth/` public; the API is host-local) is not installed:--`ai.openvibe.network` still serves the Sites placeholder. `deploy/nginx/ai.openvibe.services.conf`--mirrors that vhost for `ai.openvibe.services` (the same proxied locations to the same upstream,--`/metrics` and the API still blocked, on the `*.openvibe.services` certificate the host already holds) and--replaces the Sites placeholder for `ai.openvibe.services` when installed; it is not installed or live yet. The console also needs--`AI_CONSOLE_SESSION_SECRET` (32+ random characters) and `OV_OAUTH_CLIENT_SECRET` in the env file--(without them it answers 503 in production), and the Network must list--`https://ai.openvibe.network/auth/callback` among client `ai`'s redirect URIs (and--`https://ai.openvibe.services/auth/callback` once that vhost serves the console, since the callback is built from--the base URL).++Deployed: `/opt/openvibe.ai`, env `/etc/openvibe/ai.env` (0600; `BASE_URL=https://ai.openvibe.services`), unit++`deploy/systemd/openvibe-ai.service` (`StateDirectory=openvibe-ai`; database `ov_ai` on the host's data role),++principal `ai`. The vhost `deploy/nginx/ai.openvibe.services.conf` (on the `*.openvibe.services` certificate)++serves the home, `/stats`, `/shared/*`, `/robots.txt`, `/sitemap.xml`, `/llms.txt`, health/ready, the operator++console `/console` with its sign-in `/auth/`, and exactly the developer-app routes (the six operations by POST,++`/api/v1/runs/:id` by GET, rate-limited per address); every other `/api/` path and `/metrics` stay host-local.++`deploy/nginx/ai.openvibe.network.conf` answers 301 to the same path on ai.openvibe.services. The console needs++`AI_CONSOLE_SESSION_SECRET` (32+ random characters) and `OV_OAUTH_CLIENT_SECRET` in the env file (without them it++answers 503 in production); Network client `ai` lists `https://ai.openvibe.services/auth/callback`.## Not done yet@@@ -460,9 +455,8 @@ the base URL).themselves; moving the remaining passthrough prompts into AI templates (most `live.*` workflows areversioned templates since 2026-09-27);removing Network's fallback to Live's `/internal/ai/site-copy`; a public server-rendered status page.--- The operator console is built but not reachable yet: it needs the vhost below installed (which-- replaces the Sites placeholder), `https://ai.openvibe.network/auth/callback` listed as a redirect URI-- of Network OAuth client `ai`, and `AI_CONSOLE_SESSION_SECRET` in `/etc/openvibe/ai.env`.++- Metered models for developer apps: production app runs use free and local capacity until a tier budget++ governs their project (`AI_GOVERN_TIERS` is off in production) and OpenVibe.Billing charges project subjects.- Import holds from the 2026-09-23 run: a streamer's own provider key stays in Live, and 3,021 Livetranslations cannot become cache entries (they have no source text) and stay in Live.- Text routes now run a capability pool (`shared` plus a local model when one is configured) instead@@@ -473,23 +467,15 @@ the base URL).- The host has a backup of `ov_ai` but no restore drill has run for it; `/metrics` is built but notdeployed yet.--## Launch rule++## Launch--This repository does not make the product real, and the domain keeps its placeholder page on--[OpenVibers/OpenVibe.Sites](https://github.com/OpenVibers/OpenVibe.Sites) until all of the--following exist here (plan §12.12):----1. an owning runtime with health/readiness endpoints and observability;--2. canonical identity/auth integration (OpenVibe.Network subjects, scoped service principals);--3. server-rendered or static public routes that are useful without JavaScript;--4. real persistence and end-to-end workflows;--5. capability and event registration against `OpenVibe.Contracts`;--6. a migration/seed strategy, a security/threat review, and sitemap/robots/feed behaviour;--7. acceptance tests proving the advertised functionality.----The launch release removes the domain from `OpenVibe.Sites/sites.json`, switches routing and--registers maturity in the ecosystem registry atomically. A placeholder is never counted as an--implemented service.++Launched 2026-10-07 under the launch rule (plan §12.12): the runtime with health, readiness and metrics; Network++subjects and scoped service principals; server-rendered public routes useful without JavaScript (the home,++`/stats`); PostgreSQL persistence and end-to-end workflows; capabilities and events registered in++`OpenVibe.Contracts` (`ai.app.run` since 0.109.0, the service live since 0.110.0); migrations at boot,++robots/sitemap/llms.txt; acceptance tests ([above](#acceptance-guarantees-and-where-they-are-tested), and++`test/home.test.js` for the public routes and vhosts). The same release removed ai.openvibe.services and the++ai.openvibe.network notice from OpenVibe.Sites.---@@@ -2,19 +2,9 @@## ai.openvibe.network — Nginx Configuration (REFERENCE)#### Deployed to /etc/nginx/sites-available/ai.openvibe.network.conf — keep them in sync, and reload--## nginx after changing it. The domain keeps its OpenVibe.Sites placeholder until the launch rule in--## README.md is met; this vhost is what replaces it then.--##--## The Node service (port 4700, systemd unit openvibe-ai) serves:--## /api/health, /api/ready--## /metrics Prometheus text, answered only to direct loopback callers (never proxied)--## /api/v1/* service-to-service API (Network service tokens, audience openvibe.ai). Services on--## this host call 127.0.0.1:4700 directly, so the public vhost refuses it; open the--## location below only when a service on another host needs it (it still needs a token).--## /console the operator console (server-rendered, OpenVibe.Network staff only) and /auth/* its--## Network sign-in (redirect URI https://ai.openvibe.network/auth/callback, OAuth client--## `ai`); both noindex. /robots.txt keeps crawlers out of them.--## /stats public price and latency per provider and model (server-rendered, no sign-in)++## nginx after changing it. OpenVibe.AI's public origin is https://ai.openvibe.services++## (deploy/nginx/ai.openvibe.services.conf); this name answers 301 to the same path there.++## Services on this host never come through here: they call 127.0.0.1:4700 directly.## ═══════════════════════════════════════════════════════════════server {@@@ -23,7 +13,7 @@ server {server_name ai.openvibe.network;location /.well-known/acme-challenge/ { root /var/www/certbot; }-- location / { return 301 https://ai.openvibe.network$request_uri; }++ location / { return 301 https://ai.openvibe.services$request_uri; }}server {@@@ -39,63 +29,7 @@ server {access_log /var/log/nginx/ai.openvibe.network.access.log;error_log /var/log/nginx/ai.openvibe.network.error.log;-- # Inline images arrive as data URLs inside run inputs.-- client_max_body_size 12m;---- 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;-- location = /api/health { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }-- location = /api/ready { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }---- # Metrics are for host-local scrapers only (http://127.0.0.1:4700/metrics).-- location = /metrics { return 404; }---- # ── Service API: host-local only (see header) ───────────-- location /api/ {-- return 403;-- # To expose it (token-guarded) to off-host services instead:-- # proxy_pass http://127.0.0.1:4700;-- # proxy_http_version 1.1;-- # proxy_set_header Host $host;-- # proxy_set_header X-Real-IP $remote_addr; # client address from $remote_addr only (realip),-- # proxy_set_header X-Forwarded-For $remote_addr; # never appended to or passed through-- # proxy_set_header CF-Connecting-IP $remote_addr;-- # proxy_set_header X-Forwarded-Proto $scheme;-- # proxy_read_timeout 90s; # ?wait= holds a request up to 60 s-- }---- # ── Operator console and its Network sign-in (staff only; the app refuses everyone else) ──-- location /console {-- proxy_pass http://127.0.0.1:4700;-- proxy_set_header Host $host;-- proxy_set_header X-Real-IP $remote_addr;-- proxy_set_header X-Forwarded-For $remote_addr;-- proxy_set_header CF-Connecting-IP $remote_addr;-- proxy_set_header X-Forwarded-Proto $scheme;-- }-- location /auth/ {-- proxy_pass http://127.0.0.1:4700;-- proxy_set_header Host $host;-- proxy_set_header X-Real-IP $remote_addr;-- proxy_set_header X-Forwarded-For $remote_addr;-- proxy_set_header CF-Connecting-IP $remote_addr;-- proxy_set_header X-Forwarded-Proto $scheme;-- }-- location = /robots.txt { proxy_pass http://127.0.0.1:4700; 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 = /stats { proxy_pass http://127.0.0.1:4700; 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 = / {-- proxy_pass http://127.0.0.1:4700;-- proxy_set_header Host $host;-- proxy_set_header X-Real-IP $remote_addr;-- proxy_set_header X-Forwarded-For $remote_addr;-- proxy_set_header CF-Connecting-IP $remote_addr;-- }---- location / {-- return 404;-- }++ location / { return 301 https://ai.openvibe.services$request_uri; }}@@@ -4,20 +4,28 @@## Deployed to /etc/nginx/sites-available/ai.openvibe.services.conf — keep them in sync, and reload## nginx after changing it. It uses the *.openvibe.services certificate the host already holds## (/etc/letsencrypt/live/openvibe.services/); installed in place of the OpenVibe.Sites placeholder--## vhost of the same name, it replaces that placeholder.++## vhost of the same name, it replaces that placeholder. ai.openvibe.network (its own vhost) answers++## 301 to here.#### The Node service (port 4700, systemd unit openvibe-ai) serves:++## / the product home for a browser (server/home.js), the text API index for anything else++## /stats public price and latency per provider and model (server-rendered, no sign-in)++## /shared/* the home's OpenVibe Frame files (content-addressed); /robots.txt, /sitemap.xml, /llms.txt## /api/health, /api/ready++## /api/v1/{chat,generate,summarize,classify,extract,embed} (POST) and /api/v1/runs/:id (GET)++## developer apps (ai.app.run, app tokens from OpenVibe.Network): open here, rate-limited++## per address; the app checks the token on every request++## /api/v1/* everything else is service-to-service (first-party service tokens). Services on this++## host call 127.0.0.1:4700 directly, so the public vhost refuses it.## /metrics Prometheus text, answered only to direct loopback callers (never proxied)--## /api/v1/* service-to-service API (Network service tokens, audience openvibe.ai). Services on--## this host call 127.0.0.1:4700 directly, so the public vhost refuses it; open the--## location below only when a service on another host needs it (it still needs a token).## /console the operator console (server-rendered, OpenVibe.Network staff only) and /auth/* its## Network sign-in (redirect URI https://ai.openvibe.services/auth/callback, OAuth client## `ai`); both noindex. /robots.txt keeps crawlers out of them.--## /stats public price and latency per provider and model (server-rendered, no sign-in)## ═══════════════════════════════════════════════════════════════++# Developer-app calls per client address (after realip): a burst for interactive use, then 5 a second.++limit_req_zone $binary_remote_addr zone=ovai_apps:10m rate=5r/s;++server {listen 80;listen [::]:80;@@@ -53,6 +61,33 @@ server {# Metrics are for host-local scrapers only (http://127.0.0.1:4700/metrics).location = /metrics { return 404; }++ # ── Developer apps (ai.app.run): the six direct operations and a run read ──++ location ~ ^/api/v1/(chat|generate|summarize|classify|extract|embed)$ {++ limit_req zone=ovai_apps burst=20 nodelay;++ limit_req_status 429;++ limit_except POST { deny all; }++ proxy_pass http://127.0.0.1:4700;++ proxy_http_version 1.1;++ proxy_set_header Host $host;++ proxy_set_header X-Real-IP $remote_addr; # client address from $remote_addr only (realip),++ proxy_set_header X-Forwarded-For $remote_addr; # never appended to or passed through++ proxy_set_header CF-Connecting-IP $remote_addr;++ proxy_set_header X-Forwarded-Proto $scheme;++ proxy_read_timeout 90s; # ?wait= holds a request up to 60 s++ }++ location ~ ^/api/v1/runs/[A-Za-z0-9_-]+$ {++ limit_req zone=ovai_apps burst=20 nodelay;++ limit_req_status 429;++ limit_except GET { deny all; }++ proxy_pass http://127.0.0.1:4700;++ proxy_http_version 1.1;++ proxy_set_header Host $host;++ proxy_set_header X-Real-IP $remote_addr;++ proxy_set_header X-Forwarded-For $remote_addr;++ proxy_set_header CF-Connecting-IP $remote_addr;++ proxy_set_header X-Forwarded-Proto $scheme;++ }++# ── Service API: host-local only (see header) ───────────location /api/ {return 403;@@@ -87,6 +122,10 @@ server {location = /robots.txt { proxy_pass http://127.0.0.1:4700; 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 = /stats { proxy_pass http://127.0.0.1:4700; 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:4700; 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:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }++ location = /release.json { proxy_pass http://127.0.0.1:4700; 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:4700; 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 = / {proxy_pass http://127.0.0.1:4700;@@@ -8,16 +8,17 @@"status": "alpha","repository": "OpenVibers/OpenVibe.AI","domains": [++ "ai.openvibe.services","ai.openvibe.network"],-- "publicOrigin": "https://ai.openvibe.network",++ "publicOrigin": "https://ai.openvibe.services","health": "/api/health","ready": "/api/ready","internalOrigin": "http://127.0.0.1:4700","exposure": {-- "state": "internal",-- "publicSite": null,-- "note": "called by other services at 127.0.0.1:4700; ai.openvibe.network serves the OpenVibe.Sites placeholder page"++ "state": "live",++ "publicSite": "service",++ "note": "the public home, /stats and the developer-app API at ai.openvibe.services (2026-10-07); ai.openvibe.network answers 301 to it; services on the host call 127.0.0.1:4700"},"capabilities": ["ai.app.run",@@@ -76,14 +77,14 @@],"window": "automatic only while ovhost waits for readiness after the restart (30 s in host.example.json); manual afterwards, as long as no blocker applies","blockers": {-- "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"++ "none": "the schema code only adds (CREATE \u2026 IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"}},"contracts": {"range": ">=0.6.0 <1.0.0"},"leases": {-- "none": "no claims: one process owns ai.db (better-sqlite3, single writer); a run is taken with a compare-and-set on its status (queued → running)"++ "none": "no claims: one process owns ai.db (better-sqlite3, single writer); a run is taken with a compare-and-set on its status (queued \u2192 running)"}},"notes": "Providers, models, routing profiles, versioned prompt templates and workflows, runs, request logs, citations, scoped cache, quotas and audit. Workflow output is a draft/evidence package attributed to workflow + model + run. ai.run.queued|succeeded|failed|cached have payload contracts (v0.30.0, status planned): AI publishes them once its Events client is wired in; it publishes none today. ai.run.create takes ai.run-request@1 and returns ai.run@1."@@@ -13,9 +13,9 @@"dotenv": "^16.4.7","express": "^4.21.2","iovalkey": "^0.4.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.109.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.12.0","pg": "^8.23.0"},"devDependencies": {@@@ -464,65 +464,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",@@@ -547,32 +488,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",@@@ -617,14 +532,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",@@@ -724,34 +631,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",@@@ -840,17 +719,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",@@@ -896,17 +764,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",@@@ -975,14 +832,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",@@@ -1019,14 +868,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",@@@ -1073,14 +914,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",@@@ -1149,42 +982,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",@@@ -1422,53 +1225,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",@@@ -1478,20 +1240,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",@@@ -1516,21 +1264,10 @@"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.86.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",-- "integrity": "sha512-3o3AILEBtQ2pK0KqK7GEAlN3HJjaKUl6vrbzEDiBbhK6Nr2hivgN/5MAmJ7WRkgaDkxNsZETs/fLZC2Q4oNCiA==",++ "version": "0.109.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.109.0",++ "integrity": "sha512-Ks2zLtR9XcN6ggr1APv93Ot8o/U1oAn8fOHNruYTDh1wjL/mKiwq5Lee8mx4rl6eb2A9zY8A45P/6edb3NNxgQ==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -1573,9 +1310,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.12.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.12.0",++ "integrity": "sha512-GQpNFs2h61OKKNLexQwCfSJTLzNQ7fSbwqvrerB4oNdoxUgDvmdD43SELWycrb9DX8lkje4cR3ARznYIINnR8Q==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -1727,35 +1464,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",@@@ -1773,18 +1481,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",@@@ -1825,39 +1521,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",@@@ -2089,55 +1752,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/simple-swizzle": {"version": "0.2.4","resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",@@@ -2172,60 +1786,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",@@@ -2242,20 +1802,6 @@"license": "0BSD","optional": true},-- "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",@@@ -2278,14 +1824,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",@@@ -2304,14 +1842,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",@@@ -25,9 +25,9 @@"dotenv": "^16.4.7","express": "^4.21.2","iovalkey": "^0.4.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.109.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.12.0","pg": "^8.23.0"},"optionalDependencies": {@@@ -13,6 +13,8 @@ const { createOps } = require('./ops');const { consoleRouter } = require('./console');const views = require('./console/views');const cachePolicy = require('openvibe-shared/cache-policy');++const ovServe = require('openvibe-shared/serve');++const home = require('./home');const pkg = require('../package.json');function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys, env = process.env, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, log = console, credentials = null }) {@@@ -63,6 +65,9 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keysres.setHeader('Cache-Control', 'no-store');next();});++ // The product home's browser files (the OpenVibe Frame, openvibe-shared/serve) under content-addressed++ // /shared/* URLs from this repo's pinned openvibe-shared; each answer sets its own asset cache policy.++ app.use('/shared', ovServe.handler());// Inline images arrive as data URLs; the per-run input cap is enforced again in runs.create.app.use('/api', express.json({ limit: Math.ceil(config.runs.maxInputBytes * 1.5), type: ['application/json', 'application/*+json'] }));@@@ -113,12 +118,30 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys} catch (e) { next(e); }});-- // Crawlers: nothing here is for search engines, the console and sign-in least of all (no sitemap either).++ // Discovery for the public home (ai.openvibe.services): the home and the price page are for search engines++ // and assistants; the console, its sign-in and the API are not.app.get('/robots.txt', (_req, res) => {-- res.type('text/plain').send('User-agent: *\nDisallow: /console\nDisallow: /auth/\nDisallow: /api/\n');++ res.type('text/plain').send(`User-agent: *\nAllow: /$\nAllow: /stats\nDisallow: /console\nDisallow: /auth/\nDisallow: /api/\n\nSitemap: ${config.baseUrl}/sitemap.xml\n`);++ });++ app.get('/sitemap.xml', (_req, res) => {++ res.type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${['/', '/stats'].map((p) => ` <url><loc>${config.baseUrl}${p}</loc></url>`).join('\n')}\n</urlset>\n`);++ });++ app.get('/llms.txt', (_req, res) => {++ res.setHeader('Cache-Control', 'public, max-age=3600');++ res.type('text/plain').send(home.llmsTxt({ siteUrl: config.baseUrl }));});-- app.get('/', (_req, res) => {++ // GET /: the home page for a browser (server/home.js), 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 is++ // private because Cloudflare caches by URL and ignores Vary: Accept. The home carries its own CSP.++ 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.HOME_CSP)++ .set('Cache-Control', 'private, max-age=300')++ .send(home.renderHome({ siteUrl: config.baseUrl }));++ }res.type('text/plain').send(['OpenVibe.AI: providers, models, routing, versioned prompt templates and workflows, runs, citations, scoped cache, quotas and audit.','',@@@ -130,7 +153,9 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys'GET /stats price and latency per provider and model (public)','GET /console the operator console (OpenVibe.Network staff sign-in)','',-- 'Callers authenticate with OpenVibe.Network service tokens (audience openvibe.ai).',++ 'Callers authenticate with OpenVibe.Network tokens (audience openvibe.ai): first-party service tokens, or a',++ 'developer app\'s token (ai.app.run) on the six direct operations and GET /api/v1/runs/:id.',++ 'Developer apps: https://openvibe.codes/projects','AI output is a draft/evidence package attributed to a workflow, model and run, never to a person.','','Source: https://github.com/OpenVibers/OpenVibe.AI',@@@ -7,12 +7,8 @@* exponential backoff, and a rotation honoured on an unknown kid. OV_NETWORK_PUBLIC_KEY still pins a* PEM, which skips the fetch entirely.*-- * Capability ids are decided by openvibe-contracts when it knows them (the release named below);-- * until then allows() decides with the contracts' own grant rule (the exact id, or a `family.*`-- * grant), but only for the ids PROPOSED names — a proposal this service enforces ahead of the-- * release that registers it (docs/capabilities-proposal/, the pattern OpenVibe.Events used before-- * events.app.* shipped). ai.app.run (T6 step 10: developer apps, ADR-014) needs exactly that-- * fallback until openvibe-contracts knows the id.++ * Capability ids are decided by openvibe-contracts: an id the pinned release does not know is refused,++ * never silently allowed (ai.app.run, developer apps, is registered since openvibe-contracts 0.109.0).** Namespaces fail closed: a service token may only run workflows inside the namespaces its `ns`* claim holds (the same matching as contracts' namespaceAllowed: 'live.*' allows 'live.translate').@@@ -40,9 +36,6 @@ const CAPS = Object.freeze({appRun: 'ai.app.run',});--/** Ids enforced here before the pinned openvibe-contracts release knows them (docs/capabilities-proposal/). */--const PROPOSED = Object.freeze(['ai.app.run']);--/*** The Network signing keys, through the SDK's process-wide JWKS client (one per URL). A pinned* OV_NETWORK_PUBLIC_KEY PEM skips the fetch entirely; /api/ready reports either as ready.@@@ -64,12 +57,7 @@ function hasCap(claims, id) {function allows(claims, id) {if (!hasCap(claims, id)) return { allowed: false, code: 'capability.denied', reason: `${id} not granted` };-- if (!capabilities.get(id)) {-- // Not in the pinned contracts release yet: only a PROPOSED id is enforced (by the same exact-id-or-family-- // grant rule hasCap applied); any other unknown id is refused, never silently allowed.-- if (!PROPOSED.includes(id)) return { allowed: false, code: 'capability.denied', reason: `${id} is not a capability this release knows` };-- return { allowed: true, code: null, reason: null };-- }++ if (!capabilities.get(id)) return { allowed: false, code: 'capability.denied', reason: `${id} is not a capability this release knows` };const c = capabilities.check(claims, id);return c;}@@@ -200,4 +188,4 @@ function createAuth({ config, log = console }) {return { verify, requireCap, appOrCap, principalHas };}--module.exports = { CAPS, PROPOSED, createNetworkKeys, createAuth, hasCap, allows, isAppClaims, namespaceAllowed, effectiveNamespaces, principalSubject, bearer };++module.exports = { CAPS, createNetworkKeys, createAuth, hasCap, allows, isAppClaims, namespaceAllowed, effectiveNamespaces, principalSubject, bearer };@@@ -39,7 +39,7 @@ function load(env = process.env) {if (env.AI_PRICING_JSON) {try { pricing = JSON.parse(env.AI_PRICING_JSON) || {}; } catch { throw new Error('AI_PRICING_JSON is not valid JSON'); }}-- const baseUrl = trimUrl(env.BASE_URL || (isProduction ? 'https://ai.openvibe.network' : `http://localhost:${port}`));++ const baseUrl = trimUrl(env.BASE_URL || (isProduction ? 'https://ai.openvibe.services' : `http://localhost:${port}`));return {port,host: env.HOST || '127.0.0.1',@@@ -1,7 +1,7 @@'use strict';/*** The AI operator console (roadmap WS-O task 4): server-rendered, no JavaScript, on-- * ai.openvibe.network under /console, for OpenVibe.Network staff.++ * ai.openvibe.services under /console, for OpenVibe.Network staff.** GET /auth/login, /auth/callback Network SSO (authorization code + PKCE S256, OAuth client `ai`)* POST /auth/logout@@@ -119,7 +119,7 @@ function createFetcher(config, { transport = null } = {}) {const mod = url.protocol === 'https:' ? https : http;const req = mod.request(url, {method: 'GET',-- headers: { 'User-Agent': 'OpenVibe.AI/0.1 (+https://ai.openvibe.network)', Accept: '*/*' },++ headers: { 'User-Agent': 'OpenVibe.AI/0.1 (+https://ai.openvibe.services)', Accept: '*/*' },// Internal Media is operator-configured (usually loopback); everything else must be public.lookup: internal ? undefined : safeLookup,timeout: timeoutMs,@@@ -186,7 +186,7 @@ function createFetcher(config, { transport = null } = {}) {const mod = url.protocol === 'https:' ? https : http;const req = mod.request(url, {method: method === 'HEAD' ? 'HEAD' : 'GET',-- headers: { 'User-Agent': 'OpenVibe.AI/0.1 (+https://ai.openvibe.network)', Accept: '*/*', ...(range ? { Range: range } : {}) },++ headers: { 'User-Agent': 'OpenVibe.AI/0.1 (+https://ai.openvibe.services)', Accept: '*/*', ...(range ? { Range: range } : {}) },lookup: internal ? undefined : safeLookup,timeout: timeoutMs,});nnew file mode 100644@@@ -0,0 +1,161 @@++'use strict';++++/**++ * The product home of OpenVibe.AI (ai.openvibe.services, plan T6: AI as a developer product). The OpenVibe Frame++ * (openvibe-shared/shell) around openvibe-shared/showcase sections for developers. Every claim restates the README++ * (API, Developer apps, Providers, Local model) and the code; nothing here reads data. Prices and measured latency++ * are the /stats page, which reads them.++ *++ * renderHome({ siteUrl }) the whole document (siteUrl: the public origin, config.baseUrl)++ * HOME_CSP its Content-Security-Policy (the Frame's scripts and calls to the Network)++ * llmsTxt({ siteUrl }) /llms.txt, the same facts as plain text++ */++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.AI';++const NETWORK_URL = 'https://openvibe.network';++const PROJECTS_URL = 'https://openvibe.codes/projects';++const API_DOCS_URL = 'https://openvibe.codes/docs/api';++const SOURCE = 'https://github.com/OpenVibers/OpenVibe.AI';++const GUIDE = `${SOURCE}#developer-apps`;++const DESCRIPTION = 'One API for chat, generation, summaries, classification, extraction and embeddings across many models. '++ + 'Every run says which model answered, why it was chosen and what it cost, and sandbox apps run on free and local capacity.';++++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",++ "frame-src 'self' https://openvibe.network",++ "frame-ancestors 'none'",++ "object-src 'none'",++ "base-uri 'self'",++ "form-action 'self' https://openvibe.network",++].join('; ');++++function sections({ siteUrl }) {++ const token = [++ 'TOKEN=$(curl -s -X POST https://openvibe.network/oauth/token \\',++ ' -d grant_type=client_credentials -d client_id=$APP -d client_secret=$SECRET \\',++ ' -d audience=openvibe.ai | jq -r .access_token)',++ '# a five-minute token for your app (app_<ULID>), scoped to its project',++ ].join('\n');++ const chat = [++ `curl -s -X POST ${siteUrl}/api/v1/chat \\`,++ ' -H "Authorization: Bearer $TOKEN" -H \'Content-Type: application/json\' \\',++ ' -d \'{ "messages": [ { "role": "user", "content": "Name three uses for a paperclip." } ] }\'',++ '# → { "run": { "id": "run_…", "status": "succeeded", "output": …, "provenance": …, "explain": … } }',++ ].join('\n');++ const read = [++ `curl -s ${siteUrl}/api/v1/runs/$RUN -H "Authorization: Bearer $TOKEN" | jq .run.explain`,++ '# objective, the provider and model selected, and every candidate with its',++ '# estimated cost, latency and the reason it was passed over',++ ].join('\n');++ return showcase.hero({++ eyebrow: 'OpenVibe.AI · alpha',++ title: 'Many models, one API.', accent: 'Every answer explained.',++ lede: 'Chat, generate, summarize, classify, extract and embed through one API. OpenVibe.AI picks a model by price, measured latency '++ + 'and health, falls back when a provider fails, and tells you on every run which model answered, why it was chosen and what it cost.',++ actions: [{ label: 'Start building', href: PROJECTS_URL, primary: true }, { label: 'See prices and latency', href: '/stats' }],++ note: 'Alpha. OpenVibe.Live and OpenVibe.Network already run their AI here in production; developer apps start in the sandbox, on free and local capacity.',++ }) + showcase.features({++ title: 'What you get',++ items: [++ { icon: 'ov:text', title: 'Six operations', text: 'Chat, generate, summarize, classify, extract and embed, with structured JSON output when you ask for a schema.' },++ { icon: 'ov:map', title: 'Routing you can read', text: 'Every run carries an explanation: what it optimised for, the model it picked, and every other candidate with its estimated cost and latency.' },++ { icon: 'ov:history', title: 'Fallbacks on the record', text: 'A provider that is down, out of credentials or too slow is skipped and logged. When a fallback answers, the run says so.' },++ { icon: 'ov:host', title: 'Free and local first', text: 'A small open model runs on OpenVibe\'s own server, beside free providers. Sandbox apps use only that capacity, so trying it costs nothing.' },++ { icon: 'ov:account', title: 'Your project\'s runs', text: 'Runs belong to your project: another project can never read them. Usage is metered per project, with a free allowance first.' },++ { icon: 'ov:code', title: 'Open source', text: 'AGPL-3.0, built in the open. Every guarantee on this page has a test in the repository.' },++ ],++ }) + showcase.code({++ id: 'examples',++ title: 'Token, chat, explain',++ lede: 'With an app from OpenVibe.Codes: get a token from OpenVibe.Network, call an operation, then read why that model answered.',++ samples: [++ { label: 'Token', lang: 'bash', code: token },++ { label: 'Chat', lang: 'bash', code: chat },++ { label: 'Explain', lang: 'bash', code: read },++ ],++ }) + showcase.steps({++ title: 'Start in four steps',++ items: [++ { title: 'Create a project', text: 'Sign in at openvibe.codes/projects with your OpenVibe account and create a project.' },++ { title: 'Add an app', text: 'Add an app to it. A confidential app gets a client id and a secret, shown once.' },++ { title: 'Get a token', text: 'Exchange them at OpenVibe.Network for a five-minute token with the audience openvibe.ai.' },++ { title: 'Call an operation', text: 'POST to /api/v1/chat or any of the other five, and keep the run id to read it later.' },++ ],++ }) + showcase.limits({++ title: 'Sandbox and production',++ lede: 'Both environments call the same API; the token says which one an app is in.',++ columns: ['Sandbox', 'Production'],++ rows: [++ { label: 'Access to ai.app.run', values: ['Every sandbox app, without asking', 'Once OpenVibe staff add it to the project'] },++ { label: 'Models', values: ['Free and local capacity only', 'Free and local capacity; metered models under a project budget'] },++ { label: 'Free allowance', values: ['Per project, per provider', 'Per project, per provider'] },++ { label: 'Who can read a run', values: ['The project that made it', 'The project that made it'] },++ { label: 'A person\'s own provider key', values: ['Never used', 'Never used'] },++ ],++ }) + showcase.nextSteps({++ items: [++ { what: 'Prices and latency', text: 'Measured per provider and model over the last seven days.', href: '/stats' },++ { what: 'Create a project and an app', text: 'Sign in with your OpenVibe account; the sandbox is open to every project.', href: PROJECTS_URL, to: 'codes' },++ { what: 'API reference', text: 'The OpenAPI document generated from the published contracts.', href: API_DOCS_URL, to: 'codes' },++ { what: 'Developer guide', text: 'Tokens, limits, billing and the sandbox, in full.', href: GUIDE },++ ],++ });++}++++function renderHome({ siteUrl }) {++ const navLinks = [{ label: 'Prices', href: '/stats' }, { label: 'Guide', href: GUIDE }];++ const nav = { service: 'ai', apiBase: NETWORK_URL, links: navLinks, history: { type: 'page', title: SITE_NAME } };++ const footer = { service: 'ai', variant: 'full', mount: '#ov-footer', brandName: SITE_NAME };++ return shell.page({++ name: SITE_NAME, service: 'ai', lang: 'en',++ title: `${SITE_NAME}: many models, one API, every answer explained`,++ siteName: SITE_NAME,++ description: DESCRIPTION,++ summary: DESCRIPTION,++ canonical: `${siteUrl}/`,++ robots: 'index, follow',++ navbar: nav, footer, home: '/', navLinks,++ head: [++ appIcon.headTags({ site: 'ai' }),++ `<link rel="stylesheet" href="${ovServe.url(showcase.STYLESHEET)}">`,++ ].join('\n'),++ body: `<div id="navbar-mount"></div>++<main id="main" class="page">++${sections({ siteUrl })}++</main>`,++ });++}++++function llmsTxt({ siteUrl }) {++ return [++ `# ${SITE_NAME}`,++ '',++ `> ${DESCRIPTION}`,++ '',++ `- Home: ${siteUrl}/`,++ `- Prices and measured latency per provider and model: ${siteUrl}/stats`,++ `- Developer guide (tokens, limits, billing, sandbox): ${GUIDE}`,++ `- API reference (OpenAPI, generated from the contracts): ${API_DOCS_URL}`,++ `- Create a project and an app: ${PROJECTS_URL}`,++ `- Source (AGPL-3.0): ${SOURCE}`,++ '',++ '## Developer apps',++ '',++ '- Token: POST https://openvibe.network/oauth/token with grant_type=client_credentials, your app\'s client id and secret, audience=openvibe.ai (five minutes).',++ `- Operations: POST ${siteUrl}/api/v1/chat, /generate, /summarize, /classify, /extract, /embed; GET ${siteUrl}/api/v1/runs/:id for your own runs.`,++ '- Every run response carries explain: the objective, the model selected and every candidate with its estimated cost, latency and excluded reason.',++ '- Sandbox apps run on free and local capacity only; runs, usage and the free allowance are per project; a person\'s own provider key is never used for an app.',++ '',++ ].join('\n');++}++++module.exports = { renderHome, llmsTxt, HOME_CSP, SITE_NAME };@@@ -487,17 +487,18 @@ t.test('sign out revokes the session', async () => {for (const s of SECRETS) assert.ok(!JSON.stringify(await h.db.prepare('SELECT * FROM audit_log').all()).includes(s), 'no secret in the audit log');});--t.test('robots.txt keeps crawlers out of the console and sign-in; there is no sitemap', async () => {++t.test('robots.txt keeps crawlers out of the console and sign-in; the sitemap lists only public pages', async () => {const r = await request(h.base, 'GET', '/robots.txt');assert.strictEqual(r.status, 200);assert.ok(r.text.includes('Disallow: /console') && r.text.includes('Disallow: /auth/'));-- assert.ok(!/sitemap/i.test(r.text));-- assert.strictEqual((await request(h.base, 'GET', '/sitemap.xml')).status, 404);++ const map = await request(h.base, 'GET', '/sitemap.xml');++ assert.strictEqual(map.status, 200);++ assert.ok(!/\/console|\/auth\//.test(map.text), 'the console and sign-in are never in the sitemap');assert.ok((await request(h.base, 'GET', '/')).text.includes('/console'));});t.test('the nginx vhost proxies /console, /auth/ and /robots.txt with the client address from $remote_addr only', () => {-- const conf = fs.readFileSync(path.join(__dirname, '..', 'deploy', 'nginx', 'ai.openvibe.network.conf'), 'utf8');++ const conf = fs.readFileSync(path.join(__dirname, '..', 'deploy', 'nginx', 'ai.openvibe.services.conf'), 'utf8');const active = conf.split('\n').filter((l) => !/^\s*#/.test(l)).join('\n');for (const loc of ['location /console', 'location /auth/', 'location = /robots.txt']) {const i = active.indexOf(`${loc} {`);nnew file mode 100644@@@ -0,0 +1,98 @@++'use strict';++// The public home (plan T6: AI as a developer product). ai.openvibe.services 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. The vhosts: the developer-app routes are the only++// API the public vhost proxies, and ai.openvibe.network is a 301 to the product origin.++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const { boot, request, suite } = require('./helpers');++const { HOME_CSP } = require('../server/home');++++const SITE = 'https://ai.openvibe.services';++const BROWSER = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8';++const t = suite('home');++let h;++++t.test('boot with the public origin as BASE_URL', async () => {++ h = await boot({ env: { BASE_URL: SITE } });++});++++t.test('GET / as a browser: the home page, its CSP and a private cache', async () => {++ const r = await request(h.base, 'GET', '/', { headers: { Accept: BROWSER } });++ assert.strictEqual(r.status, 200);++ assert.match(r.headers.get('content-type'), /^text\/html/);++ assert.strictEqual(r.headers.get('content-security-policy'), HOME_CSP);++ assert.strictEqual(r.headers.get('cache-control'), 'private, max-age=300', 'Cloudflare ignores Vary: Accept');++ assert.match(r.headers.get('vary'), /Accept/);++ assert.strictEqual((r.text.match(/<h1\b/g) || []).length, 1, 'exactly one h1');++ assert.ok(r.text.includes('Many models, one API.'), 'the hero');++ assert.ok(r.text.includes(`<link rel="canonical" href="${SITE}/">`), 'the canonical public origin');++ assert.ok(r.text.includes(`${SITE}/api/v1/chat`), 'the examples call the public origin');++ assert.ok(r.text.includes('https://openvibe.codes/projects'), 'where a project and an app are made');++ assert.ok(!/ai\.openvibe\.network/.test(r.text), 'never the old name');++});++++t.test('GET / for curl and API clients: the text index, unchanged in kind', async () => {++ for (const accept of [undefined, '*/*', 'application/json', 'text/plain']) {++ const r = await request(h.base, 'GET', '/', { headers: accept ? { Accept: accept } : {} });++ assert.strictEqual(r.status, 200, String(accept));++ assert.match(r.headers.get('content-type'), /^text\/plain/, String(accept));++ assert.ok(r.text.startsWith('OpenVibe.AI: providers, models, routing'), String(accept));++ assert.ok(r.text.includes('ai.app.run') && r.text.includes('/console'), String(accept));++ }++});++++t.test('the home links the pinned Frame stylesheet, and /shared serves it', async () => {++ const r = await request(h.base, 'GET', '/', { headers: { Accept: BROWSER } });++ const href = (r.text.match(/href="(\/shared\/showcase\.css\?v=[0-9a-f]+)"/) || [])[1];++ assert.ok(href, 'the showcase stylesheet, content-addressed');++ const css = await request(h.base, 'GET', href);++ assert.strictEqual(css.status, 200);++ assert.match(css.headers.get('content-type'), /text\/css/);++ assert.match(css.headers.get('cache-control'), /immutable/);++});++++t.test('robots.txt, sitemap.xml and llms.txt name the public origin', async () => {++ const robots = await request(h.base, 'GET', '/robots.txt');++ assert.strictEqual(robots.status, 200);++ for (const line of ['Allow: /$', 'Allow: /stats', 'Disallow: /console', 'Disallow: /auth/', 'Disallow: /api/', `Sitemap: ${SITE}/sitemap.xml`]) assert.ok(robots.text.includes(line), line);++ const map = await request(h.base, 'GET', '/sitemap.xml');++ assert.strictEqual(map.status, 200);++ assert.match(map.headers.get('content-type'), /xml/);++ assert.deepStrictEqual([...map.text.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]), [`${SITE}/`, `${SITE}/stats`]);++ const llms = await request(h.base, 'GET', '/llms.txt');++ assert.strictEqual(llms.status, 200);++ assert.match(llms.headers.get('content-type'), /^text\/plain/);++ assert.ok(llms.text.startsWith('# OpenVibe.AI'));++ assert.ok(llms.text.includes(`${SITE}/stats`) && llms.text.includes(`POST ${SITE}/api/v1/chat`));++});++++const active = (file) => fs.readFileSync(path.join(__dirname, '..', 'deploy', 'nginx', file), 'utf8').split('\n').filter((l) => !/^\s*#/.test(l)).join('\n');++++t.test('the public vhost proxies the home, its files and exactly the developer-app API routes', () => {++ const conf = active('ai.openvibe.services.conf');++ for (const loc of ['location = / {', 'location = /stats', 'location = /sitemap.xml', 'location = /llms.txt', 'location /shared/']) assert.ok(conf.includes(loc), loc);++ const ops = conf.indexOf('location ~ ^/api/v1/(chat|generate|summarize|classify|extract|embed)$ {');++ const run = conf.indexOf('location ~ ^/api/v1/runs/[A-Za-z0-9_-]+$ {');++ assert.ok(ops >= 0 && run >= 0, 'the six operations and the run read');++ for (const [i, verb] of [[ops, 'POST'], [run, 'GET']]) {++ const block = conf.slice(i, conf.indexOf('\n }', i));++ assert.ok(block.includes(`limit_except ${verb} { deny all; }`), verb);++ assert.ok(block.includes('limit_req zone=ovai_apps'), 'rate-limited per address');++ for (const hdr of ['X-Real-IP $remote_addr', 'X-Forwarded-For $remote_addr', 'CF-Connecting-IP $remote_addr']) assert.ok(block.includes(hdr), hdr);++ }++ const rest = conf.indexOf('location /api/ {');++ assert.ok(conf.slice(rest, conf.indexOf('}', rest)).includes('return 403;'), 'the rest of the API stays host-local');++ assert.ok(!conf.includes('$proxy_add_x_forwarded_for'));++});++++t.test('ai.openvibe.network answers 301 to the same path on ai.openvibe.services, and proxies nothing', () => {++ const conf = active('ai.openvibe.network.conf');++ assert.ok(!conf.includes('proxy_pass'), 'nothing is served under the old name');++ assert.strictEqual((conf.match(/return 301 https:\/\/ai\.openvibe\.services\$request_uri;/g) || []).length, 2, 'HTTP and HTTPS');++});++++t.test('shutdown', async () => { await h.stop(); });++++t.run();
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
# ═══════════════════════════════════════════════════════════════
# ═══════════════════════════════════════════════════════════════
-# ai.openvibe.network — OpenVibe.AI Environment Configuration
-# Target host: ai.openvibe.services (the public home; BASE_URL still defaults to ai.openvibe.network here).
+# ai.openvibe.services — OpenVibe.AI Environment Configuration
+# The public home and API origin (ai.openvibe.network answers 301 to it). BASE_URL defaults to it in production.
# Copy to .env and fill in values. In production this lives at /etc/openvibe/ai.env (0600).
# Copy to .env and fill in values. In production this lives at /etc/openvibe/ai.env (0600).
# Secrets are read by NAME only (providers store 'env:NAME' references); nothing logs or returns them.
# Secrets are read by NAME only (providers store 'env:NAME' references); nothing logs or returns them.
# ═══════════════════════════════════════════════════════════════
# ═══════════════════════════════════════════════════════════════
@@ -35,8 +35,7 @@ OV_NETWORK_INTERNAL_URL=http://127.0.0.1:4000
@@ -35,8 +35,7 @@ OV_NETWORK_INTERNAL_URL=http://127.0.0.1:4000
# ── Operator console (/console; README "Operator console") ──
# ── Operator console (/console; README "Operator console") ──
# Network SSO as OAuth client `ai` (the client AI's service tokens use). The Network must list
# Network SSO as OAuth client `ai` (the client AI's service tokens use). The Network must list
-# ${BASE_URL}/auth/callback (https://ai.openvibe.network/auth/callback) as a redirect URI of client `ai`.
-# Target host: also list https://ai.openvibe.services/auth/callback when BASE_URL moves there.
+# ${BASE_URL}/auth/callback (https://ai.openvibe.services/auth/callback) as a redirect URI of client `ai`.
# OV_OAUTH_CLIENT_SECRET is also what the events relay and the user modules authenticate with.
# OV_OAUTH_CLIENT_SECRET is also what the events relay and the user modules authenticate with.
#OV_OAUTH_CLIENT_ID=ai
#OV_OAUTH_CLIENT_ID=ai
#OV_OAUTH_CLIENT_SECRET=
#OV_OAUTH_CLIENT_SECRET=
@@ -2,11 +2,11 @@
@@ -2,11 +2,11 @@
> Providers, models, routing, prompt templates, workflows, runs, citations, cache and quotas for every product.
> Providers, models, routing, prompt templates, workflows, runs, citations, cache and quotas for every product.
-**Status:** alpha (roadmap Wave 13). Deployed on the host since 2026-09-23 on `127.0.0.1:4700`
-(loopback only) and in production use: OpenVibe.Live runs with `AI_SERVICE=remote` and Network's
-footer copy runs `network.site_copy` here. It is not a public product: the domain keeps its placeholder
-page until the launch rule below is met.
-**Domain:** `ai.openvibe.network` (target host `ai.openvibe.services`) · **Port:** 4700 · **Unit:** `openvibe-ai.service`
+**Status:** alpha (roadmap Wave 13). Deployed on the host since 2026-09-23 on `127.0.0.1:4700` and in
+production use: OpenVibe.Live runs with `AI_SERVICE=remote` and Network's footer copy runs
+`network.site_copy` here. Public since 2026-10-07 at [ai.openvibe.services](https://ai.openvibe.services):
+the home (`server/home.js`), `/stats` and the developer-app API (`ai.app.run`).
+**Domain:** `ai.openvibe.services` (`ai.openvibe.network` answers 301 to it) · **Port:** 4700 · **Unit:** `openvibe-ai.service`
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §12.1, §12.13, §15.14, §33, §34.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §12.1, §12.13, §15.14, §33, §34.
**License:** AGPL-3.0 (same as every OpenVibe service).
**License:** AGPL-3.0 (same as every OpenVibe service).
@@ -51,9 +51,8 @@ owns publication truth: Wiki, Blog, News, Live, … decide what to publish.
@@ -51,9 +51,8 @@ owns publication truth: Wiki, Blog, News, Live, … decide what to publish.
Implemented here (the service manifest's `capabilities`, audience `openvibe.ai`; routes under
Implemented here (the service manifest's `capabilities`, audience `openvibe.ai`; routes under
[API](#api)): `ai.run.create`, `ai.run.read`, `ai.workflow.manage`, `ai.provider.manage`,
[API](#api)): `ai.run.create`, `ai.run.read`, `ai.workflow.manage`, `ai.provider.manage`,
`ai.usage.read`, `ai.credential.manage` (`/api/v1/credentials/:subject`),
`ai.usage.read`, `ai.credential.manage` (`/api/v1/credentials/:subject`),
-`ai.quota.attribution.manage` (per-streamer quotas) and `ai.app.run` (developer apps, ADR-014; a
-public capability proposed in `docs/capabilities-proposal/` until openvibe-contracts registers it —
-`server/auth.js` enforces it through its PROPOSED fallback until then). A service token's `ns`
+`ai.quota.attribution.manage` (per-streamer quotas) and `ai.app.run` (developer apps, ADR-014; public,
+registered since openvibe-contracts 0.109.0). A service token's `ns`
claim limits which workflow namespaces it may run; an app token is fixed to its project's.
claim limits which workflow namespaces it may run; an app token is fixed to its project's.
Called elsewhere, as the service principal `ai`: `events.event.publish` (Events, the outbox relay)
Called elsewhere, as the service principal `ai`: `events.event.publish` (Events, the outbox relay)
@@ -145,10 +144,10 @@ and its rollback lever (`AI_NS_REQUIRED`) were retired on 2026-09-28 (shims C-22
@@ -145,10 +144,10 @@ and its rollback lever (`AI_NS_REQUIRED`) were retired on 2026-09-28 (shims C-22
Roadmap Wave 20, ADR-014. A developer app gets a token from OpenVibe.Network (`POST /oauth/token`,
Roadmap Wave 20, ADR-014. A developer app gets a token from OpenVibe.Network (`POST /oauth/token`,
`grant_type=client_credentials` with its `app_<ULID>` client, `audience=openvibe.ai`): `sub
`grant_type=client_credentials` with its `app_<ULID>` client, `audience=openvibe.ai`): `sub
app:app_<ULID>`, `project_id prj_<ULID>`, `env sandbox|production`, ns `[project_id,
app:app_<ULID>`, `project_id prj_<ULID>`, `env sandbox|production`, ns `[project_id,
-app.<project_id>.*]`, five minutes. AI accepts one capability for it, `ai.app.run` (public;
-proposed in `docs/capabilities-proposal/ai.app.run.json` until openvibe-contracts registers it —
-until then `server/auth.js` enforces it through its PROPOSED fallback, as Events did before
-`events.app.*` shipped):
+app.<project_id>.*]`, five minutes. AI accepts one capability for it, `ai.app.run` (public). A
+sandbox app holds it without asking (Network's default sandbox allowance); a production app holds it
+once OpenVibe staff add it to the project's allowance. Create the project and its app at
+[openvibe.codes/projects](https://openvibe.codes/projects):
| Route | What |
| Route | What |
|---|---|
|---|---|
@@ -178,7 +177,7 @@ project's runs and never uses a person's BYO provider key: the `byok` provider p
@@ -178,7 +177,7 @@ project's runs and never uses a person's BYO provider key: the `byok` provider p
offered to an app run, and `credential` on its request is refused.
offered to an app run, and `credential` on its request is refused.
```bash
```bash
-AI=https://ai.openvibe.network
+AI=https://ai.openvibe.services
APP=app_01JAB… # the app's client id; SECRET is its client secret
APP=app_01JAB… # the app's client id; SECRET is its client secret
TOKEN=$(curl -s -X POST https://openvibe.network/oauth/token \
TOKEN=$(curl -s -X POST https://openvibe.network/oauth/token \
-d grant_type=client_credentials -d client_id=$APP -d client_secret=$SECRET \
-d grant_type=client_credentials -d client_id=$APP -d client_secret=$SECRET \
@@ -194,7 +193,7 @@ AI takes one on these routes alone, and every other route refuses `env: sandbox`
@@ -194,7 +193,7 @@ AI takes one on these routes alone, and every other route refuses `env: sandbox`
## Operator console
## Operator console
-`/console` on ai.openvibe.network (roadmap WS-O task 4) is a server-rendered staff console: no
+`/console` on ai.openvibe.services (roadmap WS-O task 4) is a server-rendered staff console: no
JavaScript, one inline stylesheet allowed by its CSP hash, `noindex` (meta and `X-Robots-Tag`),
JavaScript, one inline stylesheet allowed by its CSP hash, `noindex` (meta and `X-Robots-Tag`),
`no-store`, disallowed in `robots.txt`, in no sitemap. It does not load the OpenVibe Frame (the Frame
`no-store`, disallowed in `robots.txt`, in no sitemap. It does not load the OpenVibe Frame (the Frame
needs scripts and inline styles), like Billing's staff console. Code: `server/console/`.
needs scripts and inline styles), like Billing's staff console. Code: `server/console/`.
@@ -440,19 +439,15 @@ runs as `openvibe-llm.service` on 127.0.0.1:8090 ([Local model](#local-model)).
@@ -440,19 +439,15 @@ runs as `openvibe-llm.service` on 127.0.0.1:8090 ([Local model](#local-model)).
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 ai --to <sha>`. Migrations only add tables and columns.
restart; afterwards `sudo ovhost rollback ai --to <sha>`. Migrations only add tables and columns.
-Deployed: `/opt/openvibe.ai`, env `/etc/openvibe/ai.env` (0600), unit `deploy/systemd/openvibe-ai.service`
-(`StateDirectory=openvibe-ai`; database `ov_ai` on the host's data role), principal `ai`. The nginx vhost
-`deploy/nginx/ai.openvibe.network.conf` (health/ready, `/`, `/robots.txt`, the operator console
-`/console` and its sign-in `/auth/` public; the API is host-local) is not installed:
-`ai.openvibe.network` still serves the Sites placeholder. `deploy/nginx/ai.openvibe.services.conf`
-mirrors that vhost for `ai.openvibe.services` (the same proxied locations to the same upstream,
-`/metrics` and the API still blocked, on the `*.openvibe.services` certificate the host already holds) and
-replaces the Sites placeholder for `ai.openvibe.services` when installed; it is not installed or live yet. The console also needs
-`AI_CONSOLE_SESSION_SECRET` (32+ random characters) and `OV_OAUTH_CLIENT_SECRET` in the env file
-(without them it answers 503 in production), and the Network must list
-`https://ai.openvibe.network/auth/callback` among client `ai`'s redirect URIs (and
-`https://ai.openvibe.services/auth/callback` once that vhost serves the console, since the callback is built from
-the base URL).
+Deployed: `/opt/openvibe.ai`, env `/etc/openvibe/ai.env` (0600; `BASE_URL=https://ai.openvibe.services`), unit
+`deploy/systemd/openvibe-ai.service` (`StateDirectory=openvibe-ai`; database `ov_ai` on the host's data role),
+principal `ai`. The vhost `deploy/nginx/ai.openvibe.services.conf` (on the `*.openvibe.services` certificate)
+serves the home, `/stats`, `/shared/*`, `/robots.txt`, `/sitemap.xml`, `/llms.txt`, health/ready, the operator
+console `/console` with its sign-in `/auth/`, and exactly the developer-app routes (the six operations by POST,
+`/api/v1/runs/:id` by GET, rate-limited per address); every other `/api/` path and `/metrics` stay host-local.
+`deploy/nginx/ai.openvibe.network.conf` answers 301 to the same path on ai.openvibe.services. The console needs
+`AI_CONSOLE_SESSION_SECRET` (32+ random characters) and `OV_OAUTH_CLIENT_SECRET` in the env file (without them it
+answers 503 in production); Network client `ai` lists `https://ai.openvibe.services/auth/callback`.
## Not done yet
## Not done yet
@@ -460,9 +455,8 @@ the base URL).
@@ -460,9 +455,8 @@ the base URL).
themselves; moving the remaining passthrough prompts into AI templates (most `live.*` workflows are
themselves; moving the remaining passthrough prompts into AI templates (most `live.*` workflows are
versioned templates since 2026-09-27);
versioned templates since 2026-09-27);
removing Network's fallback to Live's `/internal/ai/site-copy`; a public server-rendered status page.
removing Network's fallback to Live's `/internal/ai/site-copy`; a public server-rendered status page.
-- The operator console is built but not reachable yet: it needs the vhost below installed (which
- replaces the Sites placeholder), `https://ai.openvibe.network/auth/callback` listed as a redirect URI
- of Network OAuth client `ai`, and `AI_CONSOLE_SESSION_SECRET` in `/etc/openvibe/ai.env`.
+- Metered models for developer apps: production app runs use free and local capacity until a tier budget
+ governs their project (`AI_GOVERN_TIERS` is off in production) and OpenVibe.Billing charges project subjects.
- Import holds from the 2026-09-23 run: a streamer's own provider key stays in Live, and 3,021 Live
- Import holds from the 2026-09-23 run: a streamer's own provider key stays in Live, and 3,021 Live
translations cannot become cache entries (they have no source text) and stay in Live.
translations cannot become cache entries (they have no source text) and stay in Live.
- Text routes now run a capability pool (`shared` plus a local model when one is configured) instead
- Text routes now run a capability pool (`shared` plus a local model when one is configured) instead
@@ -473,23 +467,15 @@ the base URL).
@@ -473,23 +467,15 @@ the base URL).
- The host has a backup of `ov_ai` but no restore drill has run for it; `/metrics` is built but not
- The host has a backup of `ov_ai` but no restore drill has run for it; `/metrics` is built but not
deployed yet.
deployed yet.
-## Launch rule
+## Launch
-This repository does not make the product real, and the domain keeps its placeholder page on
-[OpenVibers/OpenVibe.Sites](https://github.com/OpenVibers/OpenVibe.Sites) until all of the
-following exist here (plan §12.12):
-
-1. an owning runtime with health/readiness endpoints and observability;
-2. canonical identity/auth integration (OpenVibe.Network subjects, scoped service principals);
-3. server-rendered or static public routes that are useful without JavaScript;
-4. real persistence and end-to-end workflows;
-5. capability and event registration against `OpenVibe.Contracts`;
-6. a migration/seed strategy, a security/threat review, and sitemap/robots/feed behaviour;
-7. acceptance tests proving the advertised functionality.
-
-The launch release removes the domain from `OpenVibe.Sites/sites.json`, switches routing and
-registers maturity in the ecosystem registry atomically. A placeholder is never counted as an
-implemented service.
+Launched 2026-10-07 under the launch rule (plan §12.12): the runtime with health, readiness and metrics; Network
+subjects and scoped service principals; server-rendered public routes useful without JavaScript (the home,
+`/stats`); PostgreSQL persistence and end-to-end workflows; capabilities and events registered in
+`OpenVibe.Contracts` (`ai.app.run` since 0.109.0, the service live since 0.110.0); migrations at boot,
+robots/sitemap/llms.txt; acceptance tests ([above](#acceptance-guarantees-and-where-they-are-tested), and
+`test/home.test.js` for the public routes and vhosts). The same release removed ai.openvibe.services and the
+ai.openvibe.network notice from OpenVibe.Sites.
---
---
@@ -2,19 +2,9 @@
@@ -2,19 +2,9 @@
## ai.openvibe.network — Nginx Configuration (REFERENCE)
## ai.openvibe.network — Nginx Configuration (REFERENCE)
##
##
## Deployed to /etc/nginx/sites-available/ai.openvibe.network.conf — keep them in sync, and reload
## Deployed to /etc/nginx/sites-available/ai.openvibe.network.conf — keep them in sync, and reload
-## nginx after changing it. The domain keeps its OpenVibe.Sites placeholder until the launch rule in
-## README.md is met; this vhost is what replaces it then.
-##
-## The Node service (port 4700, systemd unit openvibe-ai) serves:
-## /api/health, /api/ready
-## /metrics Prometheus text, answered only to direct loopback callers (never proxied)
-## /api/v1/* service-to-service API (Network service tokens, audience openvibe.ai). Services on
-## this host call 127.0.0.1:4700 directly, so the public vhost refuses it; open the
-## location below only when a service on another host needs it (it still needs a token).
-## /console the operator console (server-rendered, OpenVibe.Network staff only) and /auth/* its
-## Network sign-in (redirect URI https://ai.openvibe.network/auth/callback, OAuth client
-## `ai`); both noindex. /robots.txt keeps crawlers out of them.
-## /stats public price and latency per provider and model (server-rendered, no sign-in)
+## nginx after changing it. OpenVibe.AI's public origin is https://ai.openvibe.services
+## (deploy/nginx/ai.openvibe.services.conf); this name answers 301 to the same path there.
+## Services on this host never come through here: they call 127.0.0.1:4700 directly.
## ═══════════════════════════════════════════════════════════════
## ═══════════════════════════════════════════════════════════════
server {
server {
@@ -23,7 +13,7 @@ server {
@@ -23,7 +13,7 @@ server {
server_name ai.openvibe.network;
server_name ai.openvibe.network;
location /.well-known/acme-challenge/ { root /var/www/certbot; }
location /.well-known/acme-challenge/ { root /var/www/certbot; }
- location / { return 301 https://ai.openvibe.network$request_uri; }
+ location / { return 301 https://ai.openvibe.services$request_uri; }
}
}
server {
server {
@@ -39,63 +29,7 @@ server {
@@ -39,63 +29,7 @@ server {
access_log /var/log/nginx/ai.openvibe.network.access.log;
access_log /var/log/nginx/ai.openvibe.network.access.log;
error_log /var/log/nginx/ai.openvibe.network.error.log;
error_log /var/log/nginx/ai.openvibe.network.error.log;
- # Inline images arrive as data URLs inside run inputs.
- client_max_body_size 12m;
-
- add_header X-Content-Type-Options nosniff always;
- add_header Referrer-Policy strict-origin-when-cross-origin always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
- location = /api/health { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
- location = /api/ready { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
-
- # Metrics are for host-local scrapers only (http://127.0.0.1:4700/metrics).
- location = /metrics { return 404; }
-
- # ── Service API: host-local only (see header) ───────────
- location /api/ {
- return 403;
- # To expose it (token-guarded) to off-host services instead:
- # proxy_pass http://127.0.0.1:4700;
- # proxy_http_version 1.1;
- # proxy_set_header Host $host;
- # proxy_set_header X-Real-IP $remote_addr; # client address from $remote_addr only (realip),
- # proxy_set_header X-Forwarded-For $remote_addr; # never appended to or passed through
- # proxy_set_header CF-Connecting-IP $remote_addr;
- # proxy_set_header X-Forwarded-Proto $scheme;
- # proxy_read_timeout 90s; # ?wait= holds a request up to 60 s
- }
-
- # ── Operator console and its Network sign-in (staff only; the app refuses everyone else) ──
- location /console {
- proxy_pass http://127.0.0.1:4700;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $remote_addr;
- proxy_set_header CF-Connecting-IP $remote_addr;
- proxy_set_header X-Forwarded-Proto $scheme;
- }
- location /auth/ {
- proxy_pass http://127.0.0.1:4700;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $remote_addr;
- proxy_set_header CF-Connecting-IP $remote_addr;
- proxy_set_header X-Forwarded-Proto $scheme;
- }
- location = /robots.txt { proxy_pass http://127.0.0.1:4700; 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 = /stats { proxy_pass http://127.0.0.1:4700; 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 = / {
- proxy_pass http://127.0.0.1:4700;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $remote_addr;
- proxy_set_header CF-Connecting-IP $remote_addr;
- }
-
- location / {
- return 404;
- }
+ location / { return 301 https://ai.openvibe.services$request_uri; }
}
}
@@ -4,20 +4,28 @@
@@ -4,20 +4,28 @@
## Deployed to /etc/nginx/sites-available/ai.openvibe.services.conf — keep them in sync, and reload
## Deployed to /etc/nginx/sites-available/ai.openvibe.services.conf — keep them in sync, and reload
## nginx after changing it. It uses the *.openvibe.services certificate the host already holds
## nginx after changing it. It uses the *.openvibe.services certificate the host already holds
## (/etc/letsencrypt/live/openvibe.services/); installed in place of the OpenVibe.Sites placeholder
## (/etc/letsencrypt/live/openvibe.services/); installed in place of the OpenVibe.Sites placeholder
-## vhost of the same name, it replaces that placeholder.
+## vhost of the same name, it replaces that placeholder. ai.openvibe.network (its own vhost) answers
+## 301 to here.
##
##
## The Node service (port 4700, systemd unit openvibe-ai) serves:
## The Node service (port 4700, systemd unit openvibe-ai) serves:
+## / the product home for a browser (server/home.js), the text API index for anything else
+## /stats public price and latency per provider and model (server-rendered, no sign-in)
+## /shared/* the home's OpenVibe Frame files (content-addressed); /robots.txt, /sitemap.xml, /llms.txt
## /api/health, /api/ready
## /api/health, /api/ready
+## /api/v1/{chat,generate,summarize,classify,extract,embed} (POST) and /api/v1/runs/:id (GET)
+## developer apps (ai.app.run, app tokens from OpenVibe.Network): open here, rate-limited
+## per address; the app checks the token on every request
+## /api/v1/* everything else is service-to-service (first-party service tokens). Services on this
+## host call 127.0.0.1:4700 directly, so the public vhost refuses it.
## /metrics Prometheus text, answered only to direct loopback callers (never proxied)
## /metrics Prometheus text, answered only to direct loopback callers (never proxied)
-## /api/v1/* service-to-service API (Network service tokens, audience openvibe.ai). Services on
-## this host call 127.0.0.1:4700 directly, so the public vhost refuses it; open the
-## location below only when a service on another host needs it (it still needs a token).
## /console the operator console (server-rendered, OpenVibe.Network staff only) and /auth/* its
## /console the operator console (server-rendered, OpenVibe.Network staff only) and /auth/* its
## Network sign-in (redirect URI https://ai.openvibe.services/auth/callback, OAuth client
## Network sign-in (redirect URI https://ai.openvibe.services/auth/callback, OAuth client
## `ai`); both noindex. /robots.txt keeps crawlers out of them.
## `ai`); both noindex. /robots.txt keeps crawlers out of them.
-## /stats public price and latency per provider and model (server-rendered, no sign-in)
## ═══════════════════════════════════════════════════════════════
## ═══════════════════════════════════════════════════════════════
+# Developer-app calls per client address (after realip): a burst for interactive use, then 5 a second.
+limit_req_zone $binary_remote_addr zone=ovai_apps:10m rate=5r/s;
+
server {
server {
listen 80;
listen 80;
listen [::]:80;
listen [::]:80;
@@ -53,6 +61,33 @@ server {
@@ -53,6 +61,33 @@ server {
# Metrics are for host-local scrapers only (http://127.0.0.1:4700/metrics).
# Metrics are for host-local scrapers only (http://127.0.0.1:4700/metrics).
location = /metrics { return 404; }
location = /metrics { return 404; }
+ # ── Developer apps (ai.app.run): the six direct operations and a run read ──
+ location ~ ^/api/v1/(chat|generate|summarize|classify|extract|embed)$ {
+ limit_req zone=ovai_apps burst=20 nodelay;
+ limit_req_status 429;
+ limit_except POST { deny all; }
+ proxy_pass http://127.0.0.1:4700;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr; # client address from $remote_addr only (realip),
+ proxy_set_header X-Forwarded-For $remote_addr; # never appended to or passed through
+ proxy_set_header CF-Connecting-IP $remote_addr;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_read_timeout 90s; # ?wait= holds a request up to 60 s
+ }
+ location ~ ^/api/v1/runs/[A-Za-z0-9_-]+$ {
+ limit_req zone=ovai_apps burst=20 nodelay;
+ limit_req_status 429;
+ limit_except GET { deny all; }
+ proxy_pass http://127.0.0.1:4700;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header CF-Connecting-IP $remote_addr;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
# ── Service API: host-local only (see header) ───────────
# ── Service API: host-local only (see header) ───────────
location /api/ {
location /api/ {
return 403;
return 403;
@@ -87,6 +122,10 @@ server {
@@ -87,6 +122,10 @@ server {
location = /robots.txt { proxy_pass http://127.0.0.1:4700; 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:4700; 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 = /stats { proxy_pass http://127.0.0.1:4700; 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 = /stats { proxy_pass http://127.0.0.1:4700; 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:4700; 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:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
+ location = /release.json { proxy_pass http://127.0.0.1:4700; 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:4700; 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 = / {
location = / {
proxy_pass http://127.0.0.1:4700;
proxy_pass http://127.0.0.1:4700;
@@ -8,16 +8,17 @@
@@ -8,16 +8,17 @@
"status": "alpha",
"status": "alpha",
"repository": "OpenVibers/OpenVibe.AI",
"repository": "OpenVibers/OpenVibe.AI",
"domains": [
"domains": [
+ "ai.openvibe.services",
"ai.openvibe.network"
"ai.openvibe.network"
],
],
- "publicOrigin": "https://ai.openvibe.network",
+ "publicOrigin": "https://ai.openvibe.services",
"health": "/api/health",
"health": "/api/health",
"ready": "/api/ready",
"ready": "/api/ready",
"internalOrigin": "http://127.0.0.1:4700",
"internalOrigin": "http://127.0.0.1:4700",
"exposure": {
"exposure": {
- "state": "internal",
- "publicSite": null,
- "note": "called by other services at 127.0.0.1:4700; ai.openvibe.network serves the OpenVibe.Sites placeholder page"
+ "state": "live",
+ "publicSite": "service",
+ "note": "the public home, /stats and the developer-app API at ai.openvibe.services (2026-10-07); ai.openvibe.network answers 301 to it; services on the host call 127.0.0.1:4700"
},
},
"capabilities": [
"capabilities": [
"ai.app.run",
"ai.app.run",
@@ -76,14 +77,14 @@
@@ -76,14 +77,14 @@
],
],
"window": "automatic only while ovhost waits for readiness after the restart (30 s in host.example.json); manual afterwards, as long as no blocker applies",
"window": "automatic only while ovhost waits for readiness after the restart (30 s in host.example.json); manual afterwards, as long as no blocker applies",
"blockers": {
"blockers": {
- "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
+ "none": "the schema code only adds (CREATE \u2026 IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
}
}
},
},
"contracts": {
"contracts": {
"range": ">=0.6.0 <1.0.0"
"range": ">=0.6.0 <1.0.0"
},
},
"leases": {
"leases": {
- "none": "no claims: one process owns ai.db (better-sqlite3, single writer); a run is taken with a compare-and-set on its status (queued → running)"
+ "none": "no claims: one process owns ai.db (better-sqlite3, single writer); a run is taken with a compare-and-set on its status (queued \u2192 running)"
}
}
},
},
"notes": "Providers, models, routing profiles, versioned prompt templates and workflows, runs, request logs, citations, scoped cache, quotas and audit. Workflow output is a draft/evidence package attributed to workflow + model + run. ai.run.queued|succeeded|failed|cached have payload contracts (v0.30.0, status planned): AI publishes them once its Events client is wired in; it publishes none today. ai.run.create takes ai.run-request@1 and returns ai.run@1."
"notes": "Providers, models, routing profiles, versioned prompt templates and workflows, runs, request logs, citations, scoped cache, quotas and audit. Workflow output is a draft/evidence package attributed to workflow + model + run. ai.run.queued|succeeded|failed|cached have payload contracts (v0.30.0, status planned): AI publishes them once its Events client is wired in; it publishes none today. ai.run.create takes ai.run-request@1 and returns ai.run@1."
@@ -13,9 +13,9 @@
@@ -13,9 +13,9 @@
"dotenv": "^16.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.109.0",
"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.12.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"devDependencies": {
"devDependencies": {
@@ -464,65 +464,6 @@
@@ -464,65 +464,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",
@@ -547,32 +488,6 @@
@@ -547,32 +488,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",
@@ -617,14 +532,6 @@
@@ -617,14 +532,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",
@@ -724,34 +631,6 @@
@@ -724,34 +631,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",
@@ -840,17 +719,6 @@
@@ -840,17 +719,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",
@@ -896,17 +764,6 @@
@@ -896,17 +764,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",
@@ -975,14 +832,6 @@
@@ -975,14 +832,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",
@@ -1019,14 +868,6 @@
@@ -1019,14 +868,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",
@@ -1073,14 +914,6 @@
@@ -1073,14 +914,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",
@@ -1149,42 +982,12 @@
@@ -1149,42 +982,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",
@@ -1422,53 +1225,12 @@
@@ -1422,53 +1225,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",
@@ -1478,20 +1240,6 @@
@@ -1478,20 +1240,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",
@@ -1516,21 +1264,10 @@
@@ -1516,21 +1264,10 @@
"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.86.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",
- "integrity": "sha512-3o3AILEBtQ2pK0KqK7GEAlN3HJjaKUl6vrbzEDiBbhK6Nr2hivgN/5MAmJ7WRkgaDkxNsZETs/fLZC2Q4oNCiA==",
+ "version": "0.109.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.109.0",
+ "integrity": "sha512-Ks2zLtR9XcN6ggr1APv93Ot8o/U1oAn8fOHNruYTDh1wjL/mKiwq5Lee8mx4rl6eb2A9zY8A45P/6edb3NNxgQ==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -1573,9 +1310,9 @@
@@ -1573,9 +1310,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.12.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.12.0",
+ "integrity": "sha512-GQpNFs2h61OKKNLexQwCfSJTLzNQ7fSbwqvrerB4oNdoxUgDvmdD43SELWycrb9DX8lkje4cR3ARznYIINnR8Q==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -1727,35 +1464,6 @@
@@ -1727,35 +1464,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",
@@ -1773,18 +1481,6 @@
@@ -1773,18 +1481,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",
@@ -1825,39 +1521,6 @@
@@ -1825,39 +1521,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",
@@ -2089,55 +1752,6 @@
@@ -2089,55 +1752,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/simple-swizzle": {
"node_modules/simple-swizzle": {
"version": "0.2.4",
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
@@ -2172,60 +1786,6 @@
@@ -2172,60 +1786,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",
@@ -2242,20 +1802,6 @@
@@ -2242,20 +1802,6 @@
"license": "0BSD",
"license": "0BSD",
"optional": true
"optional": true
},
},
- "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",
@@ -2278,14 +1824,6 @@
@@ -2278,14 +1824,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",
@@ -2304,14 +1842,6 @@
@@ -2304,14 +1842,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",
@@ -25,9 +25,9 @@
@@ -25,9 +25,9 @@
"dotenv": "^16.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.109.0",
"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.12.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"optionalDependencies": {
"optionalDependencies": {
@@ -13,6 +13,8 @@ const { createOps } = require('./ops');
@@ -13,6 +13,8 @@ const { createOps } = require('./ops');
const { consoleRouter } = require('./console');
const { consoleRouter } = require('./console');
const views = require('./console/views');
const views = require('./console/views');
const cachePolicy = require('openvibe-shared/cache-policy');
const cachePolicy = require('openvibe-shared/cache-policy');
+const ovServe = require('openvibe-shared/serve');
+const home = require('./home');
const pkg = require('../package.json');
const pkg = require('../package.json');
function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys, env = process.env, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, log = console, credentials = null }) {
function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys, env = process.env, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, log = console, credentials = null }) {
@@ -63,6 +65,9 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
@@ -63,6 +65,9 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
res.setHeader('Cache-Control', 'no-store');
res.setHeader('Cache-Control', 'no-store');
next();
next();
});
});
+ // The product home's browser files (the OpenVibe Frame, openvibe-shared/serve) under content-addressed
+ // /shared/* URLs from this repo's pinned openvibe-shared; each answer sets its own asset cache policy.
+ app.use('/shared', ovServe.handler());
// Inline images arrive as data URLs; the per-run input cap is enforced again in runs.create.
// Inline images arrive as data URLs; the per-run input cap is enforced again in runs.create.
app.use('/api', express.json({ limit: Math.ceil(config.runs.maxInputBytes * 1.5), type: ['application/json', 'application/*+json'] }));
app.use('/api', express.json({ limit: Math.ceil(config.runs.maxInputBytes * 1.5), type: ['application/json', 'application/*+json'] }));
@@ -113,12 +118,30 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
@@ -113,12 +118,30 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
} catch (e) { next(e); }
} catch (e) { next(e); }
});
});
- // Crawlers: nothing here is for search engines, the console and sign-in least of all (no sitemap either).
+ // Discovery for the public home (ai.openvibe.services): the home and the price page are for search engines
+ // and assistants; the console, its sign-in and the API are not.
app.get('/robots.txt', (_req, res) => {
app.get('/robots.txt', (_req, res) => {
- res.type('text/plain').send('User-agent: *\nDisallow: /console\nDisallow: /auth/\nDisallow: /api/\n');
+ res.type('text/plain').send(`User-agent: *\nAllow: /$\nAllow: /stats\nDisallow: /console\nDisallow: /auth/\nDisallow: /api/\n\nSitemap: ${config.baseUrl}/sitemap.xml\n`);
+ });
+ app.get('/sitemap.xml', (_req, res) => {
+ res.type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${['/', '/stats'].map((p) => ` <url><loc>${config.baseUrl}${p}</loc></url>`).join('\n')}\n</urlset>\n`);
+ });
+ app.get('/llms.txt', (_req, res) => {
+ res.setHeader('Cache-Control', 'public, max-age=3600');
+ res.type('text/plain').send(home.llmsTxt({ siteUrl: config.baseUrl }));
});
});
- app.get('/', (_req, res) => {
+ // GET /: the home page for a browser (server/home.js), 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 is
+ // private because Cloudflare caches by URL and ignores Vary: Accept. The home carries its own CSP.
+ 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.HOME_CSP)
+ .set('Cache-Control', 'private, max-age=300')
+ .send(home.renderHome({ siteUrl: config.baseUrl }));
+ }
res.type('text/plain').send([
res.type('text/plain').send([
'OpenVibe.AI: providers, models, routing, versioned prompt templates and workflows, runs, citations, scoped cache, quotas and audit.',
'OpenVibe.AI: providers, models, routing, versioned prompt templates and workflows, runs, citations, scoped cache, quotas and audit.',
'',
'',
@@ -130,7 +153,9 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
@@ -130,7 +153,9 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
'GET /stats price and latency per provider and model (public)',
'GET /stats price and latency per provider and model (public)',
'GET /console the operator console (OpenVibe.Network staff sign-in)',
'GET /console the operator console (OpenVibe.Network staff sign-in)',
'',
'',
- 'Callers authenticate with OpenVibe.Network service tokens (audience openvibe.ai).',
+ 'Callers authenticate with OpenVibe.Network tokens (audience openvibe.ai): first-party service tokens, or a',
+ 'developer app\'s token (ai.app.run) on the six direct operations and GET /api/v1/runs/:id.',
+ 'Developer apps: https://openvibe.codes/projects',
'AI output is a draft/evidence package attributed to a workflow, model and run, never to a person.',
'AI output is a draft/evidence package attributed to a workflow, model and run, never to a person.',
'',
'',
'Source: https://github.com/OpenVibers/OpenVibe.AI',
'Source: https://github.com/OpenVibers/OpenVibe.AI',
@@ -7,12 +7,8 @@
@@ -7,12 +7,8 @@
* exponential backoff, and a rotation honoured on an unknown kid. OV_NETWORK_PUBLIC_KEY still pins a
* exponential backoff, and a rotation honoured on an unknown kid. OV_NETWORK_PUBLIC_KEY still pins a
* PEM, which skips the fetch entirely.
* PEM, which skips the fetch entirely.
*
*
- * Capability ids are decided by openvibe-contracts when it knows them (the release named below);
- * until then allows() decides with the contracts' own grant rule (the exact id, or a `family.*`
- * grant), but only for the ids PROPOSED names — a proposal this service enforces ahead of the
- * release that registers it (docs/capabilities-proposal/, the pattern OpenVibe.Events used before
- * events.app.* shipped). ai.app.run (T6 step 10: developer apps, ADR-014) needs exactly that
- * fallback until openvibe-contracts knows the id.
+ * Capability ids are decided by openvibe-contracts: an id the pinned release does not know is refused,
+ * never silently allowed (ai.app.run, developer apps, is registered since openvibe-contracts 0.109.0).
*
*
* Namespaces fail closed: a service token may only run workflows inside the namespaces its `ns`
* Namespaces fail closed: a service token may only run workflows inside the namespaces its `ns`
* claim holds (the same matching as contracts' namespaceAllowed: 'live.*' allows 'live.translate').
* claim holds (the same matching as contracts' namespaceAllowed: 'live.*' allows 'live.translate').
@@ -40,9 +36,6 @@ const CAPS = Object.freeze({
@@ -40,9 +36,6 @@ const CAPS = Object.freeze({
appRun: 'ai.app.run',
appRun: 'ai.app.run',
});
});
-/** Ids enforced here before the pinned openvibe-contracts release knows them (docs/capabilities-proposal/). */
-const PROPOSED = Object.freeze(['ai.app.run']);
-
/**
/**
* The Network signing keys, through the SDK's process-wide JWKS client (one per URL). A pinned
* The Network signing keys, through the SDK's process-wide JWKS client (one per URL). A pinned
* OV_NETWORK_PUBLIC_KEY PEM skips the fetch entirely; /api/ready reports either as ready.
* OV_NETWORK_PUBLIC_KEY PEM skips the fetch entirely; /api/ready reports either as ready.
@@ -64,12 +57,7 @@ function hasCap(claims, id) {
@@ -64,12 +57,7 @@ function hasCap(claims, id) {
function allows(claims, id) {
function allows(claims, id) {
if (!hasCap(claims, id)) return { allowed: false, code: 'capability.denied', reason: `${id} not granted` };
if (!hasCap(claims, id)) return { allowed: false, code: 'capability.denied', reason: `${id} not granted` };
- if (!capabilities.get(id)) {
- // Not in the pinned contracts release yet: only a PROPOSED id is enforced (by the same exact-id-or-family
- // grant rule hasCap applied); any other unknown id is refused, never silently allowed.
- if (!PROPOSED.includes(id)) return { allowed: false, code: 'capability.denied', reason: `${id} is not a capability this release knows` };
- return { allowed: true, code: null, reason: null };
- }
+ if (!capabilities.get(id)) return { allowed: false, code: 'capability.denied', reason: `${id} is not a capability this release knows` };
const c = capabilities.check(claims, id);
const c = capabilities.check(claims, id);
return c;
return c;
}
}
@@ -200,4 +188,4 @@ function createAuth({ config, log = console }) {
@@ -200,4 +188,4 @@ function createAuth({ config, log = console }) {
return { verify, requireCap, appOrCap, principalHas };
return { verify, requireCap, appOrCap, principalHas };
}
}
-module.exports = { CAPS, PROPOSED, createNetworkKeys, createAuth, hasCap, allows, isAppClaims, namespaceAllowed, effectiveNamespaces, principalSubject, bearer };
+module.exports = { CAPS, createNetworkKeys, createAuth, hasCap, allows, isAppClaims, namespaceAllowed, effectiveNamespaces, principalSubject, bearer };
@@ -39,7 +39,7 @@ function load(env = process.env) {
@@ -39,7 +39,7 @@ function load(env = process.env) {
if (env.AI_PRICING_JSON) {
if (env.AI_PRICING_JSON) {
try { pricing = JSON.parse(env.AI_PRICING_JSON) || {}; } catch { throw new Error('AI_PRICING_JSON is not valid JSON'); }
try { pricing = JSON.parse(env.AI_PRICING_JSON) || {}; } catch { throw new Error('AI_PRICING_JSON is not valid JSON'); }
}
}
- const baseUrl = trimUrl(env.BASE_URL || (isProduction ? 'https://ai.openvibe.network' : `http://localhost:${port}`));
+ const baseUrl = trimUrl(env.BASE_URL || (isProduction ? 'https://ai.openvibe.services' : `http://localhost:${port}`));
return {
return {
port,
port,
host: env.HOST || '127.0.0.1',
host: env.HOST || '127.0.0.1',
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'use strict';
/**
/**
* The AI operator console (roadmap WS-O task 4): server-rendered, no JavaScript, on
* The AI operator console (roadmap WS-O task 4): server-rendered, no JavaScript, on
- * ai.openvibe.network under /console, for OpenVibe.Network staff.
+ * ai.openvibe.services under /console, for OpenVibe.Network staff.
*
*
* GET /auth/login, /auth/callback Network SSO (authorization code + PKCE S256, OAuth client `ai`)
* GET /auth/login, /auth/callback Network SSO (authorization code + PKCE S256, OAuth client `ai`)
* POST /auth/logout
* POST /auth/logout
@@ -119,7 +119,7 @@ function createFetcher(config, { transport = null } = {}) {
@@ -119,7 +119,7 @@ function createFetcher(config, { transport = null } = {}) {
const mod = url.protocol === 'https:' ? https : http;
const mod = url.protocol === 'https:' ? https : http;
const req = mod.request(url, {
const req = mod.request(url, {
method: 'GET',
method: 'GET',
- headers: { 'User-Agent': 'OpenVibe.AI/0.1 (+https://ai.openvibe.network)', Accept: '*/*' },
+ headers: { 'User-Agent': 'OpenVibe.AI/0.1 (+https://ai.openvibe.services)', Accept: '*/*' },
// Internal Media is operator-configured (usually loopback); everything else must be public.
// Internal Media is operator-configured (usually loopback); everything else must be public.
lookup: internal ? undefined : safeLookup,
lookup: internal ? undefined : safeLookup,
timeout: timeoutMs,
timeout: timeoutMs,
@@ -186,7 +186,7 @@ function createFetcher(config, { transport = null } = {}) {
@@ -186,7 +186,7 @@ function createFetcher(config, { transport = null } = {}) {
const mod = url.protocol === 'https:' ? https : http;
const mod = url.protocol === 'https:' ? https : http;
const req = mod.request(url, {
const req = mod.request(url, {
method: method === 'HEAD' ? 'HEAD' : 'GET',
method: method === 'HEAD' ? 'HEAD' : 'GET',
- headers: { 'User-Agent': 'OpenVibe.AI/0.1 (+https://ai.openvibe.network)', Accept: '*/*', ...(range ? { Range: range } : {}) },
+ headers: { 'User-Agent': 'OpenVibe.AI/0.1 (+https://ai.openvibe.services)', Accept: '*/*', ...(range ? { Range: range } : {}) },
lookup: internal ? undefined : safeLookup,
lookup: internal ? undefined : safeLookup,
timeout: timeoutMs,
timeout: timeoutMs,
});
});
new file mode 100644
new file mode 100644
@@ -0,0 +1,161 @@
@@ -0,0 +1,161 @@
+'use strict';
+
+/**
+ * The product home of OpenVibe.AI (ai.openvibe.services, plan T6: AI as a developer product). The OpenVibe Frame
+ * (openvibe-shared/shell) around openvibe-shared/showcase sections for developers. Every claim restates the README
+ * (API, Developer apps, Providers, Local model) and the code; nothing here reads data. Prices and measured latency
+ * are the /stats page, which reads them.
+ *
+ * renderHome({ siteUrl }) the whole document (siteUrl: the public origin, config.baseUrl)
+ * HOME_CSP its Content-Security-Policy (the Frame's scripts and calls to the Network)
+ * llmsTxt({ siteUrl }) /llms.txt, the same facts as plain text
+ */
+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.AI';
+const NETWORK_URL = 'https://openvibe.network';
+const PROJECTS_URL = 'https://openvibe.codes/projects';
+const API_DOCS_URL = 'https://openvibe.codes/docs/api';
+const SOURCE = 'https://github.com/OpenVibers/OpenVibe.AI';
+const GUIDE = `${SOURCE}#developer-apps`;
+const DESCRIPTION = 'One API for chat, generation, summaries, classification, extraction and embeddings across many models. '
+ + 'Every run says which model answered, why it was chosen and what it cost, and sandbox apps run on free and local capacity.';
+
+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",
+ "frame-src 'self' https://openvibe.network",
+ "frame-ancestors 'none'",
+ "object-src 'none'",
+ "base-uri 'self'",
+ "form-action 'self' https://openvibe.network",
+].join('; ');
+
+function sections({ siteUrl }) {
+ const token = [
+ 'TOKEN=$(curl -s -X POST https://openvibe.network/oauth/token \\',
+ ' -d grant_type=client_credentials -d client_id=$APP -d client_secret=$SECRET \\',
+ ' -d audience=openvibe.ai | jq -r .access_token)',
+ '# a five-minute token for your app (app_<ULID>), scoped to its project',
+ ].join('\n');
+ const chat = [
+ `curl -s -X POST ${siteUrl}/api/v1/chat \\`,
+ ' -H "Authorization: Bearer $TOKEN" -H \'Content-Type: application/json\' \\',
+ ' -d \'{ "messages": [ { "role": "user", "content": "Name three uses for a paperclip." } ] }\'',
+ '# → { "run": { "id": "run_…", "status": "succeeded", "output": …, "provenance": …, "explain": … } }',
+ ].join('\n');
+ const read = [
+ `curl -s ${siteUrl}/api/v1/runs/$RUN -H "Authorization: Bearer $TOKEN" | jq .run.explain`,
+ '# objective, the provider and model selected, and every candidate with its',
+ '# estimated cost, latency and the reason it was passed over',
+ ].join('\n');
+ return showcase.hero({
+ eyebrow: 'OpenVibe.AI · alpha',
+ title: 'Many models, one API.', accent: 'Every answer explained.',
+ lede: 'Chat, generate, summarize, classify, extract and embed through one API. OpenVibe.AI picks a model by price, measured latency '
+ + 'and health, falls back when a provider fails, and tells you on every run which model answered, why it was chosen and what it cost.',
+ actions: [{ label: 'Start building', href: PROJECTS_URL, primary: true }, { label: 'See prices and latency', href: '/stats' }],
+ note: 'Alpha. OpenVibe.Live and OpenVibe.Network already run their AI here in production; developer apps start in the sandbox, on free and local capacity.',
+ }) + showcase.features({
+ title: 'What you get',
+ items: [
+ { icon: 'ov:text', title: 'Six operations', text: 'Chat, generate, summarize, classify, extract and embed, with structured JSON output when you ask for a schema.' },
+ { icon: 'ov:map', title: 'Routing you can read', text: 'Every run carries an explanation: what it optimised for, the model it picked, and every other candidate with its estimated cost and latency.' },
+ { icon: 'ov:history', title: 'Fallbacks on the record', text: 'A provider that is down, out of credentials or too slow is skipped and logged. When a fallback answers, the run says so.' },
+ { icon: 'ov:host', title: 'Free and local first', text: 'A small open model runs on OpenVibe\'s own server, beside free providers. Sandbox apps use only that capacity, so trying it costs nothing.' },
+ { icon: 'ov:account', title: 'Your project\'s runs', text: 'Runs belong to your project: another project can never read them. Usage is metered per project, with a free allowance first.' },
+ { icon: 'ov:code', title: 'Open source', text: 'AGPL-3.0, built in the open. Every guarantee on this page has a test in the repository.' },
+ ],
+ }) + showcase.code({
+ id: 'examples',
+ title: 'Token, chat, explain',
+ lede: 'With an app from OpenVibe.Codes: get a token from OpenVibe.Network, call an operation, then read why that model answered.',
+ samples: [
+ { label: 'Token', lang: 'bash', code: token },
+ { label: 'Chat', lang: 'bash', code: chat },
+ { label: 'Explain', lang: 'bash', code: read },
+ ],
+ }) + showcase.steps({
+ title: 'Start in four steps',
+ items: [
+ { title: 'Create a project', text: 'Sign in at openvibe.codes/projects with your OpenVibe account and create a project.' },
+ { title: 'Add an app', text: 'Add an app to it. A confidential app gets a client id and a secret, shown once.' },
+ { title: 'Get a token', text: 'Exchange them at OpenVibe.Network for a five-minute token with the audience openvibe.ai.' },
+ { title: 'Call an operation', text: 'POST to /api/v1/chat or any of the other five, and keep the run id to read it later.' },
+ ],
+ }) + showcase.limits({
+ title: 'Sandbox and production',
+ lede: 'Both environments call the same API; the token says which one an app is in.',
+ columns: ['Sandbox', 'Production'],
+ rows: [
+ { label: 'Access to ai.app.run', values: ['Every sandbox app, without asking', 'Once OpenVibe staff add it to the project'] },
+ { label: 'Models', values: ['Free and local capacity only', 'Free and local capacity; metered models under a project budget'] },
+ { label: 'Free allowance', values: ['Per project, per provider', 'Per project, per provider'] },
+ { label: 'Who can read a run', values: ['The project that made it', 'The project that made it'] },
+ { label: 'A person\'s own provider key', values: ['Never used', 'Never used'] },
+ ],
+ }) + showcase.nextSteps({
+ items: [
+ { what: 'Prices and latency', text: 'Measured per provider and model over the last seven days.', href: '/stats' },
+ { what: 'Create a project and an app', text: 'Sign in with your OpenVibe account; the sandbox is open to every project.', href: PROJECTS_URL, to: 'codes' },
+ { what: 'API reference', text: 'The OpenAPI document generated from the published contracts.', href: API_DOCS_URL, to: 'codes' },
+ { what: 'Developer guide', text: 'Tokens, limits, billing and the sandbox, in full.', href: GUIDE },
+ ],
+ });
+}
+
+function renderHome({ siteUrl }) {
+ const navLinks = [{ label: 'Prices', href: '/stats' }, { label: 'Guide', href: GUIDE }];
+ const nav = { service: 'ai', apiBase: NETWORK_URL, links: navLinks, history: { type: 'page', title: SITE_NAME } };
+ const footer = { service: 'ai', variant: 'full', mount: '#ov-footer', brandName: SITE_NAME };
+ return shell.page({
+ name: SITE_NAME, service: 'ai', lang: 'en',
+ title: `${SITE_NAME}: many models, one API, every answer explained`,
+ siteName: SITE_NAME,
+ description: DESCRIPTION,
+ summary: DESCRIPTION,
+ canonical: `${siteUrl}/`,
+ robots: 'index, follow',
+ navbar: nav, footer, home: '/', navLinks,
+ head: [
+ appIcon.headTags({ site: 'ai' }),
+ `<link rel="stylesheet" href="${ovServe.url(showcase.STYLESHEET)}">`,
+ ].join('\n'),
+ body: `<div id="navbar-mount"></div>
+<main id="main" class="page">
+${sections({ siteUrl })}
+</main>`,
+ });
+}
+
+function llmsTxt({ siteUrl }) {
+ return [
+ `# ${SITE_NAME}`,
+ '',
+ `> ${DESCRIPTION}`,
+ '',
+ `- Home: ${siteUrl}/`,
+ `- Prices and measured latency per provider and model: ${siteUrl}/stats`,
+ `- Developer guide (tokens, limits, billing, sandbox): ${GUIDE}`,
+ `- API reference (OpenAPI, generated from the contracts): ${API_DOCS_URL}`,
+ `- Create a project and an app: ${PROJECTS_URL}`,
+ `- Source (AGPL-3.0): ${SOURCE}`,
+ '',
+ '## Developer apps',
+ '',
+ '- Token: POST https://openvibe.network/oauth/token with grant_type=client_credentials, your app\'s client id and secret, audience=openvibe.ai (five minutes).',
+ `- Operations: POST ${siteUrl}/api/v1/chat, /generate, /summarize, /classify, /extract, /embed; GET ${siteUrl}/api/v1/runs/:id for your own runs.`,
+ '- Every run response carries explain: the objective, the model selected and every candidate with its estimated cost, latency and excluded reason.',
+ '- Sandbox apps run on free and local capacity only; runs, usage and the free allowance are per project; a person\'s own provider key is never used for an app.',
+ '',
+ ].join('\n');
+}
+
+module.exports = { renderHome, llmsTxt, HOME_CSP, SITE_NAME };
@@ -487,17 +487,18 @@ t.test('sign out revokes the session', async () => {
@@ -487,17 +487,18 @@ t.test('sign out revokes the session', async () => {
for (const s of SECRETS) assert.ok(!JSON.stringify(await h.db.prepare('SELECT * FROM audit_log').all()).includes(s), 'no secret in the audit log');
for (const s of SECRETS) assert.ok(!JSON.stringify(await h.db.prepare('SELECT * FROM audit_log').all()).includes(s), 'no secret in the audit log');
});
});
-t.test('robots.txt keeps crawlers out of the console and sign-in; there is no sitemap', async () => {
+t.test('robots.txt keeps crawlers out of the console and sign-in; the sitemap lists only public pages', async () => {
const r = await request(h.base, 'GET', '/robots.txt');
const r = await request(h.base, 'GET', '/robots.txt');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.ok(r.text.includes('Disallow: /console') && r.text.includes('Disallow: /auth/'));
assert.ok(r.text.includes('Disallow: /console') && r.text.includes('Disallow: /auth/'));
- assert.ok(!/sitemap/i.test(r.text));
- assert.strictEqual((await request(h.base, 'GET', '/sitemap.xml')).status, 404);
+ const map = await request(h.base, 'GET', '/sitemap.xml');
+ assert.strictEqual(map.status, 200);
+ assert.ok(!/\/console|\/auth\//.test(map.text), 'the console and sign-in are never in the sitemap');
assert.ok((await request(h.base, 'GET', '/')).text.includes('/console'));
assert.ok((await request(h.base, 'GET', '/')).text.includes('/console'));
});
});
t.test('the nginx vhost proxies /console, /auth/ and /robots.txt with the client address from $remote_addr only', () => {
t.test('the nginx vhost proxies /console, /auth/ and /robots.txt with the client address from $remote_addr only', () => {
- const conf = fs.readFileSync(path.join(__dirname, '..', 'deploy', 'nginx', 'ai.openvibe.network.conf'), 'utf8');
+ const conf = fs.readFileSync(path.join(__dirname, '..', 'deploy', 'nginx', 'ai.openvibe.services.conf'), 'utf8');
const active = conf.split('\n').filter((l) => !/^\s*#/.test(l)).join('\n');
const active = conf.split('\n').filter((l) => !/^\s*#/.test(l)).join('\n');
for (const loc of ['location /console', 'location /auth/', 'location = /robots.txt']) {
for (const loc of ['location /console', 'location /auth/', 'location = /robots.txt']) {
const i = active.indexOf(`${loc} {`);
const i = active.indexOf(`${loc} {`);
new file mode 100644
new file mode 100644
@@ -0,0 +1,98 @@
@@ -0,0 +1,98 @@
+'use strict';
+// The public home (plan T6: AI as a developer product). ai.openvibe.services 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. The vhosts: the developer-app routes are the only
+// API the public vhost proxies, and ai.openvibe.network is a 301 to the product origin.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { boot, request, suite } = require('./helpers');
+const { HOME_CSP } = require('../server/home');
+
+const SITE = 'https://ai.openvibe.services';
+const BROWSER = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8';
+const t = suite('home');
+let h;
+
+t.test('boot with the public origin as BASE_URL', async () => {
+ h = await boot({ env: { BASE_URL: SITE } });
+});
+
+t.test('GET / as a browser: the home page, its CSP and a private cache', async () => {
+ const r = await request(h.base, 'GET', '/', { headers: { Accept: BROWSER } });
+ assert.strictEqual(r.status, 200);
+ assert.match(r.headers.get('content-type'), /^text\/html/);
+ assert.strictEqual(r.headers.get('content-security-policy'), HOME_CSP);
+ assert.strictEqual(r.headers.get('cache-control'), 'private, max-age=300', 'Cloudflare ignores Vary: Accept');
+ assert.match(r.headers.get('vary'), /Accept/);
+ assert.strictEqual((r.text.match(/<h1\b/g) || []).length, 1, 'exactly one h1');
+ assert.ok(r.text.includes('Many models, one API.'), 'the hero');
+ assert.ok(r.text.includes(`<link rel="canonical" href="${SITE}/">`), 'the canonical public origin');
+ assert.ok(r.text.includes(`${SITE}/api/v1/chat`), 'the examples call the public origin');
+ assert.ok(r.text.includes('https://openvibe.codes/projects'), 'where a project and an app are made');
+ assert.ok(!/ai\.openvibe\.network/.test(r.text), 'never the old name');
+});
+
+t.test('GET / for curl and API clients: the text index, unchanged in kind', async () => {
+ for (const accept of [undefined, '*/*', 'application/json', 'text/plain']) {
+ const r = await request(h.base, 'GET', '/', { headers: accept ? { Accept: accept } : {} });
+ assert.strictEqual(r.status, 200, String(accept));
+ assert.match(r.headers.get('content-type'), /^text\/plain/, String(accept));
+ assert.ok(r.text.startsWith('OpenVibe.AI: providers, models, routing'), String(accept));
+ assert.ok(r.text.includes('ai.app.run') && r.text.includes('/console'), String(accept));
+ }
+});
+
+t.test('the home links the pinned Frame stylesheet, and /shared serves it', async () => {
+ const r = await request(h.base, 'GET', '/', { headers: { Accept: BROWSER } });
+ const href = (r.text.match(/href="(\/shared\/showcase\.css\?v=[0-9a-f]+)"/) || [])[1];
+ assert.ok(href, 'the showcase stylesheet, content-addressed');
+ const css = await request(h.base, 'GET', href);
+ assert.strictEqual(css.status, 200);
+ assert.match(css.headers.get('content-type'), /text\/css/);
+ assert.match(css.headers.get('cache-control'), /immutable/);
+});
+
+t.test('robots.txt, sitemap.xml and llms.txt name the public origin', async () => {
+ const robots = await request(h.base, 'GET', '/robots.txt');
+ assert.strictEqual(robots.status, 200);
+ for (const line of ['Allow: /$', 'Allow: /stats', 'Disallow: /console', 'Disallow: /auth/', 'Disallow: /api/', `Sitemap: ${SITE}/sitemap.xml`]) assert.ok(robots.text.includes(line), line);
+ const map = await request(h.base, 'GET', '/sitemap.xml');
+ assert.strictEqual(map.status, 200);
+ assert.match(map.headers.get('content-type'), /xml/);
+ assert.deepStrictEqual([...map.text.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]), [`${SITE}/`, `${SITE}/stats`]);
+ const llms = await request(h.base, 'GET', '/llms.txt');
+ assert.strictEqual(llms.status, 200);
+ assert.match(llms.headers.get('content-type'), /^text\/plain/);
+ assert.ok(llms.text.startsWith('# OpenVibe.AI'));
+ assert.ok(llms.text.includes(`${SITE}/stats`) && llms.text.includes(`POST ${SITE}/api/v1/chat`));
+});
+
+const active = (file) => fs.readFileSync(path.join(__dirname, '..', 'deploy', 'nginx', file), 'utf8').split('\n').filter((l) => !/^\s*#/.test(l)).join('\n');
+
+t.test('the public vhost proxies the home, its files and exactly the developer-app API routes', () => {
+ const conf = active('ai.openvibe.services.conf');
+ for (const loc of ['location = / {', 'location = /stats', 'location = /sitemap.xml', 'location = /llms.txt', 'location /shared/']) assert.ok(conf.includes(loc), loc);
+ const ops = conf.indexOf('location ~ ^/api/v1/(chat|generate|summarize|classify|extract|embed)$ {');
+ const run = conf.indexOf('location ~ ^/api/v1/runs/[A-Za-z0-9_-]+$ {');
+ assert.ok(ops >= 0 && run >= 0, 'the six operations and the run read');
+ for (const [i, verb] of [[ops, 'POST'], [run, 'GET']]) {
+ const block = conf.slice(i, conf.indexOf('\n }', i));
+ assert.ok(block.includes(`limit_except ${verb} { deny all; }`), verb);
+ assert.ok(block.includes('limit_req zone=ovai_apps'), 'rate-limited per address');
+ for (const hdr of ['X-Real-IP $remote_addr', 'X-Forwarded-For $remote_addr', 'CF-Connecting-IP $remote_addr']) assert.ok(block.includes(hdr), hdr);
+ }
+ const rest = conf.indexOf('location /api/ {');
+ assert.ok(conf.slice(rest, conf.indexOf('}', rest)).includes('return 403;'), 'the rest of the API stays host-local');
+ assert.ok(!conf.includes('$proxy_add_x_forwarded_for'));
+});
+
+t.test('ai.openvibe.network answers 301 to the same path on ai.openvibe.services, and proxies nothing', () => {
+ const conf = active('ai.openvibe.network.conf');
+ assert.ok(!conf.includes('proxy_pass'), 'nothing is served under the old name');
+ assert.strictEqual((conf.match(/return 301 https:\/\/ai\.openvibe\.services\$request_uri;/g) || []).length, 2, 'HTTP and HTTPS');
+});
+
+t.test('shutdown', async () => { await h.stop(); });
+
+t.run();