openvibers/openvibe.connect README.md
README.md
| 1 | # OpenVibe Connect |
| 2 | |
| 3 | OpenVibe Connect lives at `connect.openvibe.link`; the old `.openvibe.website` addresses still work and redirect. See |
| 4 | [the staged rollout and host commands](docs/link-share-rollout.md) for how the move was deployed. |
| 5 | |
| 6 | **Live site:** https://connect.openvibe.link · **API docs:** https://openvibe.services · **For AI:** https://connect.openvibe.link/llms.txt |
| 7 | **Run it:** `npm ci && npm run dev` (Node 22 or newer) · **Tests:** `npm test` |
| 8 | |
| 9 | **Your computer, on the web, privately.** Share one port on your computer at its own HTTPS address, |
| 10 | `https://<name>--<you>.openvibe.link`, and only the people you pick can open it, signed in with OpenVibe. No router |
| 11 | settings, no open ports, no app to install: one file and Link. |
| 12 | |
| 13 | Live at **[connect.openvibe.link](https://connect.openvibe.link)**. Connect shares ports through Link; [OpenVibe.Space](https://openvibe.space) builds and hosts sites. |
| 14 | |
| 15 | ## Use it |
| 16 | |
| 17 | 1. Sign in at connect.openvibe.link and make a share: a name (`agents`), the port on your computer (`8790`), and |
| 18 | who can open it. |
| 19 | 2. On your computer (Link 22 or newer): |
| 20 | |
| 21 | ```bash |
| 22 | curl -fsSLO https://connect.openvibe.link/ov-connect.js |
| 23 | node ov-connect.js --key ovc_… --port 8790 |
| 24 | ``` |
| 25 | |
| 26 | 3. Open `https://agents--you.openvibe.link` anywhere, on a phone over 5G included, and sign in. |
| 27 | |
| 28 | The client opens one outgoing connection, so it works behind any router or hotspot, and it reconnects by itself. It |
| 29 | only ever connects to `localhost` on the port you gave it. |
| 30 | |
| 31 | ### From a computer with OpenVibe Link (no key) |
| 32 | |
| 33 | A computer you added on [openvibe.link/machines](https://openvibe.link/machines) runs OpenVibe Link and has its own |
| 34 | OpenVibe sign-in. On the share's page, pick it under "Or serve it from one of your computers", then on that computer: |
| 35 | |
| 36 | ```bash |
| 37 | openvibe-link share --share shr_… # serve it now |
| 38 | sudo openvibe-link share add --share shr_… # or keep it up with the Link service |
| 39 | ``` |
| 40 | |
| 41 | There is no key to copy. The computer proves itself twice at every connection: with its Network node token (subject |
| 42 | `link:lnk_…`, audience `openvibe.link`, 5 minutes) and with a tunnel token Connect gives it for that one share (60 |
| 43 | seconds, one use). It sends a fresh node token before the old one runs out, so a computer removed on OpenVibe stops |
| 44 | serving within about five minutes. While a computer serves a share, the share's key does not work; "Use a connect key |
| 45 | instead" lets the computer go and shows a new key once (ADR-057 decision 4, CON-4). |
| 46 | |
| 47 | ### Webhooks that wait for your computer |
| 48 | |
| 49 | On a share's page, "Make a webhook address" gives you `https://connect.openvibe.link/h/hk_…` and a path on your app |
| 50 | (`/webhooks/github`). Give that address to GitHub, Stripe or anything else that sends webhooks. Each webhook is sealed, |
| 51 | kept for up to 72 hours and handed to your app, at that path, as soon as your computer is connected, so a laptop that |
| 52 | sleeps misses nothing (CON-15, ADR-057 decision 7): |
| 53 | |
| 54 | - The address takes POST, PUT and PATCH, up to 1 MB and 120 a minute, and answers `202` at once. A share holds at most |
| 55 | 1,000 waiting webhooks; after that the address answers `429` and the provider retries. |
| 56 | - Connect keeps the method, the provider's headers minus hop-by-hop, credential and forwarding headers, and the body. |
| 57 | The body is sealed at rest with a key derived from the service's session secret and is never readable from the |
| 58 | database alone. It publishes only a pointer, `connect.hook.received`, to OpenVibe.Events, in the same transaction. |
| 59 | - The moment the computer connects, Connect hands it every webhook that waited, oldest first. Events also delivers the |
| 60 | pointer back to Connect's own subscription, retried for about 72 hours, which finds it settled. Connect hands the |
| 61 | webhook to your app with `x-openvibe-hook`, `x-openvibe-event-id` (dedupe on it) and `x-openvibe-received-at` (when |
| 62 | the provider's webhook first arrived). Your app's answer below 500 settles it, and its body is dropped. While the |
| 63 | computer is offline, or the app answers 5xx, Connect says 503 and Events tries again. |
| 64 | - The share's page and the API's delivery log show what arrived, what the app answered and when (never a body). |
| 65 | - Nobody signs in to send a webhook, so check the provider's own signature in your app. |
| 66 | |
| 67 | | Call | What | |
| 68 | |---|---| |
| 69 | | `GET /api/v1/shares/:id/hooks` | its webhook addresses, with `received`, `delivered`, `waiting` and the last deliveries | |
| 70 | | `POST /api/v1/shares/:id/hooks` `{ path, label? }` | a new address → `201 { hook }` (at most 10 a share) | |
| 71 | | `DELETE /api/v1/shares/:id/hooks/:hook` | remove it; webhooks still waiting are dropped | |
| 72 | | `GET /api/v1/shares/:id/hooks/:hook/deliveries` | the delivery log: method, path, status, error and when | |
| 73 | |
| 74 | ## What your app sees |
| 75 | |
| 76 | - `Host: localhost:<port>`; the share's address in `X-Forwarded-Host`; `X-Forwarded-Proto: https`. |
| 77 | - Who the visitor is: `X-OpenVibe-User` (their username) and `X-OpenVibe-Subject` (their OpenVibe subject id). |
| 78 | A visitor cannot send these themselves. |
| 79 | - The visitor's cookies for your share's address, never the gate's own cookie. |
| 80 | - WebSockets and server-sent events work. Request bodies up to 100 MB. |
| 81 | |
| 82 | ## Who can open a share |
| 83 | |
| 84 | | Mode | Who | |
| 85 | |---|---| |
| 86 | | `owner` (default) | only you | |
| 87 | | `named` | you and the OpenVibe usernames you list | |
| 88 | | `signin` | anyone signed in to OpenVibe (OpenVibe staff only until the abuse check is in, as with Space's public publishing) | |
| 89 | |
| 90 | Public shares (no sign-in) come once the abuse check and openvibe.link's entry in the Public Suffix List are in |
| 91 | (ADR-057, CON-9 and CON-21). Changing who can open a share signs everyone out of it. |
| 92 | |
| 93 | Anyone can report a share at [/report](https://connect.openvibe.link/report) (CON-9 v0): its address, what is |
| 94 | wrong and what they saw. OpenVibe staff read the open reports at `/admin/reports` and can close a share; its computer |
| 95 | is disconnected and its address then says it was closed after a report. A report keeps the reporter's account only |
| 96 | when they were signed in, never an address. |
| 97 | |
| 98 | Connect also checks each share itself, once a day, a little after its computer connects (CON-9, the signed probe). It |
| 99 | reads the share's front page through the tunnel with an `x-openvibe-probe: t=<unix>,v1=<hmac>` header, so your app can |
| 100 | tell the request is Connect's, and looks for the signs of a credential trap: a password, card or wallet-phrase field on a |
| 101 | page that names a brand (PayPal, Google, a bank…), or one with an urgent "verify your account" story. A page that looks |
| 102 | like one becomes an automatic report for staff; Connect never closes a share by itself here, and nothing of the page is |
| 103 | kept beyond the report's one line. An ordinary sign-in page for your own app is not reported. |
| 104 | |
| 105 | ### API keys for programs (CON-16) |
| 106 | |
| 107 | A program can call a share without signing in: on the share's page, make an **API key** (a name and how many requests a |
| 108 | minute it may make, 60 by default), and send it as `Authorization: Bearer ovsk_…`. That is how a model on your |
| 109 | computer (Ollama, llama.cpp's server, anything OpenAI-compatible) or an API of yours is called from a script, an editor or |
| 110 | another app: |
| 111 | |
| 112 | ``` |
| 113 | curl -H "Authorization: Bearer ovsk_…" https://llm--you.openvibe.link/v1/models |
| 114 | ``` |
| 115 | |
| 116 | - **A key opens the share's API, never its pages.** An HTML answer to a key is refused at the edge (406), so a key |
| 117 | posted in public cannot turn a share into a public website past the sign-in gate. A browser still signs in. |
| 118 | - **Your app never sees the key.** The edge drops it; your app sees `X-OpenVibe-Subject: key:<sak_…>` and |
| 119 | `X-OpenVibe-User: key: <name>`. |
| 120 | - **A wrong, revoked or other share's key is 401**, never a redirect to sign in. Over its rate a key gets 429 with |
| 121 | `Retry-After: 60`; the share's own limits still apply. |
| 122 | - Only the key's hash is kept and it is shown once. At most ten live keys a share; revoking one stops it at once, and |
| 123 | closing the share stops them all. WebSocket upgrades take a key too. |
| 124 | - **A daily quota (v1).** A key may also have a number of requests a day (`per_day`, optional), counted in UTC days and |
| 125 | kept in the database, so a restart does not reset it. Over it, a key gets 429 `key_daily_quota` with `Retry-After` |
| 126 | until midnight UTC. |
| 127 | - **What each key did today (v1).** The share page and the list show each key's requests today (against its quota) |
| 128 | and what it used: the MCP tools it called (JSON-RPC `tools/call`, by name, a batch counting each) and the models it |
| 129 | asked for (an OpenAI- or Ollama-style `"model"` on a model path such as `/v1/chat/completions` or `/api/generate`). |
| 130 | The edge reads the first 64 KB of a key's JSON POST for those names; names and counts are kept, never arguments, |
| 131 | prompts or bodies. |
| 132 | - API: `GET`, `POST /api/v1/shares/:id/api-keys` (`{ name, per_minute, per_day }` → `{ api_key, key }`, the key once; |
| 133 | the list adds `today` and `used: [{ what, n }]`) and `DELETE /api/v1/shares/:id/api-keys/:sak_id`. Cases: |
| 134 | `test/api-keys.test.js`, `test/api-key-quotas.test.js`. |
| 135 | |
| 136 | ### Claude and other MCP clients (CON-16, MCP OAuth 2.1) |
| 137 | |
| 138 | If what you share is an MCP server, its address works as a connector URL in Claude or any MCP client |
| 139 | (`https://tools--you.openvibe.link/mcp`). The client signs the person in with OpenVibe, with no key to paste: |
| 140 | |
| 141 | 1. The share answers a program that has no credentials `401` with |
| 142 | `WWW-Authenticate: Bearer resource_metadata="https://<share>/.well-known/oauth-protected-resource"`. That document |
| 143 | (RFC 9728) names Connect as the authorization server. Connect's metadata (RFC 8414) is at |
| 144 | `/.well-known/oauth-authorization-server`. |
| 145 | 2. The client registers itself (`POST /oauth/register`, RFC 7591). Only public clients with PKCE are accepted, and their |
| 146 | redirect URIs must be https, or http on a loopback address for a desktop app. |
| 147 | 3. `GET /oauth/authorize` with `resource=<the share's address>` (RFC 8707) and an S256 challenge. The person signs in, |
| 148 | the share's own rule decides whether they may, and they consent on Connect. An unknown client or redirect URI gets |
| 149 | a page, never a redirect. |
| 150 | 4. `POST /oauth/token` swaps the code (60 seconds, used once) and verifier for an access token (`ovat_…`, an hour) bound |
| 151 | to that share, and a refresh token (`ovrt_…`, 30 days). Each refresh rotates both; the old pair stops. |
| 152 | |
| 153 | The edge takes `Authorization: Bearer ovat_…` as the person who consented. The app sees `X-OpenVibe-Subject` and |
| 154 | `X-OpenVibe-User` as for a signed-in visitor, never the token. A policy change, the owner signing the app out (on the |
| 155 | share's page, under "Use it from Claude and other MCP clients") or the share closing stops every token. Only hashes are |
| 156 | kept. Cases: `test/mcp-oauth.test.js`. |
| 157 | |
| 158 | ## API |
| 159 | |
| 160 | With a person's OpenVibe token as a Bearer (or the session on connect.openvibe.link): |
| 161 | |
| 162 | | Call | What | |
| 163 | |---|---| |
| 164 | | `GET /api/v1/shares` | your shares, with `connected` | |
| 165 | | `POST /api/v1/shares` `{ label, port, mode?, named?, expires_in? }` | make one → `201 { share, key }` (the key is in this answer only); `expires_in` (600 s to 30 days) closes it by itself | |
| 166 | | `GET /api/v1/shares/:id` | one share | |
| 167 | | `PATCH /api/v1/shares/:id` `{ mode?, named?, port?, note?, expires_in? }` | change it; `expires_in: null` keeps it open until you close it | |
| 168 | | `DELETE /api/v1/shares/:id` | close it (410 from then on) | |
| 169 | | `POST /api/v1/shares/:id/key` | a new key → `{ share, key }`; the connected computer is disconnected | |
| 170 | | `GET /api/v1/shares/:id/visits` | who opened it in the last 7 days | |
| 171 | | `GET /api/v1/shares/:id/requests` `?after=<seq>` | the last 100 requests through it, newest first (the inspector, below) | |
| 172 | | `GET /api/v1/machines` | your computers that can serve a share (OpenVibe Link, from Network) | |
| 173 | | `PUT /api/v1/shares/:id/node` `{ node: 'lnk_…' \| null }` | one of your computers serves it (the key stops working) → `{ share }`; `null` lets it go → `{ share, key }` | |
| 174 | |
| 175 | A computer, with its Network node token (audience `openvibe.link`) as a Bearer: |
| 176 | |
| 177 | | Call | What | |
| 178 | |---|---| |
| 179 | | `GET /api/v1/link/shares` | the open shares this computer serves: `id`, `host`, `url`, `local_port`, `mode` | |
| 180 | | `POST /api/v1/link/shares/:id/tunnel-token` | `{ tunnel_token, expires_in: 60, share }`, for one hello on `/tunnel`: `{ t: 'hello', v: 1, share, node_token, tunnel }`, then `{ t: 'reauth', node_token }` before the node token runs out | |
| 181 | |
| 182 | ## How it works |
| 183 | |
| 184 | ``` |
| 185 | browser ──https──▶ Cloudflare ──▶ nginx ──▶ Connect (5240) ══ WebSocket (ovconnect-ws/1) ══▶ ov-connect.js ──▶ localhost:<port> |
| 186 | gate: /grant → one-time code → __Host-ovc |
| 187 | ``` |
| 188 | |
| 189 | - `server/shares.js` — shares, names, keys (hashed), policies, counters. |
| 190 | - `server/edge/tunnels.js` — the computers' WebSockets (`/tunnel`), one per share; a newer one takes over. |
| 191 | - `server/edge/frames.js` — the wire format: JSON control messages, binary data frames per stream. |
| 192 | - `server/edge/gate.js` — the one-time codes and the signed host-only cookie. |
| 193 | - `server/edge/proxy.js` — a visitor's request on a share host: the gate, then the tunnel, streaming both ways. |
| 194 | - `server/edge/inspector.js` — the share page's Requests list (CON-8 v0): the last 100 requests per share, in memory |
| 195 | only, with the method, the path (query values shown as "…"), status, time, bytes and the visitor's name. No headers, |
| 196 | no bodies; a restart forgets them. |
| 197 | - `server/auth/node-auth.js` — a computer as OpenVibe Link: its node token, and the single-use tunnel tokens (CON-4). |
| 198 | - `server/machines.js` — the owner's computers, read from Network with the owner's own token. |
| 199 | - `server/hooks.js`, `server/http/hooks.js` — webhook addresses, the sealed kept webhooks, the 1,000-deep queue, the |
| 200 | delivery log and Events' deliveries (CON-15). |
| 201 | - `public/ov-connect.js` — the client. |
| 202 | |
| 203 | The v0 transport is a WebSocket on 443 through Cloudflare (ADR-057's fallback path), so nothing needs a UDP port or a |
| 204 | record that shows the origin. The QUIC edge, TCP/UDP shares, custom domains, the private mesh and durable webhooks |
| 205 | are the next steps (CON-3, CON-7, CON-11 to CON-15). |
| 206 | |
| 207 | ## Run it |
| 208 | |
| 209 | ```bash |
| 210 | npm install |
| 211 | npm run dev # PGlite under data/pglite, http://localhost:5240 |
| 212 | npm test # every file on PGlite |
| 213 | npm run test:pg # the same on the PostgreSQL + PgBouncer containers |
| 214 | ``` |
| 215 | |
| 216 | Environment: `.env.example`. Production: `/etc/openvibe/connect.env`, unit `openvibe-connect`, vhost |
| 217 | `deploy/nginx/connect.openvibe.website.conf`. |
| 218 | |
| 219 | ## License |
| 220 | |
| 221 | AGPL-3.0-only. The client (`public/ov-connect.js`) is part of this repository under the same license. |
| 222 | |
| 223 | <!-- versions:start --> |
| 224 | - openvibe-contracts: v0.419.0 |
| 225 | - openvibe-sdk: v0.166.0 |
| 226 | - openvibe-shared: v4.241.0 |
| 227 | <!-- versions:end --> |
| 228 |