openvibers/openvibe.live Commits

The Rundown reads chat again (after_id), and the WHIP host answers WHIP only (#324)

OP OpenVibers committed 21 days ago · ac2e9397bb · 5 files, +69 −2

* The Rundown reads a stream's chat again: the first page names no after_id (Chat refused after_id=0)

* The WHIP host answers WHIP only; every other path moves to openvibe.live

Changes

ddiff --git a/server/index.js b/server/index.js
iindex 153e9c35..7d70665a 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -417,6 +417,8 @@ function isAllowedOrigin(origin) {
} catch { return false; }
}
++// The WHIP host answers WHIP only; its other paths go to the site (server/web/whip-host.js).
++app.use(require('./web/whip-host').createWhipHostRedirect({ whipUrl: () => (config.whip?.enabled ? config.whip.publicUrl : null), siteUrl: () => config.baseUrl }));
// WHIP ingest is keyed by the stream key, not by a cookie, so it is open to every origin —
// that is what lets a static, backend-less site publish from the browser (docs/whip.md).
// It must therefore bypass the credentialed allowlist below, which would 403 the preflight.
ddiff --git a/server/rundown/desk.js b/server/rundown/desk.js
iindex 27b63001..f29aa328 100644
---- a/server/rundown/desk.js
++++ b/server/rundown/desk.js
@@@ -191,7 +191,8 @@ async function streamChat(stream) {
try {
const chat = require('../chat/chat-client');
for (let page = 0; page < 6; page++) {
-- const out = await chat.readMessages({ stream_id: stream.id, after_id: hit.afterId, limit: CHAT_PAGE });
++ // The first page names no after_id: Chat refuses after_id=0 (an id is positive), which left every stream unfiltered.
++ const out = await chat.readMessages({ stream_id: stream.id, after_id: hit.afterId || undefined, limit: CHAT_PAGE });
const rows = (out && out.messages) || [];
for (const m of rows) {
hit.afterId = Math.max(hit.afterId, Number(m.id) || 0);
ddiff --git a/server/web/whip-host.js b/server/web/whip-host.js
nnew file mode 100644
iindex 00000000..f20ba268
---- /dev/null
++++ b/server/web/whip-host.js
@@@ -0,0 +1,27 @@
++'use strict';
++/**
++ * The WHIP ingest host (config.whip.publicUrl, whip.openvibe.live) answers WHIP only: /whip and /whip/* (and
++ * /api/health), and every other path moves to the site's own address with a 301. The same process serves both
++ * names, so opened in a browser it showed the whole site on a second origin: a second copy of every page, and API
++ * calls the credentialed CORS allowlist refused (logged once an hour, 2026-10-23).
++ *
++ * app.use(createWhipHostRedirect({ whipUrl: () => …, siteUrl: () => … }))
++ *
++ * Both are read per request: config.whip is filled by the URL-registry refresh after the middleware is built. With
++ * no WHIP host, or one that is the site's own, it does nothing.
++ */
++function hostOf(url) {
++ try { const u = new URL(String(url)); return /^https?:$/.test(u.protocol) ? u : null; } catch { return null; }
++}
++
++function createWhipHostRedirect({ whipUrl, siteUrl }) {
++ return function whipHostRedirect(req, res, next) {
++ const whip = hostOf(whipUrl());
++ const site = hostOf(siteUrl());
++ if (!whip || !site || whip.hostname === site.hostname || req.hostname !== whip.hostname) return next();
++ if (req.path === '/whip' || req.path.startsWith('/whip/') || req.path === '/api/health') return next();
++ return res.redirect(301, `${site.origin}${req.originalUrl}`);
++ };
++}
++
++module.exports = { createWhipHostRedirect };
ddiff --git a/test/rundown.test.js b/test/rundown.test.js
iindex 94c232f5..7706002b 100644
---- a/test/rundown.test.js
++++ b/test/rundown.test.js
@@@ -340,7 +340,8 @@ console.log('✅ API: front page, topic, reactions as a signed-out reader; old /
const started = Date.parse((await db.get('SELECT started_at FROM streams WHERE id = ?', [st])).started_at.replace(' ', 'T') + 'Z');
const chat = require('../server/chat/chat-client');
const prevRead = chat.readMessages;
-- chat.readMessages = async (q) => (Number(q.stream_id) === st && !q.after_id ? { messages: [{ id: 7, message: 'WTF this site is so cool', message_type: 'chat', timestamp: new Date(started + 5000).toISOString().replace('T', ' ').slice(0, 19) }] } : { messages: [] });
++ // Like Chat: an after_id that is not a positive id is a 400 (the first page names none).
++ chat.readMessages = async (q) => (q.after_id !== undefined && !(Number(q.after_id) > 0) ? Promise.reject(new Error('Chat answered 400 (Invalid after_id)')) : Number(q.stream_id) === st && !q.after_id ? { messages: [{ id: 7, message: 'WTF this site is so cool', message_type: 'chat', timestamp: new Date(started + 5000).toISOString().replace('T', ' ').slice(0, 19) }] } : { messages: [] });
desk._test.reset();
await desk.tick();
const sent = calls.filter((c) => c.workflow === 'live.rundown.segment' && c.input.streamer === 'ttsstreamer');
ddiff --git a/test/whip-host.test.js b/test/whip-host.test.js
nnew file mode 100644
iindex 00000000..208ce4c7
---- /dev/null
++++ b/test/whip-host.test.js
@@@ -0,0 +1,36 @@
++'use strict';
++// The WHIP host answers WHIP only (server/web/whip-host.js, 2026-10-23): on whip.openvibe.live, /whip, /whip/* and
++// /api/health pass; every other path is a 301 to the same path on the site. The site's own host, a missing WHIP
++// host or one equal to the site's pass everything.
++const assert = require('assert');
++const { createWhipHostRedirect } = require('../server/web/whip-host');
++
++const run = (mw, hostname, originalUrl) => {
++ const path = originalUrl.split('?')[0];
++ let out = 'next';
++ mw({ hostname, path, originalUrl }, { redirect: (code, to) => { out = `${code} ${to}`; } }, () => {});
++ return out;
++};
++
++let whip = 'https://whip.openvibe.live';
++const mw = createWhipHostRedirect({ whipUrl: () => whip, siteUrl: () => 'https://openvibe.live' });
++
++assert.strictEqual(run(mw, 'whip.openvibe.live', '/whip/abc123'), 'next');
++assert.strictEqual(run(mw, 'whip.openvibe.live', '/whip/abc123/res_1'), 'next');
++assert.strictEqual(run(mw, 'whip.openvibe.live', '/whip'), 'next');
++assert.strictEqual(run(mw, 'whip.openvibe.live', '/api/health'), 'next');
++assert.strictEqual(run(mw, 'whip.openvibe.live', '/'), '301 https://openvibe.live/');
++assert.strictEqual(run(mw, 'whip.openvibe.live', '/@kim?tab=vods'), '301 https://openvibe.live/@kim?tab=vods');
++assert.strictEqual(run(mw, 'whip.openvibe.live', '/api/streams/live'), '301 https://openvibe.live/api/streams/live');
++assert.strictEqual(run(mw, 'whip.openvibe.live', '/whipped'), '301 https://openvibe.live/whipped', 'only /whip itself and below');
++assert.strictEqual(run(mw, 'openvibe.live', '/'), 'next', 'the site itself is untouched');
++
++// The WHIP host is read per request (the URL registry fills it after boot): none, or the site's own, does nothing.
++whip = null;
++assert.strictEqual(run(mw, 'whip.openvibe.live', '/'), 'next');
++whip = 'https://openvibe.live';
++assert.strictEqual(run(mw, 'openvibe.live', '/'), 'next');
++whip = 'not a url';
++assert.strictEqual(run(mw, 'whip.openvibe.live', '/'), 'next');
++
++console.log('whip host: WHIP only, everything else to the site');