Repositories openvibers openvibe.surf official
openvibers/openvibe.surf
Open source surf maps in the browser: ride maps, let an agent surf the web for you, and see what's on across OpenVibe.
Get it
Get this repository
A Git token is made for this copy and shown once. It can read and write the repositories you own, and expires in an hour.
git clone https://git.openvibe.codes/openvibers/openvibe.surf.git
OpenVibe Link asks you on your computer. The folder is inside the root you chose in Link, and the request ends after two minutes.
Clone to my computerA Codes session clones from our disk: no network, no GitHub quota. When this repository has a GitHub twin, Codes pushes there too.
Open in CodesREADME.md
View fileOpenVibe.Surf
Ride maps, surf the web, surf what's on.
Live site: https://openvibe.surf · API docs: https://openvibe.services · For AI: https://openvibe.surf/llms.txt
Run it: npm ci && npm run dev (Node 22 or newer) · Tests: npm test
Status: v1, the three-surf front (v0.1.0, see [STATUS.json](STATUS.json)).
Domain: openvibe.surf · Port: 5110 · Service id: surf · Env prefix: SURF
License: AGPL-3.0-only (the same as every OpenVibe service).
OpenVibe.Surf is the Surf room. One room, three surfs:
- Ride — CS-style surf in the browser, served by OpenVibe.Games. The map catalogue is proxied from Games and
every map plays in an embed (https://openvibe.games/surf/embed?map=<id>&embed=1). - Watch — lean-back channel surfing over OpenVibe.Live: live streams, clips people cut and the AI moments of
the week, merged into one feed with one card shape. Open a party and everyone follows the host's channel. - Web — Surf with Vibe: tell an agent what you are looking for and it browses the web for you on a private
desk through OpenVibe.Actor, keeps its sources and finishes with a cited report.
What it is
- Identity: service id
surf, name OpenVibe.Surf, port 5110, host 127.0.0.1. It stores no accounts, no
database and no tokens: the person's Network session lives in httpOnly cookies. - Sign-in: an OAuth 2 client of OpenVibe.Network (
surf), authorization code with PKCE S256, a state check
and a server-side code exchange. - In memory, no database: no database, no Valkey, no Events. The only state is a 30 s Watch feed cache, a
60 s Ride cache, the SDK's JWKS cache and the watch-together parties, all in this one process. A restart ends
every watch party.
Watch
GET /api/v1/watch/feed?topic=all|live|clips|moments&cursor=
Merges three public OpenVibe.Live reads (OV_LIVE_INTERNAL_URL, default http://127.0.0.1:3000). The home's
Watch scene is the head of this same feed, fetched server-side, so it never shows a written-in channel:
| Source | Live route | What it gives |
|---|---|---|
| live | GET /api/streams | { streams: [...] } with username, display_name, avatar_url, title, viewer_count, thumbnail_url |
| clips | GET /api/content/feed?type=clips&sort=new&window=week | items with href, thumbnail_url, preview_url, title, channel, duration_seconds |
| moments | GET /api/content/moments?type=clips&sort=top&window=week | the same shape, AI-cut clips |
Every card is one shape:
{
"key": "live:bo",
"kind": "live",
"title": "Surf practice, all day",
"channel": { "username": "bo", "display_name": "Bo", "avatar_url": null },
"thumbnail": "https://openvibe.media/live/bo.jpg",
"play": { "type": "live", "src": null, "page": "https://openvibe.live/@bo" },
"duration_seconds": null,
"viewers": 38,
"why": "Live now · 38 watching"
}
- A clip or moment has
play.type: "hls",play.src= itspreview_url(an HLS master playlist on
openvibe.media: Media 302s/c/<id>tomaster.m3u8) andplay.page= its Live page. A live stream has
play.type: "live",src: nullandpage=https://openvibe.live/@<username>(Live has no embeddable
player yet). - Ranking: live streams first, by viewers; then moments and clips interleaved by freshness. Finally, no two
cards from the same channel sit next to each other when another channel is available. - Cache: a complete merged page for a topic/cursor is cached 30 s (
SURF_WATCH_CACHE_MS); a partial page is
not kept, so the next request retries the missing source. Each Live source gets 2 s
(SURF_WATCH_SOURCE_TIMEOUT_MS); a source that fails or times out is left out, named insources
({ live: "ok", clips: "ok", moments: "error" }) andpartialis true. - Cursor: opaque; it carries each Live feed's own
nextcursor. Page two does not repeat live streams. - Cache-Control:
public, max-age=30.
Watch together
A signed-in person opens a party (POST /watch/party, from this site) and gets an unguessable id at
/watch/p/<id>; that link is the invitation. Opening it sends a guest to sign in first and joins everyone else.
The page reuses the TV markup with a sf-party- bar: members, host, Share (the network's one Share sheet), Leave, and, if the host
pasted one, a link to an OpenVibe.Chat room (https://openvibe.chat/..., validated). There is no chat inside Surf.
- Host: the person who opened the party. Only the host commands, with
POST /watch/p/<id>/command(alsoPOST /watch/p/<id>and/state) and
{ channel_key, position_s, playing, at }, from this site's own Origin. A member's command is 403, and so is a
command from another site. - Sync:
GET /watch/p/<id>/eventsis Server-Sent Events (text/event-stream):stateonce, thencommand
when the host changes channel, play/pause or seek,memberson a join/leave, andchatwhen the room link
changes. The browser applies commands, follows play/pause and seeks, and corrects a drift over 2 s. - Leaving and lifetime:
POST /watch/p/<id>/leave(the Leave button) removes that member; if the host leaves
while others remain, the earliest remaining member becomes host. The last member out starts a 30-minute clock.
The Map holds at most 200 open parties and 50 members each; an empty party is dropped on the next read 30
minutes after its last member leaves. Everything is memory only: a restart ends every party.
Ride
GET /api/v1/ride/maps and GET /api/v1/ride/maps/:id
Proxy to OpenVibe.Games (OV_GAMES_INTERNAL_URL, default http://127.0.0.1:8000), cached 60 s
(SURF_RIDE_CACHE_MS). The routes /api/surf/maps and /api/surf/maps/:id are being added to Games now. Until
they answer, Games gives 404: the list is { "maps": [], "games": "not_ready" } and a map detail is a shell that
still carries the embed ({ "map": { "id": … }, "top": [], "games": "not_ready" }). A Games timeout or 5xx is a
502 games.unavailable problem and is never cached.
Each map carries embed_url:
https://openvibe.games/surf/embed?map=<id>&embed=1 (SURF_GAMES_PUBLIC_URL).
Web — Surf with Vibe
POST /api/v1/web/tasks (signed in; this site's own Origin) with { "query": "…" } (3–500 characters).
The person's own Network access token from the httpOnly surf_at cookie is sent to OpenVibe.Actor
(OV_ACTOR_INTERNAL_URL, default http://127.0.0.1:4950) as a Bearer on POST /api/v1/tasks. Actor accepts a
person's session token there; the task is theirs — their allowance and their task list — not OpenVibe.Surf's.
The task text is a short standing brief plus the query unchanged:
Surf the web for me with your tools. Use search_web to find pages, read_page to read them, and a desk when a
page needs one. Keep every source you use and finish with a short report that cites each source.
The request carries an Idempotency-Key header (and Actor's body idempotency_key, the same value), so a retried
form does not start a second task. The answer is { "task_id": "tsk_…", "page": "https://openvibe.actor/tasks/tsk_…" }
(201, Location set). Actor's 4xx refusal (401, 403, 429 allowance/budget, 422) keeps its status and its words;
an Actor that does not answer is a 502 actor.unavailable.
POST /web/go (form field q) is the same task as a plain form for no-JavaScript: it creates the task and
303s to the Actor task page, so the question is never in a URL. It is refused from another site (Origin or
Sec-Fetch-Site), a guest is sent to sign in, a bad query is a 400 with the form again, and an Actor refusal keeps
its status and prints its words. It is private, no-store; a GET /web/go starts nothing and goes back to /web.
Pages
| Route | What it is |
|---|---|
| / | the Surf room: the run motif (ramps, speed lines, a ticking timer), the three tiles under the hero, scenes (the Watch scene is the real Live feed's first cards), questions |
| /ride | the maps with tier, stages, runs and the record, and the embed of the first map |
| /ride/:map | one map: name, tier, stages, records if Games answers, the embed |
| /watch | the TV: the screen plays clips and moments in the page (HLS), the guide lists every channel; ↑ ↓, a swipe or a tap changes channel; a live stream that records to MP4 plays over Media's DVR playlist, a little behind live, and any other opens on its channel; Watch together opens a party |
| /watch/p/:id | a watch party: the same TV, a sf-party- bar with the members, host, Share and Leave, and the optional OpenVibe.Chat room link; signed-in members follow the host's channel and position over SSE |
| /web | the Surf with Vibe form, examples, and what happens next |
| /developers | the API, for people and agents |
| anything else | the 404 page |
All of them are server-rendered through the OpenVibe Frame (openvibe-shared/shell) and readable without
JavaScript, with stable sf- ids and classes. A page rendered for a signed-in person is private, no-store; a
public page is public, max-age=60 with Vary: Cookie. public/js/surf.js adds what the pages are complete
without: the TV (native HLS, else /vendor/hls.light.min.js 1.7.3, loaded only then), the Web form in the page and
the home timer; everything works without it.
API
| Route | Auth | Cache | What it does |
|---|---|---|---|
| GET /api/v1/watch/feed | none | public, max-age=30 | the merged Live feed |
| GET /api/v1/ride/maps | none | public, max-age=60 | the Games surf map catalogue |
| GET /api/v1/ride/maps/:id | none | public, max-age=60 | one map and its fastest runs |
| POST /api/v1/web/tasks | cookie + Origin | private, no-store | create one Actor task as the person |
| POST /web/go | cookie, this site only | private, no-store | the no-JavaScript task form, 303 to Actor |
| POST /watch/party | cookie + Origin | private, no-store | open a party, 303 to /watch/p/<id> (JSON: 201) |
| GET /watch/p/:id/events | cookie, member | no-store | the party's SSE stream (state, command, members, chat) |
| POST /watch/p/:id/command | cookie, host + Origin | private, no-store | { channel_key, position_s, playing, at }, broadcast over SSE |
| POST /watch/p/:id/leave | cookie + Origin | private, no-store | leave the party, 303 to /watch (JSON: 200) |
| POST /watch/p/:id/chat | cookie, host + Origin | private, no-store | set the https://openvibe.chat/... room link |
Errors are RFC 9457 application/problem+json. A blank map id is 400; a bad Watch topic or cursor is 400. The
only outbound person token is the Authorization: Bearer on the request to Actor.
Plumbing: GET /api/health, GET /api/ready (200 ready or degraded; Network, JWKS, the OAuth client, Live,
Games and Actor are optional checks), GET /release.json, POST /release-metrics, /shared/* (the pinned Frame),
/css/* and /js/* (content-hashed ?v= → immutable), GET /robots.txt, GET /sitemap.xml (the public pages
and the Ride maps), GET /llms.txt and GET /metrics (loopback only).
Sign-in
server/auth/sso.js is OpenVibe.Design's module with this site's names:
GET /auth/login?next=/web[&silent=1]mints a state and a PKCE S256 verifier/challenge, stores them (and the
sanitizednext) in the httpOnlysurf_oauthflow cookie scoped to/auth, and redirects to
${OV_NETWORK_URL}/oauth/authorizewithclient_id=surfandredirect_uri=${BASE_URL}/auth/callback.GET /auth/callbackchecks the state in constant time, exchanges the code at the Network's internal
/oauth/tokenwith the client secret and the code verifier, and setssurf_at(24 h) andsurf_rt(30 d):
httpOnly, SameSite=Lax, Secure in production, Path=/.GET /auth/logoutclears them and revokes the refresh token (best effort);GET /auth/meanswers{ user }or
{ user: null }for the shared navbar. A FedCM assertion or an app/service token never becomes a session.sanitizeNextaccepts same-site relative paths only. An absolute URL on this site's own origin counts as its
path; another origin,//evil, a control character or a backslash goes to/.
The Network signs the access token (RS256); server/auth/keys.js fetches and refreshes its JWKS from
${OV_NETWORK_INTERNAL_URL}/api/.well-known/jwks through openvibe-sdk/auth.
Configuration
See [.env.example](.env.example). Required in production: BASE_URL and OV_OAUTH_CLIENT_SECRET (the surf
OAuth client on the Network).
| Variable | Default | What |
|---|---|---|
| OV_NETWORK_INTERNAL_URL | http://127.0.0.1:4000 | sign-in token exchange and JWKS |
| OV_NETWORK_URL | https://openvibe.network | where a browser is sent to authorize |
| OV_NETWORK_ISSUER | OV_NETWORK_URL | the token issuer |
| OV_LIVE_INTERNAL_URL | http://127.0.0.1:3000 | Watch's three Live reads |
| OV_GAMES_INTERNAL_URL | http://127.0.0.1:8000 | Ride's map reads |
| OV_ACTOR_INTERNAL_URL | http://127.0.0.1:4950 | Web's task creation |
| SURF_GAMES_PUBLIC_URL | https://openvibe.games | the embed origin and embed_url |
| SURF_LIVE_PUBLIC_URL | https://openvibe.live | card page links |
| SURF_ACTOR_PUBLIC_URL | https://openvibe.actor | the task page a person is sent to |
| SURF_WATCH_CACHE_MS | 30000 | the merged Watch feed cache |
| SURF_WATCH_SOURCE_TIMEOUT_MS | 2000 | one Live source's budget |
| SURF_RIDE_CACHE_MS | 60000 | the Ride cache |
| SURF_GAMES_TIMEOUT_MS | 4000 | one Games read's budget |
| SURF_ACTOR_TIMEOUT_MS | 15000 | one Actor task creation's budget |
| SURF_RATE_LIMIT_PER_MIN | 120 | per-IP /api and /auth budget |
| SURF_PARTY_MAX | 200 | open watch parties (may only be lowered, hard cap 200) |
| SURF_PARTY_MEMBER_MAX | 50 | members per party (may only be lowered, hard cap 50) |
| COOKIE_SECURE | true in production | Secure cookies |
How to run
npm install
npm run dev # http://localhost:5110
OV_ALLOW_SUITE=1 npm test
npm test runs every test/*.test.js (including test/party.test.js: create/join/leave, host-only and
same-site commands, SSE delivery, the 30-minute TTL and the caps) in its own process with in-process stand-ins
(test/helpers/network.js, test/helpers/upstreams.js): a real RS256 key pair and the OAuth token endpoint for
Network, and recorded Live, Games and Actor routes. Nothing calls the real internet. OV_ALLOW_SUITE=1 is the
suite opt-in the shared test runner expects.
Deploy (for the lead)
- Deploy:
sudo ovhost deploy surfon the host (checkout at/opt/openvibe.surf, unit
openvibe-surf.serviceon 127.0.0.1:5110, env/etc/openvibe/surf.env). - nginx: [deploy/nginx/openvibe.surf.conf](deploy/nginx/openvibe.surf.conf), installed with
ov-vhost-install./apiis reads only;/api/v1/web/and/web/goare the forms zone;/authis GET only;
/auth/megets the API budget;/internaland/metricsare 404. The SSE route
GET /watch/p/<id>/eventshasproxy_buffering off, no gzip and a 1-hour read timeout. - Readiness:
/api/readyreportsdegraded(HTTP 200) when Network, Live or Games does not answer; the
process still serves the pages and the crawl artifacts.
Security (threat notes)
Reporting a vulnerability: [SECURITY.md](SECURITY.md).
- Session tokens are httpOnly cookies; no script can read them and nothing renders or logs them. The only place
they go is theAuthorizationheader of a request to the configured Actor origin. - A Web task must come from this site's own origin (
Origin), so another site cannot make a signed-in visitor
spend their Actor allowance./web/gorefuses a cross-site navigation when the browser says it is one. nextafter sign-in is this site's own path only; control characters and backslashes are refused because
browsers drop or rewrite them.- Watch's card fields are allow-listed and escaped; only
http(s)URLs becomehref/src. Actor's error
detail is trimmed and no token or request body is logged. - A watch party's id is 144 random bits. Only its host may command; a browser command must come from this
site's own Origin (Origin/Sec-Fetch-Site, while a request with neither is a non-browser client and still
needs the session cookie). Only a member may open the SSE stream, and the party page isprivate, no-store.
The Map is capped and an empty party is dropped after 30 minutes. Member names and the optional Chat link are
escaped; the Chat link is an absolutehttps://openvibe.chat/URL with no credentials, and Surf stores no chat. - Secrets live only in the environment file; only variable names appear in code and docs, and no secret or token
is logged.
Part of the OpenVibe network. Built in the open by
OpenVibers.
<!-- versions:start -->
- openvibe-contracts: v0.419.0
- openvibe-sdk: v0.161.0
- openvibe-shared: v4.237.0
<!-- versions:end -->