openvibers/openvibe.live Commits
Guard that Live pastes go to Community, never Media's retired API (#17)
Changes
@@@ -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');@@@ -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*/nnew file mode 100644@@@ -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)');
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
* ╚═══════════════════════════════════════════════════════════╝
* ╚═══════════════════════════════════════════════════════════╝
*
*
* Streaming: JSMPEG + WebRTC (Mediasoup) + RTMP
* 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)
* Identity/SSO + OpenCoins wallet: OpenVibe.Network (openvibe.network)
* Chat: WebSocket with anon handling + word filter
* Chat: WebSocket with anon handling + word filter
* Currencies: Vibes (tips/cashout, local) + OpenCoins (network wallet)
* Currencies: Vibes (tips/cashout, local) + OpenCoins (network wallet)
@@ -113,8 +113,8 @@ const monetizationRoutes = require('./monetization/routes');
@@ -113,8 +113,8 @@ const monetizationRoutes = require('./monetization/routes');
const coinsRoutes = require('./monetization/coins-routes');
const coinsRoutes = require('./monetization/coins-routes');
const cosmeticsRoutes = require('./monetization/cosmetics-routes');
const cosmeticsRoutes = require('./monetization/cosmetics-routes');
const cosmeticsModule = require('./monetization/cosmetics');
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 mediaClient = require('./media-client');
const vodRoutes = require('./media-proxy/vods');
const vodRoutes = require('./media-proxy/vods');
const clipRoutes = require('./media-proxy/clips');
const clipRoutes = require('./media-proxy/clips');
@@ -5,6 +5,10 @@
@@ -5,6 +5,10 @@
* call there with Live's service token; the person is named by their canonical subject (never by a
* 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 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:
* Live-local exception:
* - POST /:slug/set-avatar → updates users.avatar_url in live.db
* - POST /:slug/set-avatar → updates users.avatar_url in live.db
*/
*/
new file mode 100644
new file mode 100644
@@ -0,0 +1,79 @@
@@ -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)');