openvibers/openvibe.live Commits

Chat sockets never carry the token in the URL (C-05): chat, the media player and the broadcast PiP send it in their first join message; a message typed while the chat socket reconnects stays in the box instead of being re-posted to global chat over REST (C-06). Docs: bots authenticate /ws/chat with an Authorization header or the join message (?token= is deprecated); POST /api/chat/send is the bots' global-chat API

OP OpenVibers committed 24 Sept 2026 · ae589aca17 · 6 files, +23 −62


Changes

ddiff --git a/docs/api-tokens.md b/docs/api-tokens.md
iindex adf2f7fa..7057efaf 100644
---- a/docs/api-tokens.md
++++ b/docs/api-tokens.md
@@@ -72,13 +72,18 @@ For the OpenVibe.Live VS Code companion or any other coding-feed publisher, use
### WebSocket (Chat)
--Pass the token as the `token` query parameter:
++Send the token in an `Authorization: Bearer` header on the upgrade request (Node's `ws`, Python's `websockets` and most server libraries can), or as the `token` of your first `join` message (browsers cannot set headers):
```
--wss://openvibe.live/ws/chat?token=hbt_YOUR_TOKEN_HERE&streamId=123
++wss://openvibe.live/ws/chat?stream=123
++Authorization: Bearer hbt_YOUR_TOKEN_HERE
```
--The token works everywhere a JWT would — the server auto-detects the `hbt_` prefix and validates accordingly.
++```json
++{ "type": "join", "streamId": 123, "token": "hbt_YOUR_TOKEN_HERE" }
++```
++
++The token works everywhere a JWT would — the server auto-detects the `hbt_` prefix and validates accordingly. A `?token=` query parameter still works but is deprecated (compatibility shim C-05): URLs end up in proxy logs and browser history, so move to the header or the `join` message.
## Managing Tokens
@@@ -108,11 +113,12 @@ const WebSocket = require('ws');
const TOKEN = 'hbt_your_token_here';
const STREAM_ID = '123';
--const ws = new WebSocket(
-- `wss://openvibe.live/ws/chat?token=${TOKEN}&streamId=${STREAM_ID}`
--);
++const ws = new WebSocket(`wss://openvibe.live/ws/chat?stream=${STREAM_ID}`, {
++ headers: { Authorization: `Bearer ${TOKEN}` },
++});
ws.on('open', () => {
++ ws.send(JSON.stringify({ type: 'join', streamId: Number(STREAM_ID) }));
console.log('Connected to chat');
});
ddiff --git a/docs/chat-system.md b/docs/chat-system.md
iindex fd58f073..bd4279a5 100644
---- a/docs/chat-system.md
++++ b/docs/chat-system.md
@@@ -92,8 +92,10 @@ Body: { "streamId": 123, "fromTime": "2024-01-01T00:00", "toTime": "2024-01-02T0
### Connect
```
--wss://openvibe.live/ws/chat?token=JWT_OR_API_TOKEN&streamId=123
++wss://openvibe.live/ws/chat?stream=123
++Authorization: Bearer JWT_OR_API_TOKEN (non-browser clients)
```
++Browsers send the token in the first `join` message instead (`{ "type": "join", "streamId": 123, "token": "…" }`); the `ov_token`/`token` cookies also authenticate the upgrade on the site's own origin. A `?token=` query parameter still works for older bots but is deprecated (C-05): URLs are logged.
### Message Types (Client → Server)
| Type | Fields | Description |
ddiff --git a/public/fragments/documentation.html b/public/fragments/documentation.html
iindex fe64968f..8f986332 100644
---- a/public/fragments/documentation.html
++++ b/public/fragments/documentation.html
@@@ -96,7 +96,7 @@
<h3>Base URL &amp; Auth</h3>
<p>Base URL: <code>https://openvibe.live</code> (replace with your instance).</p>
--<p>Auth: <code>Authorization: Bearer &lt;JWT&gt;</code> header, or <code>?token=JWT</code> query param on WebSocket URLs.</p>
++<p>Auth: <code>Authorization: Bearer &lt;JWT&gt;</code> header. On WebSockets, the header on the upgrade request or the <code>token</code> of the first <code>join</code> message; a <code>?token=</code> query param still works but is deprecated (URLs are logged).</p>
<p>API tokens: <code>hbt_</code> prefix, scoped permissions, created from Dashboard → API Tokens.</p>
</div>
@@@ -168,7 +168,7 @@ openvibe.network /api/admin/streamer-mod/* → OpenVibe.Live /api/mod/*</code>
<p><strong>All identity is OpenVibe SSO.</strong> There is no local password login on OpenVibe.Live &mdash; <code>POST /api/auth/register</code> and <code>POST /api/auth/login</code> are permanently disabled (return <code>410</code>). Humans sign in through openvibe.network OAuth2; OpenVibe.Live receives an <strong>RS256 JWT</strong> signed by openvibe.network and verifies it locally. For bots and scripts, use a long-lived <strong><code>hbt_</code> API token</strong> (below) &mdash; no browser login needed.</p>
<h3>How auth is sent</h3>
--<p>Every authenticated request carries a token as <code>Authorization: Bearer &lt;token&gt;</code> (also read from the <code>ov_token</code> cookie, or a <code>?token=</code> query param on WebSocket URLs). <code>requireAuth</code> accepts either:</p>
++<p>Every authenticated request carries a token as <code>Authorization: Bearer &lt;token&gt;</code> (also read from the <code>ov_token</code> cookie; on WebSockets also from the first <code>join</code> message, and from a deprecated <code>?token=</code> query param). <code>requireAuth</code> accepts either:</p>
<ul>
<li><strong>API token</strong> &mdash; a string starting with <code>hbt_</code>. Best for bots/integrations. Cannot itself create/list/revoke tokens.</li>
<li><strong>openvibe.network JWT (RS256)</strong> &mdash; from the SSO login flow; auto-links/creates a local account. Roles come from the token.</li>
@@@ -491,7 +491,7 @@ while True:
<table class="doc-table">
<thead><tr><th>Method</th><th>Path</th><th>Auth</th><th>Description</th></tr></thead>
<tbody>
--<tr><td>POST</td><td>/api/chat/send</td><td>Yes</td><td>Send message via REST (fallback)</td></tr>
++<tr><td>POST</td><td>/api/chat/send</td><td>Yes</td><td>Post a message to global chat (bots; API tokens need the <code>chat</code> scope)</td></tr>
<tr><td>GET</td><td>/api/chat/:streamId/history</td><td>No</td><td>Chat history for a stream</td></tr>
<tr><td>GET</td><td>/api/chat/:streamId/users</td><td>No</td><td>Users in a stream's chat</td></tr>
<tr><td>GET</td><td>/api/chat/user/:username/profile</td><td>No</td><td>User profile card data</td></tr>
@@@ -1237,7 +1237,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<table class="doc-table">
<thead><tr><th>Method</th><th>Path</th><th>Auth</th><th>Notes</th></tr></thead>
<tbody>
--<tr><td>POST</td><td>/api/chat/send</td><td>Yes</td><td>REST fallback to post a global chat message</td></tr>
++<tr><td>POST</td><td>/api/chat/send</td><td>Yes</td><td>Post a message to global chat (bots; API tokens need the <code>chat</code> scope)</td></tr>
<tr><td>GET</td><td>/api/chat/:streamId/history</td><td>Opt</td><td>Per-streamer history (all slots + offline unless <code>?scope=stream</code>)</td></tr>
<tr><td>GET</td><td>/api/chat/global/history</td><td>Opt</td><td>Global chat history</td></tr>
<tr><td>GET</td><td>/api/chat/:streamId/replay</td><td>Opt</td><td>Chat replay for VOD/clip sync (<code>?from,to</code>)</td></tr>
ddiff --git a/public/js/broadcast.js b/public/js/broadcast.js
iindex 47acbddc..4a414085 100644
---- a/public/js/broadcast.js
++++ b/public/js/broadcast.js
@@@ -6270,7 +6270,6 @@ function _connectMediaPipSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const url = new URL(`${protocol}//${window.location.host}/ws/chat`);
const token = localStorage.getItem('token');
-- if (token) url.searchParams.set('token', token);
url.searchParams.set('stream', activeId);
const ws = new WebSocket(url.toString());
ddiff --git a/public/js/chat.js b/public/js/chat.js
iindex 5ed4658b..eb1da690 100644
---- a/public/js/chat.js
++++ b/public/js/chat.js
@@@ -2629,9 +2629,8 @@ function initChat(streamId, channelUserId = null) {
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
const token = localStorage.getItem('token');
-- // Pass token and stream in URL so the server can authenticate on connect
++ // The stream goes in the URL; the token only in the join message (never in a URL, where proxies log it).
const params = new URLSearchParams();
-- if (token) params.set('token', token);
if (streamId) params.set('stream', streamId);
const wsUrl = `${protocol}://${host}:${port}/ws/chat?${params.toString()}`;
@@@ -2793,7 +2792,6 @@ function _reconnectChatWs(streamId) {
const token = localStorage.getItem('token');
const params = new URLSearchParams();
-- if (token) params.set('token', token);
if (streamId) params.set('stream', streamId);
const wsUrl = `${protocol}://${host}:${port}/ws/chat?${params.toString()}`;
@@@ -4286,9 +4284,10 @@ function sendChat(overrideInput = null) {
return;
}
-- // If WS is down (server restarting, etc.), send to global chat via REST API
++ // WS down (Chat restarting): the message stays in the box until the socket is back. It is never
++ // re-routed to another room (the REST send is a bot API, not a browser fallback).
if (!chatWs || chatWs.readyState !== WebSocket.OPEN) {
-- _sendChatViaRest(text, input);
++ addSystemMessage('Chat is reconnecting. Your message is still in the box: send it again in a moment.');
return;
}
@@@ -4338,49 +4337,6 @@ function sendChat(overrideInput = null) {
startSlowModeCooldown();
}
--/**
-- * Fallback: send a chat message via REST API when the WebSocket is down.
-- * The message goes to global chat so the user isn't silenced during reconnects.
-- */
--async function _sendChatViaRest(text, input) {
-- const token = localStorage.getItem('token');
-- if (!token) {
-- addSystemMessage('Chat disconnected — log in to send messages while reconnecting');
-- return;
-- }
-- try {
-- const autoDeleteMinutes = normalizeChatAutoDeleteMinutes(chatSettings.autoDeleteMinutes);
-- const payload = { message: text, reply_to_id: _chatReplyTo?.id || undefined };
-- if (autoDeleteMinutes >= (chatSelfDeletePolicy.minMinutes || CHAT_SELF_DELETE_MINUTES) && canUseViewerAutoDeleteHere()) {
-- payload.auto_delete_minutes = autoDeleteMinutes;
-- }
--
-- const res = await fetch('/api/chat/send', {
-- method: 'POST',
-- headers: {
-- 'Authorization': `Bearer ${token}`,
-- 'Content-Type': 'application/json',
-- },
-- body: JSON.stringify(payload),
-- });
-- if (!res.ok) {
-- const err = await res.json().catch(() => ({}));
-- addSystemMessage(err.error || 'Failed to send message');
-- return;
-- }
-- // Clear input on success
-- input.value = '';
-- _autoResizeTextarea(input);
-- input.focus();
-- clearChatReply();
-- if (chatStreamId) {
-- addSystemMessage('Sent to global chat (stream chat reconnecting)');
-- }
-- } catch {
-- addSystemMessage('Server unreachable — message not sent');
-- }
--}
--
/* ── Reply-to helpers ─────────────────────────────────────────── */
/**
@@@ -6025,7 +5981,6 @@ function _openGlobalFeed() {
const token = localStorage.getItem('token');
const params = new URLSearchParams();
-- if (token) params.set('token', token);
// No stream param = global chat
const wsUrl = `${protocol}://${host}:${port}/ws/chat?${params.toString()}`;
const ws = new WebSocket(wsUrl);
ddiff --git a/public/js/media-player.js b/public/js/media-player.js
iindex 6699e850..44c08146 100644
---- a/public/js/media-player.js
++++ b/public/js/media-player.js
@@@ -840,7 +840,6 @@ function connectSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const url = new URL(`${protocol}//${window.location.host}/ws/chat`);
-- if (mpToken()) url.searchParams.set('token', mpToken());
url.searchParams.set('stream', mpLiveStream.id);
const socket = new WebSocket(url.toString());