openvibers/openvibe.connect README.md

README.md

main 14.3 KB · 228 lines · markdown

1# OpenVibe Connect
2
3OpenVibe 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
11settings, no open ports, no app to install: one file and Link.
12
13Live 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
171. 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.
192. 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
263. Open `https://agents--you.openvibe.link` anywhere, on a phone over 5G included, and sign in.
27
28The client opens one outgoing connection, so it works behind any router or hotspot, and it reconnects by itself. It
29only ever connects to `localhost` on the port you gave it.
30
31### From a computer with OpenVibe Link (no key)
32
33A computer you added on [openvibe.link/machines](https://openvibe.link/machines) runs OpenVibe Link and has its own
34OpenVibe 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
37openvibe-link share --share shr_… # serve it now
38sudo openvibe-link share add --share shr_… # or keep it up with the Link service
39```
40
41There 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
43seconds, one use). It sends a fresh node token before the old one runs out, so a computer removed on OpenVibe stops
44serving within about five minutes. While a computer serves a share, the share's key does not work; "Use a connect key
45instead" 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
49On 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,
51kept for up to 72 hours and handed to your app, at that path, as soon as your computer is connected, so a laptop that
52sleeps 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
90Public 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
93Anyone can report a share at [/report](https://connect.openvibe.link/report) (CON-9 v0): its address, what is
94wrong and what they saw. OpenVibe staff read the open reports at `/admin/reports` and can close a share; its computer
95is disconnected and its address then says it was closed after a report. A report keeps the reporter's account only
96when they were signed in, never an address.
97
98Connect also checks each share itself, once a day, a little after its computer connects (CON-9, the signed probe). It
99reads the share's front page through the tunnel with an `x-openvibe-probe: t=<unix>,v1=<hmac>` header, so your app can
100tell the request is Connect's, and looks for the signs of a credential trap: a password, card or wallet-phrase field on a
101page that names a brand (PayPal, Google, a bank…), or one with an urgent "verify your account" story. A page that looks
102like one becomes an automatic report for staff; Connect never closes a share by itself here, and nothing of the page is
103kept 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
107A program can call a share without signing in: on the share's page, make an **API key** (a name and how many requests a
108minute it may make, 60 by default), and send it as `Authorization: Bearer ovsk_…`. That is how a model on your
109computer (Ollama, llama.cpp's server, anything OpenAI-compatible) or an API of yours is called from a script, an editor or
110another app:
111
112```
113curl -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
138If 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
1411. 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`.
1452. 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.
1473. `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.
1504. `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
153The 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
155share's page, under "Use it from Claude and other MCP clients") or the share closing stops every token. Only hashes are
156kept. Cases: `test/mcp-oauth.test.js`.
157
158## API
159
160With 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
175A 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```
185browser ──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
200delivery log and Events' deliveries (CON-15).
201- `public/ov-connect.js` — the client.
202
203The v0 transport is a WebSocket on 443 through Cloudflare (ADR-057's fallback path), so nothing needs a UDP port or a
204record that shows the origin. The QUIC edge, TCP/UDP shares, custom domains, the private mesh and durable webhooks
205are the next steps (CON-3, CON-7, CON-11 to CON-15).
206
207## Run it
208
209```bash
210npm install
211npm run dev # PGlite under data/pglite, http://localhost:5240
212npm test # every file on PGlite
213npm run test:pg # the same on the PostgreSQL + PgBouncer containers
214```
215
216Environment: `.env.example`. Production: `/etc/openvibe/connect.env`, unit `openvibe-connect`, vhost
217`deploy/nginx/connect.openvibe.website.conf`.
218
219## License
220
221AGPL-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