openvibers/openvibe.connect Commits

Connect: read Link addresses everywhere (#72)

OP OpenVibers committed 5 days ago · 25d7adc665 · 31 files, +105 −93

Changes

ddiff --git a/.env.example b/.env.example
iindex 8731b43..09c3f49 100644
---- a/.env.example
++++ b/.env.example
@@@ -36,8 +36,9 @@ CONNECT_TWILIO_CLIENT_SECRET=
CONNECT_EVENTS_URL=
CONNECT_EVENTS_SECRET=
--# Shares. The session secret signs visitors' cookies on share hosts (required in production, 32+ characters).
--CONNECT_SHARE_DOMAIN=openvibe.website
++# Shares. The canonical domain is Link; the old openvibe.website aliases stay served (CONNECT_SHARE_DOMAINS).
++# The session secret signs visitors' cookies on share hosts (required in production, 32+ characters).
++CONNECT_SHARE_DOMAIN=openvibe.link
CONNECT_SESSION_SECRET=
CONNECT_SESSION_HOURS=12
CONNECT_SHARES_PER_PERSON=5
ddiff --git a/README.md b/README.md
iindex e8fda23..5020e0e 100644
---- a/README.md
++++ b/README.md
@@@ -1,22 +1,22 @@
# OpenVibe Connect
--Shares move to `.openvibe.link` beside their existing `.openvibe.website` aliases. See
--[the staged rollout and host commands](docs/link-share-rollout.md) before deployment.
++OpenVibe Connect lives at `connect.openvibe.link`; the old `.openvibe.website` addresses still work and redirect. See
++[the staged rollout and host commands](docs/link-share-rollout.md) for how the move was deployed.
**Your computer, on the web, privately.** Share one port on your computer at its own HTTPS address,
`https://<name>--<you>.openvibe.link`, and only the people you pick can open it, signed in with OpenVibe. No router
settings, no open ports, no app to install: one file and Node.
--Live at **[connect.openvibe.website](https://connect.openvibe.website)**. Connect shares ports through Link; [OpenVibe.Space](https://openvibe.space) builds and hosts sites.
++Live at **[connect.openvibe.link](https://connect.openvibe.link)**. Connect shares ports through Link; [OpenVibe.Space](https://openvibe.space) builds and hosts sites.
## Use it
--1. Sign in at connect.openvibe.website and make a share: a name (`agents`), the port on your computer (`8790`), and
++1. Sign in at connect.openvibe.link and make a share: a name (`agents`), the port on your computer (`8790`), and
who can open it.
2. On your computer (Node 22 or newer):
```bash
-- curl -fsSLO https://connect.openvibe.website/ov-connect.js
++ curl -fsSLO https://connect.openvibe.link/ov-connect.js
node ov-connect.js --key ovc_… --port 8790
```
@@@ -36,14 +36,14 @@ sudo openvibe-node share add --share shr_… # or keep it up with the Link
```
There is no key to copy. The computer proves itself twice at every connection: with its Network node token (subject
--`node:nod_…`, audience `openvibe.website`, 5 minutes) and with a tunnel token Connect gives it for that one share (60
++`node:nod_…`, audience `openvibe.link`, 5 minutes) and with a tunnel token Connect gives it for that one share (60
seconds, one use). It sends a fresh node token before the old one runs out, so a computer removed on OpenVibe stops
serving within about five minutes. While a computer serves a share, the share's key does not work; "Use a connect key
instead" lets the computer go and shows a new key once (ADR-057 decision 4, CON-4).
### Webhooks that wait for your computer
--On a share's page, "Make a webhook address" gives you `https://connect.openvibe.website/h/hk_…` and a path on your app
++On a share's page, "Make a webhook address" gives you `https://connect.openvibe.link/h/hk_…` and a path on your app
(`/webhooks/github`). Give that address to GitHub, Stripe or anything else that sends webhooks. Each webhook is sealed,
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
sleeps misses nothing (CON-15, ADR-057 decision 7):
@@@ -84,10 +84,10 @@ sleeps misses nothing (CON-15, ADR-057 decision 7):
| `named` | you and the OpenVibe usernames you list |
| `signin` | anyone signed in to OpenVibe (OpenVibe staff only until the abuse check is in, as with Space's public publishing) |
--Public shares (no sign-in) come once the abuse check and openvibe.website's entry in the Public Suffix List are in
++Public shares (no sign-in) come once the abuse check and openvibe.link's entry in the Public Suffix List are in
(ADR-057, CON-9 and CON-21). Changing who can open a share signs everyone out of it.
--Anyone can report a share at [/report](https://connect.openvibe.website/report) (CON-9 v0): its address, what is
++Anyone can report a share at [/report](https://connect.openvibe.link/report) (CON-9 v0): its address, what is
wrong and what they saw. OpenVibe staff read the open reports at `/admin/reports` and can close a share; its computer
is disconnected and its address then says it was closed after a report. A report keeps the reporter's account only
when they were signed in, never an address.
@@@ -107,7 +107,7 @@ computer (Ollama, llama.cpp's server, anything OpenAI-compatible) or an API of y
another app:
```
--curl -H "Authorization: Bearer ovsk_…" https://llm--you.openvibe.website/v1/models
++curl -H "Authorization: Bearer ovsk_…" https://llm--you.openvibe.link/v1/models
```
- **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
@@@ -133,7 +133,7 @@ curl -H "Authorization: Bearer ovsk_…" https://llm--you.openvibe.website/v1/mo
### Claude and other MCP clients (CON-16, MCP OAuth 2.1)
If what you share is an MCP server, its address works as a connector URL in Claude or any MCP client
--(`https://tools--you.openvibe.website/mcp`). The client signs the person in with OpenVibe, with no key to paste:
++(`https://tools--you.openvibe.link/mcp`). The client signs the person in with OpenVibe, with no key to paste:
1. The share answers a program that has no credentials `401` with
`WWW-Authenticate: Bearer resource_metadata="https://<share>/.well-known/oauth-protected-resource"`. That document
@@@ -154,7 +154,7 @@ kept. Cases: `test/mcp-oauth.test.js`.
## API
--With a person's OpenVibe token as a Bearer (or the session on connect.openvibe.website):
++With a person's OpenVibe token as a Bearer (or the session on connect.openvibe.link):
| Call | What |
|---|---|
@@@ -169,7 +169,7 @@ With a person's OpenVibe token as a Bearer (or the session on connect.openvibe.w
| `GET /api/v1/machines` | your computers that can serve a share (OpenVibe Link, from Network) |
| `PUT /api/v1/shares/:id/node` `{ node: 'nod_…' \| null }` | one of your computers serves it (the key stops working) → `{ share }`; `null` lets it go → `{ share, key }` |
--A computer, with its Network node token (audience `openvibe.website`) as a Bearer:
++A computer, with its Network node token (audience `openvibe.link`) as a Bearer:
| Call | What |
|---|---|
ddiff --git a/SECURITY.md b/SECURITY.md
iindex fff9837..c1431a3 100644
---- a/SECURITY.md
++++ b/SECURITY.md
@@@ -12,9 +12,11 @@ We reply within 7 days, keep you updated while we fix it, and credit you when th
## Scope
--Every OpenVibe service and site (connect.openvibe.website and every <name>--<user>.openvibe.website, openvibe.live, openvibe.network, openvibe.today, openvibe.tools,
--openvibe.media, openvibe.community, openvibe.games and the other openvibe.* domains) and every repository under
--github.com/OpenVibers. Machine-readable contact details are at `/.well-known/security.txt` on each site.
++Every OpenVibe service and site (connect.openvibe.link and every <name>--<user>.openvibe.link, the old
++connect.openvibe.website and every <name>--<user>.openvibe.website alias, openvibe.live, openvibe.network,
++openvibe.today, openvibe.tools, openvibe.media, openvibe.community, openvibe.games and the other openvibe.* domains)
++and every repository under github.com/OpenVibers. Machine-readable contact details are at `/.well-known/security.txt`
++on each site.
## What OpenVibe Connect holds
@@@ -30,7 +32,7 @@ github.com/OpenVibers. Machine-readable contact details are at `/.well-known/sec
- **The gate.** Every visitor signs in with OpenVibe and must be allowed by the share's policy (only the owner, the
owner and named people, or anyone signed in). There are no public shares: those wait for the abuse check and the
-- Public Suffix List entry for openvibe.website (ADR-057, CON-9 and CON-21). Because anyone can make an OpenVibe
++ Public Suffix List entry for openvibe.link (ADR-057, CON-9 and CON-21). Because anyone can make an OpenVibe
account, "anyone signed in" is open to OpenVibe staff only until then.
- **Host-only cookies.** The gate's cookie on a share host is `__Host-ovc` (Secure, HttpOnly, SameSite=Lax, Path=/, no
Domain), HMAC-signed and bound to that share and its policy version; changing the policy invalidates every cookie.
ddiff --git a/STATUS.json b/STATUS.json
iindex 889b4e0..e02c821 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -3,10 +3,10 @@
"version": "0.1.0",
"stage": "product",
"code": true,
-- "plan": "Website Connect v0 (plan 2026-10-28 §5.9b, ADR-057 reviewed 2026-11-02): share one port on your computer at https://<name>--<user>.openvibe.website behind OpenVibe sign-in",
++ "plan": "OpenVibe Connect v0 (plan 2026-10-28 §5.9b, ADR-057 reviewed 2026-11-02): share one port on your computer at https://<name>--<user>.openvibe.link behind OpenVibe sign-in",
"runtime": "node 22, express 4, ws 8, PostgreSQL 18 (openvibe-sdk/db, async)",
"port": "5240",
-- "domain": "connect.openvibe.website",
++ "domain": "connect.openvibe.link",
"contracts": "openvibe-contracts v0.338.0",
"sdk": "openvibe-sdk v0.94.0",
"shared": "openvibe-shared v4.154.0",
@@@ -17,7 +17,7 @@
],
"summary": "OpenVibe Connect puts one port on a person's computer on the web at its own HTTPS address, behind OpenVibe sign-in. A one-file client (Node 22, no dependencies) opens one outgoing WebSocket through Cloudflare and carries requests to localhost: HTTP with streaming both ways, server-sent events and WebSocket upgrades. Every visitor signs in and must be allowed by the share's policy (only the owner, named people, or anyone signed in); the share host keeps its own host-only signed cookie. Nothing that passes through a share is stored; a webhook addressed to one is sealed and kept for up to 72 hours until its computer takes it.",
"works": [
-- "Shares at <name>--<user>.openvibe.website, never colliding with Space sites; five per person; a closed share keeps its address",
++ "Shares at <name>--<user>.openvibe.link, never colliding with Space sites; five per person; a closed share keeps its address",
"The client: hello with a hashed-at-rest key, streams both ways with backpressure, SSE, WebSocket upgrades as a byte pipe, reconnect, takeover, stop on a bad key",
"The gate: /grant with OpenVibe sign-in, one-time codes, __Host-ovc bound to the share and its policy version",
"Durable webhooks (CON-15): an address on each share keeps up to 1,000 webhooks for 72 hours, sealed at rest, replays them in order with x-openvibe-received-at and a delivery log the owner can read",
ddiff --git a/deploy/nginx/connect.openvibe.website.conf b/deploy/nginx/connect.openvibe.website.conf
iindex 3e2c327..ce2b699 100644
---- a/deploy/nginx/connect.openvibe.website.conf
++++ b/deploy/nginx/connect.openvibe.website.conf
@@@ -1,15 +1,18 @@
## ═══════════════════════════════════════════════════════════════
## OpenVibe Connect — Nginx configuration (REFERENCE)
##
--## Deployed to /etc/nginx/sites-available/connect.openvibe.website.conf (ov-vhost-install). The Node app (port 5240,
--## systemd unit openvibe-connect) serves two kinds of host:
++## Deployed to /etc/nginx/sites-available/connect.openvibe.website.conf (ov-vhost-install). The file name keeps the
++## original host; the Node app (port 5240, systemd unit openvibe-connect) serves four kinds of host:
##
--## connect.openvibe.website the pages, the API, sign-in, and the computers' tunnel (WebSocket /tunnel)
--## <label>--<user>.openvibe.website a share: the gate, then the tunnel to that person's computer
++## connect.openvibe.link canonical: the pages, the API, sign-in, and the computers' tunnel (WebSocket /tunnel)
++## <label>--<user>.openvibe.link canonical share hosts: the gate, then the tunnel to that person's computer
++## connect.openvibe.website the old control name: pages 302 to Link; /tunnel still answers
++## <label>--<user>.openvibe.website the old share names: still served, and 302 to Link when CONNECT_REDIRECT_LEGACY=1
##
## Every other *.openvibe.website host is OpenVibe.Space's (deploy/nginx/openvibe.website.conf in OpenVibe.Space),
## whose server_name leaves out any host with "--" in it: a Space site's name can never contain "--", so the two never
--## meet. connect.openvibe.website is an exact name and wins over Space's pattern anyway.
++## meet. connect.openvibe.website is an exact name and wins over Space's pattern anyway. A *.openvibe.link host without
++## "--" is not a Connect host: the Link apex stays Network's.
##
## A share streams: no proxy buffering either way (server-sent events, uploads), WebSocket upgrades pass, and a
## connection may stay open for an hour. The app sets every cache header itself (a share is never cached by
@@@ -37,7 +40,7 @@ server {
location / { return 301 https://$host$request_uri; }
}
--# ── connect.openvibe.website ──────────────────────────────────
++# ── connect.openvibe.website (old control name; pages 302 to Link) ──
server {
listen 443 ssl;
listen [::]:443 ssl;
@@@ -113,7 +116,7 @@ server {
}
}
--# ── <label>--<user>.openvibe.website: a share ─────────────────
++# ── <label>--<user>.openvibe.website: an old share alias ──────
server {
listen 443 ssl;
listen [::]:443 ssl;
@@@ -148,7 +151,7 @@ server {
}
}
--# Link aliases: install only once the wildcard certificate exists.
++# The canonical Link hosts.
# ── connect.openvibe.link ──────────────────────────────────
server {
listen 443 ssl;
ddiff --git a/deploy/systemd/openvibe-connect.service b/deploy/systemd/openvibe-connect.service
iindex 3ac3daa..84fe47a 100644
---- a/deploy/systemd/openvibe-connect.service
++++ b/deploy/systemd/openvibe-connect.service
@@@ -1,5 +1,5 @@
[Unit]
--Description=OpenVibe Connect — your computer, on the web, privately (connect.openvibe.website and every <name>--<user>.openvibe.website)
++Description=OpenVibe Connect — your computer, on the web, privately (connect.openvibe.link and every <name>--<user>.openvibe.link)
# Network (sign-in, JWKS, account events) should be up first, but Connect degrades honestly without it: connected
# shares keep passing requests for people already let in, and sign-in waits for the signing key (see /api/ready).
After=network.target postgresql.service pgbouncer.service valkey-server.service
ddiff --git a/package.json b/package.json
iindex 98c6264..4e8c6e3 100644
---- a/package.json
++++ b/package.json
@@@ -2,7 +2,7 @@
"name": "openvibe-connect",
"version": "0.1.0",
"private": true,
-- "description": "OpenVibe Connect: share one port on your computer at its own HTTPS address on openvibe.website, behind OpenVibe sign-in.",
++ "description": "OpenVibe Connect: share one port on your computer at its own HTTPS address on openvibe.link, behind OpenVibe sign-in.",
"main": "server/index.js",
"scripts": {
"start": "node server/index.js",
ddiff --git a/public/css/connect.css b/public/css/connect.css
iindex 94ec762..d3093b6 100644
---- a/public/css/connect.css
++++ b/public/css/connect.css
@@@ -1,5 +1,5 @@
/* OpenVibe Connect: the pieces sc.room does not draw. Every colour is a theme token (kit.css, room.css); the room's
-- accent is openvibe.website's (--room-accent / --room-fill / --room-on). Phone first; nothing moves on its own. */
++ accent is the Website room's (--room-accent / --room-fill / --room-on). Phone first; nothing moves on its own. */
.cn-wrap { max-width: 1120px; margin: 0 auto; padding: 0 max(16px, env(safe-area-inset-left, 0px)) 48px; }
.cn-page { max-width: 760px; padding-top: 20px; display: grid; gap: 22px; }
ddiff --git a/public/ov-connect.js b/public/ov-connect.js
iindex 72ebf97..4bbafea 100644
---- a/public/ov-connect.js
++++ b/public/ov-connect.js
@@@ -11,7 +11,7 @@
*
* --key <ovc_…> your share's connect key (or OV_CONNECT_KEY)
* --port <n> the local port to share (or the first bare number)
-- * --server <wss://> default wss://connect.openvibe.website/tunnel
++ * --server <wss://> default wss://connect.openvibe.link/tunnel
* --quiet no line per request
*
* Source: https://github.com/OpenVibers/OpenVibe.Connect (AGPL-3.0). Protocol: ovconnect-ws/1 (server/edge/frames.js).
@@@ -55,7 +55,7 @@ function main() {
const opts = parseArgs(process.argv.slice(2));
if (opts.version) { console.log(`ov-connect ${VERSION}`); return; }
if (opts.help || !opts.key || !opts.port) {
-- console.log('ov-connect: share a port on this computer at your OpenVibe Connect address.\n\n node ov-connect.js --key ovc_… --port 8080\n\nMake a share and its key at https://connect.openvibe.website');
++ console.log('ov-connect: share a port on this computer at your OpenVibe Connect address.\n\n node ov-connect.js --key ovc_… --port 8080\n\nMake a share and its key at https://connect.openvibe.link');
process.exitCode = opts.help ? 0 : 2;
return;
}
@@@ -71,7 +71,7 @@ function main() {
/**
* The client itself (exported for tests): createClient({ key, port, server, quiet, log, localHost }) → { start, stop, onReady }
*/
--const DEFAULT_SERVER = 'wss://connect.openvibe.website/tunnel';
++const DEFAULT_SERVER = 'wss://connect.openvibe.link/tunnel';
function createClient(opts) {
opts = { ...opts, server: opts.server || DEFAULT_SERVER };
ddiff --git a/server/agent-descriptor.js b/server/agent-descriptor.js
iindex ce448c6..157c6bf 100644
---- a/server/agent-descriptor.js
++++ b/server/agent-descriptor.js
@@@ -4,9 +4,9 @@
module.exports = {
id: "connect",
name: "OpenVibe Connect",
-- purpose: "Share one port on your computer at https://<name>--<username>.openvibe.website, behind OpenVibe sign-in; programs pass with a share's API key, MCP clients sign in with OAuth.",
-- home: "https://connect.openvibe.website",
-- site: "connect.openvibe.website",
++ purpose: "Share one port on your computer at https://<name>--<username>.openvibe.link, behind OpenVibe sign-in; programs pass with a share's API key, MCP clients sign in with OAuth.",
++ home: "https://connect.openvibe.link",
++ site: "connect.openvibe.link",
api: "/api/v1",
openapi: '/openapi.json',
policy: {
ddiff --git a/server/app.js b/server/app.js
iindex fbf1982..ac5dacb 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -4,9 +4,9 @@
* OpenVibe.Connect: the Express app factory; server/index.js listens, tests build their own instance with a temp
* database and a mock Network.
*
-- * <label>--<user>.openvibe.website/* a share: the gate, then the tunnel (edge/proxy.js), before anything else
++ * <label>--<user>.openvibe.link/* a share: the gate, then the tunnel (edge/proxy.js), before anything else
* /, /shares/*, /grant the pages (http/pages.js)
-- * /api/v1/node/* a person's computer as OpenVibe Node (http/node-api.js, CON-4)
++ * /api/v1/node/* a person's computer as OpenVibe Link (http/node-api.js, CON-4)
* /api/v1/* the API (http/api.js)
* /ov-connect.js the client, served as it is in public/
* /auth/* Network SSO with PKCE (auth/sso.js)
@@@ -243,7 +243,7 @@ async function createApp(opts = {}) {
// ── Sign-in (OAuth2 + PKCE client of OpenVibe.Network) ──
app.use('/auth/', rateLimit({ windowMs: 15 * 60_000, limit: 60, standardHeaders: true, legacyHeaders: false }));
app.use('/auth', sso.routes());
-- { const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'connect', service: 'connect', host: 'connect.openvibe.website', name: 'OpenVibe Connect', profile: 'ugc' })); }
++ { const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'connect', service: 'connect', host: 'connect.openvibe.link', name: 'OpenVibe Connect', profile: 'ugc' })); }
// ── Webhooks to a share's hooks, and Events' deliveries of them back (CON-15; /internal/ is loopback only) ──
app.use(createHookRoutes(ctx));
ddiff --git a/server/auth/node-auth.js b/server/auth/node-auth.js
iindex 69810e9..3a69564 100644
---- a/server/auth/node-auth.js
++++ b/server/auth/node-auth.js
@@@ -1,10 +1,11 @@
'use strict';
/**
-- * A person's computer, as OpenVibe Node (CON-4, ADR-057 decision 4). Two credentials, both checked:
++ * A person's computer, as OpenVibe Link (CON-4, ADR-057 decision 4). Two credentials, both checked:
*
* 1. its Network node token: an RS256 service token whose subject is node:<nod_…> (actor_type node), for audience
-- * openvibe.website, 300 s. Network issues it to any active machine a person owns (Network #538);
++ * openvibe.link (old binaries: openvibe.website), 300 s. Network issues it to any active machine a person owns
++ * (Network #538);
* 2. a tunnel token Connect mints for one share and that computer: `ovt_` + 32 random bytes, 60 s, one use. It is
* kept in memory as a SHA-256 hash only, never logged, stored or put in an event.
*
ddiff --git a/server/config.js b/server/config.js
iindex 9e118a8..7d3d216 100644
---- a/server/config.js
++++ b/server/config.js
@@@ -1,7 +1,7 @@
'use strict';
/**
-- * OpenVibe.Connect configuration (connect.openvibe.website). Every value comes from the environment (production:
++ * OpenVibe.Connect configuration (connect.openvibe.link). Every value comes from the environment (production:
* /etc/openvibe/connect.env, see .env.example). Only environment variable NAMES appear in code and docs; secrets are
* never logged.
*
@@@ -16,7 +16,7 @@ function load(env = process.env) {
const nodeEnv = env.NODE_ENV || 'development';
const isProduction = nodeEnv === 'production';
const port = int(env.PORT, 5240);
-- const baseUrl = trim(env.BASE_URL || (isProduction ? 'https://connect.openvibe.website' : `http://localhost:${port}`));
++ const baseUrl = trim(env.BASE_URL || (isProduction ? 'https://connect.openvibe.link' : `http://localhost:${port}`));
const networkUrl = trim(env.OV_NETWORK_URL || 'https://openvibe.network');
return {
@@@ -41,7 +41,7 @@ function load(env = process.env) {
networkIssuer: trim(env.OV_NETWORK_ISSUER || networkUrl),
// The registered service/admin audience stays Website; node audiences are independent.
audience: env.CONNECT_AUDIENCE || 'openvibe.website',
-- // Old Link binaries still request the Website audience during the transition.
++ // Old binaries still request the Website audience during the transition.
nodeAudiences: [...new Set([env.CONNECT_AUDIENCE || 'openvibe.link', 'openvibe.link', 'openvibe.website'])],
oauth: {
clientId: env.OV_OAUTH_CLIENT_ID || 'connect',
ddiff --git a/server/edge/gate.js b/server/edge/gate.js
iindex 9acf0a9..cd38313 100644
---- a/server/edge/gate.js
++++ b/server/edge/gate.js
@@@ -3,10 +3,10 @@
/**
* The gate in front of every share: who may open it, and how a visitor's browser proves it on a share host.
*
-- * A share host is another origin (agents--alex.openvibe.website), and openvibe.website is not on the Public Suffix
++ * A share host is another origin (agents--alex.openvibe.link), and openvibe.link is not on the Public Suffix
* List yet, so the gate keeps every cookie host-only and signs it itself:
*
-- * 1. A visitor without a valid cookie is sent to https://connect.openvibe.website/grant?share=<id>&next=<path>.
++ * 1. A visitor without a valid cookie is sent to https://connect.openvibe.link/grant?share=<id>&next=<path>.
* 2. There, signed in to OpenVibe (sign-in happens first if needed), the share's policy is checked. A person it lets
* in gets a one-time code (60 seconds, used once, bound to that share and host) and goes back to
* https://<share host>/__ovc/callback?code=…&next=….
ddiff --git a/server/edge/proxy.js b/server/edge/proxy.js
iindex cd87ea2..7891129 100644
---- a/server/edge/proxy.js
++++ b/server/edge/proxy.js
@@@ -1,7 +1,7 @@
'use strict';
/**
-- * The visitor's side of a share: every request to https://<label>--<user>.openvibe.website.
++ * The visitor's side of a share: every request to https://<label>--<user>.openvibe.link.
*
* handle(req, res) an HTTP request (SSE and streaming bodies included)
* upgrade(req, socket, head) a WebSocket upgrade: after the gate, a byte pipe to the local port
@@@ -13,8 +13,8 @@
* X-Forwarded-Host, the visitor's address in X-Forwarded-For, and who they are in X-OpenVibe-User and
* X-OpenVibe-Subject (headers of those names from the visitor are dropped). The gate's own cookie never reaches it.
*
-- * Responses: a Set-Cookie's Domain attribute is removed (until openvibe.website is on the Public Suffix List, a
-- * cookie for .openvibe.website would reach every other share and every Space site), and nothing a share sends may be
++ * Responses: a Set-Cookie's Domain attribute is removed (until openvibe.link is on the Public Suffix List, a
++ * cookie for .openvibe.link would reach every other share), and nothing a share sends may be
* kept by Cloudflare's cache (Cloudflare-CDN-Cache-Control: no-store; Cache-Control is made private): a cached copy
* would be served past the gate.
*/
ddiff --git a/server/edge/tunnels.js b/server/edge/tunnels.js
iindex 45aac7a..7fb6e55 100644
---- a/server/edge/tunnels.js
++++ b/server/edge/tunnels.js
@@@ -2,7 +2,7 @@
/**
* The tunnels: one WebSocket per connected share, opened by the client (public/ov-connect.js) to
-- * wss://connect.openvibe.website/tunnel and authenticated by its first message, { t: 'hello', key }.
++ * wss://connect.openvibe.link/tunnel and authenticated by its first message, { t: 'hello', key }.
*
* createTunnels({ shares, config, log }) → { handleUpgrade(req, socket, head), get(shareId), close(shareId, code, why),
* closeAll(), status() }
ddiff --git a/server/hooks.js b/server/hooks.js
iindex 37d0c5c..7dfb45a 100644
---- a/server/hooks.js
++++ b/server/hooks.js
@@@ -2,7 +2,7 @@
/**
* Hooks that wait for your computer (CON-15, ADR-057 decision 7). A hook is an address a provider sends webhooks to,
-- * https://connect.openvibe.website/h/<hk_…>, bound to one share and a path on the computer's app. A webhook is kept
++ * https://connect.openvibe.link/h/<hk_…>, bound to one share and a path on the computer's app. A webhook is kept
* (72 hours) and a pointer to it is published as connect.hook.received through the outbox, in one transaction. Events
* delivers it to Connect's own subscription (retried for up to 72 hours), and Connect hands it to the share's
* computer when it is connected. The computer may be asleep for days and still get every webhook, in order of arrival,
ddiff --git a/server/http/api.js b/server/http/api.js
iindex 04e50a6..dad5927 100644
---- a/server/http/api.js
++++ b/server/http/api.js
@@@ -2,21 +2,21 @@
/**
* /api/v1: OpenVibe.Connect's JSON API, for a person (the connect session cookie, or a Network user token as a
-- * Bearer). A session write must come from connect.openvibe.website itself (403 connect.cross_site).
++ * Bearer). A session write must come from connect.openvibe.link itself (403 connect.cross_site).
*
* GET /shares your shares (website.share@1 shapes, plus connected)
* POST /shares { label, port, mode?, named?, expires_in? } → 201 { share, key } the key is in this answer only
* GET /shares/:id one of your shares
* PATCH /shares/:id { mode?, named?, port?, note?, expires_in?, offline_site? } expires_in: seconds from now
* (600 to 30 days), or null to stay open until you close it; offline_site: where a browser
-- * goes while the computer is off, one of the sites on the domain (CON-17), '' or null to clear
++ * goes while the computer is off, one of your OpenVibe.Space sites (CON-17), '' or null to clear
* DELETE /shares/:id close it (the host answers 410 from then on)
* POST /shares/:id/key a new key → { share, key }; the connected computer is disconnected
* PUT /shares/:id/node { node: 'nod_…' } one of your computers serves it, as OpenVibe Link (CON-4): the key
* stops working → { share }; { node: null } lets the computer go → { share, key } (a new key)
* GET /machines your computers that can serve a share (Network's list of your machines)
* GET /shares/:id/hooks its webhook addresses (CON-15): each kept webhook waits up to 72 hours for the computer
-- * POST /shares/:id/hooks { path, label? } → 201 { hook }: https://connect.openvibe.website/h/<id> → that path
++ * POST /shares/:id/hooks { path, label? } → 201 { hook }: https://connect.openvibe.link/h/<id> → that path
* DELETE /shares/:id/hooks/:hook removes it; webhooks still waiting are dropped
* GET /shares/:id/hooks/:hook/deliveries the owner's log: what the app answered, and when
* GET /shares/:id/visits who opened it in the last 7 days
ddiff --git a/server/http/node-api.js b/server/http/node-api.js
iindex 26468ae..8a29bcc 100644
---- a/server/http/node-api.js
++++ b/server/http/node-api.js
@@@ -2,7 +2,7 @@
/**
* /api/v1/node: a person's computer, as OpenVibe Link (CON-4, ADR-057 decision 4). Every call carries the computer's
-- * Network node token (Authorization: Bearer, subject node:nod_…, audience openvibe.website); nothing here answers to a
++ * Network node token (Authorization: Bearer, subject node:nod_…, audience openvibe.link); nothing here answers to a
* person's token or the session.
*
* GET /shares the open shares this computer serves → { shares: [{ id, host, url, local_port, mode }] }
@@@ -14,14 +14,15 @@ const { http } = require('openvibe-contracts');
const { asyncRouter } = require('./router');
function createNodeApi(ctx) {
-- const { shares, nodeAuth } = ctx;
++ const { config, shares, nodeAuth } = ctx;
++ const host = new URL(config.baseUrl).host;
const r = asyncRouter();
r.use((req, res, next) => { res.set('Cache-Control', 'no-store'); next(); });
const fail = (res, req, status, code, detail) => http.sendProblem(res, status, code, { detail, ctx: req.ov });
r.use(async (req, res, next) => {
const header = String(req.headers.authorization || '');
-- if (!header.startsWith('Bearer ')) return fail(res, req, 401, 'token.required', 'send this computer\'s Network node token (audience openvibe.website) as a Bearer');
++ if (!header.startsWith('Bearer ')) return fail(res, req, 401, 'token.required', 'send this computer\'s Network node token (audience openvibe.link) as a Bearer');
const v = await nodeAuth.verify(header.slice(7).trim());
if (!v.ok) return fail(res, req, v.status, v.code, v.reason);
req.node = v.node;
@@@ -35,7 +36,7 @@ function createNodeApi(ctx) {
r.post('/shares/:id/tunnel-token', async (req, res) => {
const share = await shares.get(req.params.id);
// Another person's share and a share this computer does not serve look the same: not found.
-- if (!share || !share.node || share.node.id !== req.node) return fail(res, req, 404, 'share.not_found', 'This computer does not serve that share. Pick it on the share\'s page on connect.openvibe.website.');
++ if (!share || !share.node || share.node.id !== req.node) return fail(res, req, 404, 'share.not_found', `This computer does not serve that share. Pick it on the share's page on ${host}.`);
if (share.status !== 'active') return fail(res, req, 410, 'share.revoked', 'That share is closed.');
const t = nodeAuth.mint(share.id, req.node);
return res.json({ tunnel_token: t.token, expires_in: t.expires_in, share: brief(share) });
ddiff --git a/server/http/oauth-routes.js b/server/http/oauth-routes.js
iindex 4db6834..7245d8d 100644
---- a/server/http/oauth-routes.js
++++ b/server/http/oauth-routes.js
@@@ -10,7 +10,7 @@
* POST /oauth/token code + PKCE verifier, or a refresh token → tokens
*
* The metadata, registration and token answers carry CORS for any origin (MCP clients run in browsers too); the
-- * consent is a page on connect.openvibe.website and cannot be posted from elsewhere.
++ * consent is a page on connect.openvibe.link and cannot be posted from elsewhere.
*/
const express = require('express');
const { asyncRouter } = require('./router');
@@@ -23,6 +23,7 @@ function createOAuthRoutes(ctx) {
const { config, oauth, shares, proxy } = ctx;
const r = asyncRouter();
const issuer = config.baseUrl;
++ const host = new URL(issuer).host;
const page = (req, res, o, status = 200) => send(res, status, { viewer: req.viewer, config, path: req.originalUrl, ...o });
const signedIn = (req) => Boolean(req.viewer && req.viewer.kind === 'user' && req.viewer.subject);
const cors = (res) => { res.set('Access-Control-Allow-Origin', '*'); res.set('Access-Control-Allow-Headers', 'content-type, authorization, mcp-protocol-version'); res.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); };
@@@ -112,7 +113,7 @@ function createOAuthRoutes(ctx) {
res.set('Cache-Control', 'no-store');
res.set('Referrer-Policy', 'no-referrer');
if (!signedIn(req)) return res.redirect(303, '/auth/login?next=%2F');
-- if (!sameOrigin(req, config.baseUrl)) return page(req, res, { title: 'Not from this site', body: views.message({ title: 'That came from somewhere else', text: 'Answer on connect.openvibe.website itself.', link: { href: '/', label: 'OpenVibe Connect' } }) }, 403);
++ if (!sameOrigin(req, config.baseUrl)) return page(req, res, { title: 'Not from this site', body: views.message({ title: 'That came from somewhere else', text: `Answer on ${host} itself.`, link: { href: '/', label: 'OpenVibe Connect' } }) }, 403);
const q = pick(req.body || {});
const ok = await checked(req, res, q);
if (!ok) return undefined;
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 7c62949..29a8541 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -1,7 +1,7 @@
'use strict';
/**
-- * Connect's pages on connect.openvibe.website. They work without JavaScript: forms are plain POSTs.
++ * Connect's pages on connect.openvibe.link. They work without JavaScript: forms are plain POSTs.
*
* / the front door; signed in, your shares and the form to make one
* POST /shares make a share → its page, with the key shown once
@@@ -32,6 +32,7 @@ const seo = require('openvibe-shared/seo');
function createPageRoutes(ctx) {
const { config, shares, tunnels, gate, proxy, machines, hooks, apiKeys } = ctx;
++ const host = new URL(config.baseUrl).host;
const r = asyncRouter();
r.use(express.urlencoded({ extended: false, limit: '16kb' }));
const page = (req, res, o, status = 200) => send(res, status, { viewer: req.viewer, config, path: req.originalUrl, ...o });
@@@ -41,7 +42,7 @@ function createPageRoutes(ctx) {
const guard = (req, res) => {
if (!signedIn(req)) { res.redirect(303, loginTo(req.originalUrl)); return false; }
if (!sameOrigin(req, config.baseUrl)) {
-- page(req, res, { title: 'Not from this site', body: views.message({ title: 'That came from somewhere else', text: 'Changes to your shares must be made on connect.openvibe.website itself.', link: { href: '/', label: 'Your shares' } }) }, 403);
++ page(req, res, { title: 'Not from this site', body: views.message({ title: 'That came from somewhere else', text: `Changes to your shares must be made on ${host} itself.`, link: { href: '/', label: 'Your shares' } }) }, 403);
return false;
}
return true;
@@@ -274,7 +275,7 @@ function createPageRoutes(ctx) {
r.post('/report', async (req, res) => {
const values = { host: req.body.host, reason: req.body.reason, details: req.body.details };
const again = (status, error) => page(req, res, { title: 'Report a share', body: views.reportForm({ values, error, reasons: REPORT_REASONS, signedIn: signedIn(req) }) }, status);
-- if (!sameOrigin(req, config.baseUrl)) return again(403, 'Send the report from connect.openvibe.website itself.');
++ if (!sameOrigin(req, config.baseUrl)) return again(403, `Send the report from ${host} itself.`);
if (overReports(req)) return again(429, 'That is a lot of reports in an hour. Try again later, or write to OpenVibe from the Help site.');
try {
await shares.report({ host: values.host, reason: values.reason, details: values.details, reporter: signedIn(req) ? req.viewer.subject : null });
@@@ -297,7 +298,7 @@ function createPageRoutes(ctx) {
});
r.post('/admin/reports/:id/:action', async (req, res) => {
if (!staffOnly(req, res)) return undefined;
-- if (!sameOrigin(req, config.baseUrl)) return page(req, res, { title: 'Not from this site', body: views.message({ title: 'That came from somewhere else', text: 'Handle reports on connect.openvibe.website itself.', link: { href: '/admin/reports', label: 'Reports' } }) }, 403);
++ if (!sameOrigin(req, config.baseUrl)) return page(req, res, { title: 'Not from this site', body: views.message({ title: 'That came from somewhere else', text: `Handle reports on ${host} itself.`, link: { href: '/admin/reports', label: 'Reports' } }) }, 403);
try {
const out = await shares.handleReport(owner(req), req.params.id, req.params.action);
if (out.action === 'close' && out.share) {
@@@ -325,9 +326,9 @@ function createPageRoutes(ctx) {
// What llms.txt says (SEO-33: /llms-full.txt adds the README, Connect's full documentation, after it).
const NAME = 'OpenVibe Connect';
-- const SUMMARY = 'Share one port on your computer at https://<name>--<username>.openvibe.website, behind OpenVibe sign-in.';
++ const SUMMARY = `Share one port on your computer at https://<name>--<username>.${config.shares.domain}, behind OpenVibe sign-in.`;
const DETAILS = [
-- 'Client: one file, Node 22+: curl -fsSLO https://connect.openvibe.website/ov-connect.js; node ov-connect.js --key ovc_… --port 8080.',
++ `Client: one file, Node 22+: curl -fsSLO ${site}/ov-connect.js; node ov-connect.js --key ovc_… --port 8080.`,
'Who can open a share: only the owner, the owner and named OpenVibe usernames, or anyone signed in to OpenVibe. No public shares yet.',
'The app behind a share receives X-OpenVibe-User and X-OpenVibe-Subject; Host is localhost:<port>.',
'API (a person\'s Network token as a Bearer): GET/POST /api/v1/shares, GET/PATCH/DELETE /api/v1/shares/{id}, POST /api/v1/shares/{id}/key.',
ddiff --git a/server/http/principal.js b/server/http/principal.js
iindex 5e7d3ed..a7a4d9f 100644
---- a/server/http/principal.js
++++ b/server/http/principal.js
@@@ -6,8 +6,9 @@
* { kind: 'user', subject: 'usr_…', username, role, token, viaSession }
* a person: their Network user token as a Bearer, or this site's session cookie (the connect session).
* { kind: 'app', requester: 'app:…' | 'agent:…' | 'service:…', project, claims }
-- * a Network app, agent or service token for audience connect.openvibe.website. No Connect route needs one; the shape is
-- * kept so a future capability route can be added in one place.
++ * a Network app, agent or service token for Connect's service audience (CONNECT_AUDIENCE,
++ * openvibe.website). No Connect route needs one; the shape is kept so a future capability route can be added in
++ * one place.
* { kind: 'anonymous' }
*
* A request that presents a token is judged on that token alone: a bad one is refused, never downgraded to the
@@@ -50,6 +51,7 @@ function sameOrigin(req, baseUrl) {
}
function createPrincipal({ config, keys }) {
++ const host = new URL(config.baseUrl).host;
async function resolve(req) {
const header = String(req.headers.authorization || '');
if (header.startsWith('Bearer ')) {
@@@ -86,9 +88,9 @@ function createPrincipal({ config, keys }) {
function require_(cap) {
if (!CAPABILITIES.includes(cap)) throw new Error(`requireCapability(${cap}): not one of OpenVibe.Connect's capabilities`);
return function guard(req, res, next) {
-- if (req.principal.kind === 'anonymous') return http.sendProblem(res, 401, 'token.required', { detail: 'Sign in at connect.openvibe.website, or send a person\'s Network token as a Bearer.', ctx: req.ov });
++ if (req.principal.kind === 'anonymous') return http.sendProblem(res, 401, 'token.required', { detail: `Sign in at ${host}, or send a person's Network token as a Bearer.`, ctx: req.ov });
if (req.principal.kind === 'user') {
-- if (req.principal.viaSession && req.method !== 'GET' && !sameOrigin(req, config.baseUrl)) return http.sendProblem(res, 403, 'connect.cross_site', { detail: 'A signed-in request that changes something must come from connect.openvibe.website itself.', ctx: req.ov });
++ if (req.principal.viaSession && req.method !== 'GET' && !sameOrigin(req, config.baseUrl)) return http.sendProblem(res, 403, 'connect.cross_site', { detail: `A signed-in request that changes something must come from ${host} itself.`, ctx: req.ov });
return next();
}
return http.sendProblem(res, 403, 'connect.person_required', { detail: 'This route is for a person, not an app.', ctx: req.ov });
@@@ -99,12 +101,12 @@ function createPrincipal({ config, keys }) {
function requirePerson(req, res, next) {
if (req.principal.kind === 'user') {
if (req.principal.viaSession && req.method !== 'GET' && req.method !== 'HEAD' && !sameOrigin(req, config.baseUrl)) {
-- return http.sendProblem(res, 403, 'connect.cross_site', { detail: 'A signed-in request that changes something must come from connect.openvibe.website itself.', ctx: req.ov });
++ return http.sendProblem(res, 403, 'connect.cross_site', { detail: `A signed-in request that changes something must come from ${host} itself.`, ctx: req.ov });
}
return next();
}
if (req.principal.kind === 'app') return http.sendProblem(res, 403, 'connect.person_required', { detail: 'This route is for a person, not an app or a service.', ctx: req.ov });
-- return http.sendProblem(res, 401, 'token.required', { detail: 'Sign in at connect.openvibe.website, or send a person\'s Network token as a Bearer.', ctx: req.ov });
++ return http.sendProblem(res, 401, 'token.required', { detail: `Sign in at ${host}, or send a person's Network token as a Bearer.`, ctx: req.ov });
}
return { middleware, resolve, requireCapability: require_, requirePerson };
ddiff --git a/server/index.js b/server/index.js
iindex 0d07e06..917cd17 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -2,7 +2,7 @@
/**
* OpenVibe.Connect: process entry. `node server/index.js`
-- * Listens on PORT (5240) behind nginx (deploy/): connect.openvibe.website and every <label>--<user>.openvibe.website.
++ * Listens on PORT (5240) behind nginx (deploy/): connect.openvibe.link and every <label>--<user>.openvibe.link.
*/
const { createApp } = require('./app');
const { gracefulStop } = require('openvibe-sdk/service');
ddiff --git a/server/probe.js b/server/probe.js
iindex 08ecfac..f2c537d 100644
---- a/server/probe.js
++++ b/server/probe.js
@@@ -65,7 +65,7 @@ function createProbe({ shares, proxy, config, log = console, now = () => Date.no
if (!force && now() - at < DAY_MS) return null;
last.set(share.id, now());
if (last.size > 10000) last.delete(last.keys().next().value);
-- const out = await proxy.fetchThrough(share, { path: '/', headers: [['x-openvibe-probe', signature(share.id, secret, Math.floor(now() / 1000))], ['user-agent', 'OpenVibeConnectProbe/1 (+https://connect.openvibe.website/report)']] });
++ const out = await proxy.fetchThrough(share, { path: '/', headers: [['x-openvibe-probe', signature(share.id, secret, Math.floor(now() / 1000))], ['user-agent', `OpenVibeConnectProbe/1 (+${String(config.baseUrl).replace(/\/+$/, '')}/report)`]] });
if (!out || out.offline || out.error || !out.body) return null;
if (!/html/i.test(out.type || '') && !/<html|<form/i.test(out.body.toString('utf8', 0, 2048))) return { verdict: 'ok', flags: [], brands: [], why: null };
const a = assess(out.body.toString('utf8'), share);
ddiff --git a/server/render/layout.js b/server/render/layout.js
iindex be559bb..4dc638f 100644
---- a/server/render/layout.js
++++ b/server/render/layout.js
@@@ -2,7 +2,7 @@
/**
* Page shell. Every page is server-rendered through openvibe-shared/shell and is complete without JavaScript.
-- * Connect is the "your machine" half of openvibe.website, so it wears that room's accent (openvibe-shared/rooms
++ * Connect is the "your machine" half of OpenVibe Link, so it wears the Website room's accent (openvibe-shared/rooms
* `website`, #8EA0FF dark / #4B53C2 light); the navigation's name is `Connect`.
*/
const crypto = require('crypto');
@@@ -24,7 +24,7 @@ const NAV = [
{ label: 'Connect', href: '/', icon: 'fa-plug' },
{ label: 'Sites', href: 'https://openvibe.space', icon: 'fa-globe' },
];
--const API_BASE = 'https://connect.openvibe.website';
++const API_BASE = 'https://connect.openvibe.link';
const hashes = new Map();
function assetVersion(rel) {
@@@ -50,9 +50,9 @@ function layout(o) {
const canonical = o.canonical === undefined ? `${String(config.baseUrl).replace(/\/+$/, '')}${pathNow.split('?')[0]}` : o.canonical;
const navbar = {
service: 'connect',
-- // Connect.OpenVibe.Website, the way the navbar names every product on a subdomain (Bench.OpenVibe.Services).
++ // Connect.OpenVibe.Link, the way the navbar names every product on a subdomain (Bench.OpenVibe.Services).
// With tldLabel 'Connect' the host's own "connect" was added in front: "Connect.OpenVibe.Connect" (11-02).
-- brand: { sub: 'connect', tld: 'website' },
++ brand: { sub: 'connect', tld: 'link' },
apiBase: NETWORK_URL,
links: NAV.map((l) => ({ ...l, active: l.href === active || (l.href !== '/' && pathNow.startsWith(`${l.href}/`)) })),
appBar: [
ddiff --git a/server/settings.js b/server/settings.js
iindex c76c186..e5d47d8 100644
---- a/server/settings.js
++++ b/server/settings.js
@@@ -18,7 +18,7 @@ const SERVICE = 'connect';
const NAMESPACE = 'connect.signin_apps';
const BASE_PATH = '/internal/config';
const GROUP = 'Sign-in apps';
--const REDIRECT = 'https://connect.openvibe.website/auth/callback';
++const REDIRECT = 'https://connect.openvibe.link/auth/callback';
/**
* One entry per outside service. `env` names the fallback variables; `test` is the one cheap
ddiff --git a/server/shares.js b/server/shares.js
iindex 65d2d4f..686374b 100644
---- a/server/shares.js
++++ b/server/shares.js
@@@ -47,13 +47,13 @@ const SIGNIN_NOT_YET = '"Anyone signed in" opens with the abuse check. For now,
const isStaff = (owner) => Boolean(owner && owner.role === 'admin');
// A Space site's name: one DNS label with single hyphens, so never a share's "--" host (OpenVibe.Space's NAME_RE).
const SITE_LABEL_RE = /^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,62}$/;
--const OFFLINE_SITE_HELP = 'The fallback is one of the sites on this domain, such as your OpenVibe.Space site: https://<name>.openvibe.website, optionally with a path.';
++const OFFLINE_SITE_HELP = 'The fallback is one of your OpenVibe.Space sites: https://<name>.openvibe.website, optionally with a path.';
/**
* Where a browser goes while a share's computer is off (CON-17): '' / null / undefined-as-cleared → null; otherwise
-- * a site on the share domain itself (https://<name>.<domain>, one label, never a share's "--" host), no port and no
-- * credentials, a path and query allowed, the fragment dropped. Anything else throws, so a share can never send its
-- * visitors off the domain.
++ * a Space site on the Website domain (https://<name>.openvibe.website, one label, never a share's "--" host), no port
++ * and no credentials, a path and query allowed, the fragment dropped. Anything else throws, so a share can never send
++ * its visitors off the allowed domain.
*/
function offlineSite(value, { domain, scheme = 'https' }) {
if (value == null || String(value).trim() === '') return null;
@@@ -86,7 +86,7 @@ function hostFor(label, username, domain) {
return `${first}.${domain}`;
}
--/** The share label in a host, or null: "agents--alex.openvibe.website" → { label: 'agents', handle: 'alex' }. */
++/** The share label in a host, or null: "agents--alex.openvibe.link" → { label: 'agents', handle: 'alex' }. */
function parseHost(host, domain) {
if (Array.isArray(domain)) return domain.map((d) => parseHost(host, d)).find(Boolean) || null;
const h = String(host || '').toLowerCase().replace(/:\d+$/, '');
ddiff --git a/server/views/index.js b/server/views/index.js
iindex 6fffc07..ccc5a7e 100644
---- a/server/views/index.js
++++ b/server/views/index.js
@@@ -175,7 +175,7 @@ function home({ viewer, shares = [], connectedIds = new Set(), stats = {}, baseU
items: [
['What can the server make my computer do?', 'Nothing but answer requests for your share on the one port you gave the client, on localhost. The client never opens a connection anywhere else, and you can read all of it: it is one file.'],
['Who can open my share?', 'People you allow, after they sign in with OpenVibe: only you, or you and the people you name. "Anyone signed in" is open to OpenVibe staff until the abuse check is in. Changing who can open a share signs everyone out of it at once.'],
-- ['Can I make it public?', 'Not yet. A public address can be abused for phishing, so public shares come with the abuse check and openvibe.website\'s entry in the Public Suffix List. Until then every visitor signs in.'],
++ ['Can I make it public?', `Not yet. A public address can be abused for phishing, so public shares come with the abuse check and ${esc(domain)}'s entry in the Public Suffix List. Until then every visitor signs in.`],
['What does my app see?', 'The request as the browser sent it, with Host set to localhost:<port>, the share\'s address in X-Forwarded-Host, the visitor\'s address in X-Forwarded-For, and who they are in X-OpenVibe-User and X-OpenVibe-Subject. Cookies your app sets stay on your share\'s address.'],
['What are the limits?', 'Five open shares per person, 64 requests at once per share, 1,200 requests a minute per share, 100 MB per request body, and 10 GB a day across your shares. Nothing is stored: requests pass through.'],
['How is this different from ngrok or Tailscale?', 'It is part of your OpenVibe account: the people you let in are OpenVibe people, no separate login, no install beyond one file. TCP and UDP shares (game servers), custom domains, a private mesh between your machines and durable webhooks are the next steps on the plan.'],
ddiff --git a/test/agent-json.test.js b/test/agent-json.test.js
iindex 8f3e149..a66fde5 100644
---- a/test/agent-json.test.js
++++ b/test/agent-json.test.js
@@@ -9,10 +9,10 @@ const { agentManifest } = require('openvibe-shared/agent-layer');
const m = agentManifest(descriptor);
assert.strictEqual(m.network, 'OpenVibe');
--assert.strictEqual(m.home, 'https://connect.openvibe.website');
++assert.strictEqual(m.home, 'https://connect.openvibe.link');
assert.ok(m.purpose && m.purpose.length > 40, 'a purpose');
assert.ok(/^https:\/\//.test(m.api), 'its API, absolute');
--assert.strictEqual(m.llms, 'https://connect.openvibe.website/llms.txt');
++assert.strictEqual(m.llms, 'https://connect.openvibe.link/llms.txt');
assert.ok(Array.isArray(m.policy.auth) && m.policy.auth.length, 'how it is called');
const app = fs.readFileSync(path.join(__dirname, '..', 'server', 'app.js'), 'utf8');
assert.match(app, /require\('openvibe-shared\/agent-layer'\)\.mount\(app, \{ descriptor: require\('\.\/agent-descriptor'\) \}\);/);
ddiff --git a/test/pages.test.js b/test/pages.test.js
iindex eb9b481..329fbad 100644
---- a/test/pages.test.js
++++ b/test/pages.test.js
@@@ -71,7 +71,7 @@ const { check, done } = require('./helpers/check');
assert.ok(page.text.includes(`name="offline_site" type="url" maxlength="300" value="${site}"`), 'the saved site is shown');
const bad = await t.get(`/shares/${id}/policy`, { as: kim, form: { mode: 'named', named: '@lee, sam', offline_site: 'https://evil.example/' } });
assert.strictEqual(bad.status, 422);
-- assert.match(bad.text, /one of the sites on this domain/);
++ assert.match(bad.text, /one of your OpenVibe\.Space sites/);
await t.get(`/shares/${id}/policy`, { as: kim, form: { mode: 'named', named: '@lee, sam', offline_site: '' } });
assert.strictEqual((await t.get('/api/v1/shares', { as: kim })).json().shares[0].offline_site, null, 'cleared');
});
@@@ -127,7 +127,7 @@ const { check, done } = require('./helpers/check');
const agent = await t.get('/.well-known/agent.json');
assert.strictEqual(agent.status, 200);
const m = agent.json();
-- assert.deepStrictEqual([m.network, m.id, m.home], ['OpenVibe', 'connect', 'https://connect.openvibe.website']);
++ assert.deepStrictEqual([m.network, m.id, m.home], ['OpenVibe', 'connect', 'https://connect.openvibe.link']);
// SEO-33: llms.txt, then the README (Connect's full documentation).
const full = await t.get('/llms-full.txt');
assert.strictEqual(full.status, 200);
ddiff --git a/test/rules.test.js b/test/rules.test.js
iindex 9cf82d5..2c944a9 100644
---- a/test/rules.test.js
++++ b/test/rules.test.js
@@@ -77,7 +77,7 @@ const { createGate } = require('../server/edge/gate');
assert.strictEqual(parseArgs(['--key=ovc_y', '--port=9']).key, 'ovc_y');
// Used as a module (createClient) with no server, it still goes to production, not to "undefined".
const { DEFAULT_SERVER } = require('../public/ov-connect');
-- assert.strictEqual(DEFAULT_SERVER, 'wss://connect.openvibe.website/tunnel');
++ assert.strictEqual(DEFAULT_SERVER, 'wss://connect.openvibe.link/tunnel');
assert.strictEqual(parseArgs([]).server, DEFAULT_SERVER);
});