openvibers/openvibe.live Commits

Guard that Live pastes go to Community, never Media's retired API (#17)

OP OpenVibers committed 3 Oct 2026 · fa51aa4d26 · 3 files, +86 −3

local checks ✓ 3/3 · CI ✓ 3/3 · review SHIP by DeepSeek harness

https://github.com/OpenVibers/OpenVibe.Live/pull/17

Changes

ddiff --git a/server/index.js b/server/index.js
iindex a4feeeb3..573a5742 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -6,7 +6,7 @@
* ╚═══════════════════════════════════════════════════════════╝
*
* Streaming: JSMPEG + WebRTC (Mediasoup) + RTMP
-- * Media (VODs/clips/pastes/thumbnails): OpenVibe.Media (openvibe.media)
++ * Media (VODs/clips/thumbnails): OpenVibe.Media (openvibe.media); pastes: OpenVibe.Community
* Identity/SSO + OpenCoins wallet: OpenVibe.Network (openvibe.network)
* Chat: WebSocket with anon handling + word filter
* Currencies: Vibes (tips/cashout, local) + OpenCoins (network wallet)
@@@ -113,8 +113,8 @@ const monetizationRoutes = require('./monetization/routes');
const coinsRoutes = require('./monetization/coins-routes');
const cosmeticsRoutes = require('./monetization/cosmetics-routes');
const cosmeticsModule = require('./monetization/cosmetics');
--// Media subsystem (VODs/clips/pastes/thumbnails) lives in OpenVibe.Media now —
--// these thin proxy routers preserve the public API paths the SPA calls.
++// Media subsystem (VODs/clips/thumbnails) lives in OpenVibe.Media now, and pastes in
++// OpenVibe.Community — these thin proxy routers preserve the public API paths the SPA calls.
const mediaClient = require('./media-client');
const vodRoutes = require('./media-proxy/vods');
const clipRoutes = require('./media-proxy/clips');
ddiff --git a/server/media-proxy/pastes.js b/server/media-proxy/pastes.js
iindex 5bdc36a5..a205e3b8 100644
---- a/server/media-proxy/pastes.js
++++ b/server/media-proxy/pastes.js
@@@ -5,6 +5,10 @@
* call there with Live's service token; the person is named by their canonical subject (never by a
* Live id), anonymous callers stay anonymous, and staff routes still pass Live's requireAdmin first.
*
++ * Live does not call Media's read-only paste API (`/api/v1/:app/pastes`, plan T10 step 2 — being
++ * retired); every paste read and write here goes to Community. Media's public paste URLs
++ * (/p/:slug[/raw|/screenshot]) are built in media-client.js for legacy rows only.
++ *
* Live-local exception:
* - POST /:slug/set-avatar → updates users.avatar_url in live.db
*/
ddiff --git a/test/pastes-not-media.test.js b/test/pastes-not-media.test.js
nnew file mode 100644
iindex 00000000..a86011d2
---- /dev/null
++++ b/test/pastes-not-media.test.js
@@@ -0,0 +1,79 @@
++'use strict';
++
++// Media's read-only paste surface (`/api/v1/:app/pastes`, plan T10 step 2) is being retired on the
++// note that "nothing calls the app API any more". This pins the Live half of that claim: since pastes
++// moved to OpenVibe.Community, Live reads and writes them only through server/pastes-client.js (see
++// test/pastes-community.test.js), and nothing under server/ or scripts/ builds a Media paste
++// application-API URL or hands a paste path to the Media client. A follow-up acting on Media's note
++// must find Live does not read those routes.
++//
++// Media's public paste URLs (/p/:slug[/raw|/screenshot]) are a different surface and stay: Live still
++// builds them in server/media-client.js, but only for legacy rows Media keeps serving.
++//
++// node test/pastes-not-media.test.js
++
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++
++const ROOT = path.join(__dirname, '..');
++
++// A literal Media tenant path: /api/v1/<app>/…/pastes.
++const MEDIA_API_PASTE = /\/api\/v1\/[^'"`\s)]*pastes/gi;
++// A call through the Media client (media.request(method, path, …)) whose path names pastes.
++const MEDIA_CLIENT_PASTE = /\bmedia(?:Client)?\.request\(\s*['"`][^'"`]*['"`]\s*,\s*['"`][^'"`]*pastes/gi;
++const PATTERNS = [MEDIA_API_PASTE, MEDIA_CLIENT_PASTE];
++
++function walk(dir, out = []) {
++ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
++ const p = path.join(dir, e.name);
++ if (e.isDirectory()) { if (e.name !== 'node_modules') walk(p, out); }
++ else if (e.name.endsWith('.js')) out.push(p);
++ }
++ return out;
++}
++
++const isCommentLine = (line) => /^\s*(\/\/|\*|\/\*)/.test(line);
++
++/** Every match for `regexes` in `src` that lands on a code line (not inside a comment). */
++function findMediaPasteCalls(src, regexes) {
++ const lines = src.split('\n');
++ const hits = [];
++ for (const re of regexes) {
++ re.lastIndex = 0;
++ let m;
++ while ((m = re.exec(src))) {
++ const startLine = src.slice(0, m.index).split('\n').length;
++ const endLine = src.slice(0, m.index + m[0].length - 1).split('\n').length;
++ if (isCommentLine(lines[startLine - 1]) || isCommentLine(lines[endLine - 1])) continue;
++ hits.push({ line: endLine, text: lines[endLine - 1].trim() });
++ }
++ }
++ return hits;
++}
++
++// The scanner itself: it has to catch the shapes the retired Media proxy used, and leave the public
++// URL builders and the retirement comments alone.
++for (const [src, n] of [
++ ["media.request('POST', `/pastes/${slug}/censor`)", 1],
++ ["await fetch(`${MEDIA_URL}/api/v1/live/pastes/abc`)", 1],
++ ["media.request('GET', `/vods/${id}`)", 0],
++ ["// media.request('POST', `/pastes/${slug}/censor`) — retired", 0],
++ ["const API_BASE = `${MEDIA_URL}/api/v1/${MEDIA_APP_ID}`;", 0],
++ ["pastesClient.request('GET', '/admin/stats')", 0],
++]) {
++ const got = findMediaPasteCalls(src, PATTERNS).length;
++ assert.strictEqual(got, n, `scanner: expected ${n} hit(s) for ${JSON.stringify(src)}, got ${got}`);
++}
++
++const offenders = [];
++for (const file of [...walk(path.join(ROOT, 'server')), ...walk(path.join(ROOT, 'scripts'))]) {
++ const src = fs.readFileSync(file, 'utf8');
++ for (const hit of findMediaPasteCalls(src, PATTERNS)) {
++ offenders.push(`${path.relative(ROOT, file)}:${hit.line} — ${hit.text}`);
++ }
++}
++assert.deepStrictEqual(offenders, [],
++ `Live still calls Media's read-only paste API (retired, T10 step 2):\n${offenders.join('\n')}`);
++
++console.log('pastes-not-media: ok (Live paste paths are Community-only)');