openvibers/openvibe.connect AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Connect |
| 2 | |
| 3 | ## Project overview |
| 4 | |
| 5 | "Your computer, on the web, privately." Connect gives one port on a person's computer its own HTTPS |
| 6 | address, `https://<name>--<you>.openvibe.link`, visible only to the people the owner picks, signed in |
| 7 | with OpenVibe. A small Link client opens one outgoing WebSocket (the tunnel), so no router setting or |
| 8 | open port is needed; Connect terminates TLS, runs the gate and proxies to the app through the tunnel. |
| 9 | Node.js/Express + `ws`, PostgreSQL, CommonJS, **no build step**. |
| 10 | |
| 11 | - Canonical origin `https://connect.openvibe.link`; the old `*.openvibe.website` addresses redirect. |
| 12 | Share hosts are `<label>--<user>.openvibe.link` (a regex vhost). |
| 13 | - Port `5240`, unit `openvibe-connect.service`, release root `/opt/openvibe.connect`. |
| 14 | - Design: ADR-057 in OpenVibe.Contracts; CON-4/CON-16 are the Link and MCP decisions. |
| 15 | |
| 16 | ## What it owns |
| 17 | |
| 18 | - Shares (a port, a name, an audience rule), their aliases and their lifecycle; the `shr_` id and the |
| 19 | one-time `ovc_` connect key. |
| 20 | - The gate (who may open a share: owner, named people, signed-in, public), the tunnel registry, the |
| 21 | edge proxy (HTTP, WebSocket, SSE, 100 MB bodies) and the inspector. |
| 22 | - A computer as an **OpenVibe Link** endpoint (`/api/v1/link/*`, node tokens, `openvibe-link share`), |
| 23 | the `link.egress` relay and the party desktop grant. |
| 24 | - Project webhooks that wait for a computer (`server/hooks.js`), API keys for programs (`ovk_`/…, with |
| 25 | quotas) and the MCP OAuth 2.1 authorization server for shares that are MCP servers. |
| 26 | - Pages, `/ov-connect.js` (the client, served as-is), `/openapi.json`, `/llms.txt`, agent descriptor, |
| 27 | `robots.txt`, `sitemap.xml`, `/release.json`, health/ready, `/metrics` (loopback only). |
| 28 | - Its own PostgreSQL database (ADR-035; migrations applied at boot) and the `connect.*` events. |
| 29 | |
| 30 | Not owned: the tunnel's app (the person's own server), the Link machine/plugins (OpenVibe.Link), |
| 31 | identity (Network), the sites Space builds and hosts (Space). |
| 32 | |
| 33 | ## Who calls it, and whom it calls |
| 34 | |
| 35 | - Callers: people in the browser; the Link client on a person's computer (`/tunnel`); OpenVibe Link's |
| 36 | share commands; MCP clients (Claude and others) through the OAuth flow; other services through the |
| 37 | API with a Network service token and `connect.*`. |
| 38 | - Depends on: **Network** (SSO with PKCE, JWKS, node tokens for Link machines, |
| 39 | `identity.subject.resolve`), **Events** (outbox; account export/deletion), and the person's |
| 40 | computer over the tunnel. There is no Media/AI/Live dependency in the serving path. |
| 41 | - What the app behind a share sees: `Host: localhost:<port>`, `X-Forwarded-Host`, `X-Forwarded-Proto: |
| 42 | https`, `X-OpenVibe-User` and `X-OpenVibe-Subject` (set by the edge, never forwardable by a visitor), |
| 43 | the visitor's cookies for that share, WebSockets/SSE, bodies up to 100 MB. |
| 44 | |
| 45 | ## Repository map |
| 46 | |
| 47 | - **Entry:** [server/index.js](server/index.js) — `createApp()`, HTTP listen, `/tunnel` upgrade; |
| 48 | lifecycle closes tunnels, proxy, gate, keys and telemetry. |
| 49 | - **App:** [server/app.js](server/app.js) — the share-host branch comes **first** (gate → tunnel → |
| 50 | proxy), then `/`, `/shares/*`, `/grant`, `/api/v1/link/*`, `/api/v1/*`, `/ov-connect.js`, `/auth`, |
| 51 | `/internal/events`, machine endpoints, `/tunnel`. |
| 52 | - **Config/db:** [server/config.js](server/config.js), [server/db.js](server/db.js). |
| 53 | - **Shares/gate/tunnel:** [server/shares.js](server/shares.js), [server/edge/gate.js](server/edge/gate.js), |
| 54 | [server/edge/tunnels.js](server/edge/tunnels.js), [server/edge/proxy.js](server/edge/proxy.js), |
| 55 | [server/edge/inspector.js](server/edge/inspector.js), [server/edge/frames.js](server/edge/frames.js). |
| 56 | - **Link:** [server/auth/link-auth.js](server/auth/link-auth.js), [server/machines.js](server/machines.js), |
| 57 | [server/link-egress.js](server/link-egress.js), [server/http/link-api.js](server/http/link-api.js), |
| 58 | [server/party-grants.js](server/party-grants.js). |
| 59 | - **Programs:** [server/api-keys.js](server/api-keys.js), [server/oauth.js](server/oauth.js), |
| 60 | [server/http/oauth-routes.js](server/http/oauth-routes.js), [server/hooks.js](server/hooks.js), |
| 61 | [server/http/hooks.js](server/http/hooks.js). |
| 62 | - **Pages/render:** [server/http/pages.js](server/http/pages.js), [server/views/index.js](server/views/index.js), |
| 63 | [server/render/layout.js](server/render/layout.js), [public/js/connect.js](public/js/connect.js). |
| 64 | - **Client:** [public/ov-connect.js](public/ov-connect.js) — a single file, no install. |
| 65 | - **Deploy:** [deploy/systemd/openvibe-connect.service](deploy/systemd/openvibe-connect.service), |
| 66 | [deploy/nginx/connect.openvibe.website.conf](deploy/nginx/connect.openvibe.website.conf), |
| 67 | [docs/link-share-rollout.md](docs/link-share-rollout.md). |
| 68 | |
| 69 | ## Commands |
| 70 | |
| 71 | ```bash |
| 72 | npm install |
| 73 | npm run dev # NODE_ENV=development node server/index.js; PGlite under data/pglite |
| 74 | npm start # node server/index.js, http://127.0.0.1:5240 |
| 75 | npm test # test/run.js, temp PGlite, mock Network |
| 76 | npm run test:pg # same suite on PostgreSQL (CONNECT_TEST_STORE=pg) |
| 77 | npm test -- hooks # filter by file name; --strict makes a skip fail |
| 78 | ov test # the harness's sharded run of what the change reaches |
| 79 | ``` |
| 80 | |
| 81 | End-to-end from a computer: `node ov-connect.js --key ovc_… --port 8790`, or |
| 82 | `openvibe-link share --share shr_…` on a Link machine. |
| 83 | |
| 84 | ## Database and migrations |
| 85 | |
| 86 | Connect's own PostgreSQL database (ADR-035), PGlite in development/tests. Async through |
| 87 | `openvibe-sdk/db`; migrations `migrations/NNNN_*.sql` are applied at boot — never edit an applied one, |
| 88 | add a new number. Only hashes of API keys, tunnel tokens and OAuth tokens are stored; a webhook body |
| 89 | is sealed at rest with a key derived from the service session secret and is never readable from the |
| 90 | database by a reader. Rows that a share/computer no longer needs are removed at expiry. |
| 91 | |
| 92 | ## Auth |
| 93 | |
| 94 | - **People** sign in through Network SSO (PKCE, HttpOnly cookie). A share's rule decides whether a |
| 95 | signed-in person may open it; the edge then sets `X-OpenVibe-User`/`X-OpenVibe-Subject`. The |
| 96 | visitor's own `Authorization` is consumed at the edge, never forwarded. |
| 97 | - **Link machines** prove themselves twice at every connection: a Network node token |
| 98 | (`link:lnk_…`, audience `openvibe.link`, 5 minutes) and a one-use tunnel token from Connect (60 s) |
| 99 | for that share. A computer removed on Network stops serving within about five minutes because the |
| 100 | node token is refreshed and checked. |
| 101 | - **MCP clients** use OAuth 2.1: `401` with RFC 9728 resource metadata, RFC 8414 server metadata, |
| 102 | RFC 7591 dynamic registration (public clients + PKCE only; https or loopback redirect URIs), |
| 103 | RFC 8707 `resource`, a 60-second one-use code and rotating `ovat_`/`ovrt_` tokens bound to the |
| 104 | share. Unknown clients/redirects get a page, never a redirect. |
| 105 | - **API keys** for programs are shown once, stored hashed and carry quotas; a policy change, the owner |
| 106 | signing the app out or closing the share stops every token. |
| 107 | |
| 108 | ## Frontend loading model |
| 109 | |
| 110 | Server-rendered pages and plain forms; the only page script is [public/js/connect.js](public/js/connect.js). |
| 111 | `public/ov-connect.js` is the downloadable client and is served as-is. There is no SPA, bundler or |
| 112 | transpiler; `openvibe-shared` supplies the Frame, legal/SEO helpers and `/shared/*` from the pinned |
| 113 | node_modules. |
| 114 | |
| 115 | ## Deploy |
| 116 | |
| 117 | Unit [deploy/systemd/openvibe-connect.service](deploy/systemd/openvibe-connect.service) runs |
| 118 | `/opt/openvibe.connect/server/index.js` on `127.0.0.1:5240`, env `/etc/openvibe/connect.env` (0600). |
| 119 | The vhost is the **combined** file |
| 120 | [deploy/nginx/connect.openvibe.website.conf](deploy/nginx/connect.openvibe.website.conf), installed |
| 121 | with `sudo ov-vhost-install …` and reloaded by that installer — it keeps both Website servers, the |
| 122 | exact Link control host and the `<label>--<user>.openvibe.link` regex, with HTTP/1.1 Upgrade, |
| 123 | streaming and no buffering. The repository does not document an `ovhost deploy connect` command; use |
| 124 | the host's normal service environment and the staged rollout in |
| 125 | [docs/link-share-rollout.md](docs/link-share-rollout.md). Do not install a second copy of the vhost |
| 126 | (its maps and rate-limit zones are declared once) and do not replace the Website certificate. |
| 127 | |
| 128 | ## Tests |
| 129 | |
| 130 | `npm test` runs each `test/*.test.js` in its own process with a temp PGlite database, an in-process |
| 131 | Network mock and Chat/Live/Video fixtures. It covers share creation and the gate, both share aliases, |
| 132 | tunnel auth and revocation, the Link API and egress relay, API key quotas, MCP OAuth (metadata, |
| 133 | registration, PKCE, rotation), webhooks and their sealed bodies, the pages/llms/openapi, the nginx Link |
| 134 | rules and owner settings. `ov test` runs the same. |
| 135 | |
| 136 | ## Conventions |
| 137 | |
| 138 | - CommonJS, 4-space indent, single quotes, semicolons; factories with injectable config/db/clock/fetch/ |
| 139 | log; the edge has its own modules and must not import page code. |
| 140 | - Errors are problem+json on the API and small pages elsewhere; never echo an upstream app's body on a |
| 141 | proxy error. |
| 142 | - Hostnames and origins are validated exactly: a share is `<label>--<user>.openvibe.link` |
| 143 | (lowercase letters/digits/hyphens; a Space site name can never contain `--`). |
| 144 | - Secrets (connect keys, tunnel/OAuth tokens, webhook bodies) are shown once or sealed; never logged. |
| 145 | |
| 146 | ## Pitfalls |
| 147 | |
| 148 | - **Order matters in the app:** the share-host branch must stay before every ordinary route, or a |
| 149 | share can reach Connect's own pages/API. |
| 150 | - **The client only ever connects to `localhost`** on the port it was given; do not add a remote |
| 151 | target. |
| 152 | - **A share key stops working while a Link computer serves the share**; "Use a connect key instead" |
| 153 | lets the computer go and shows a new key once. |
| 154 | - **The tunnel token is one-use and 60 seconds**; the node token is 5 minutes and refreshed. Do not |
| 155 | cache or log either. |
| 156 | - **`X-OpenVibe-User`/`X-OpenVibe-Subject` come from the edge only.** A visitor-supplied value must be |
| 157 | stripped. |
| 158 | - **OAuth errors that must not redirect** (unknown client, bad redirect URI) render a page; never |
| 159 | redirect. |
| 160 | - **One combined vhost only**; a second install duplicates maps/zones and breaks nginx. |
| 161 | - **100 MB bodies and WebSockets are part of the product**; a proxy change that buffers or caps them |
| 162 | lower is a regression. |
| 163 | - **Store a webhook body sealed** and remove it at expiry; a plaintext copy is a bug. |
| 164 | |
| 165 | ## Documentation |
| 166 | |
| 167 | - [README.md](README.md) — what a share is, the gate, Link, MCP OAuth, API and run instructions. |
| 168 | - [docs/link-share-rollout.md](docs/link-share-rollout.md) — the staged host rollout and commands. |
| 169 | - ADR-057 (Link shares) in OpenVibe.Contracts; CON-4/CON-16 track the work. |
| 170 |