openvibers/openvibe.live Commits
Channel links carry the @ everywhere: Live's own go-live push (golive-notify.js) and the AI 'caught live' pastes (stream-memory-job.js) still built https://openvibe.live/<username>, which is a 404 by rule. test/channel-links.test.js scans server/ and public/js for a username placed straight after an origin or base-URL expression without the @ (it catches the shapes Live used; the four allowed lines are PowerChat's own tip pages and relay API paths, each with its reason)
Changes
@@@ -200,7 +200,7 @@ async function _maybeLivePaste(stream, image, r, offset) {// AI output is never filed under a person (roadmap 33): origin 'ai', the stream rides along as its source.const paste = await require('../pastes-client').createPaste({slug: moments.makeSlug ? moments.makeSlug() : undefined, user_id: stream.user_id,-- title: r.title.slice(0, 80), content: `${r.description}\n\n🔴 Caught live on @${username}'s stream — ${base}/${username}`, language: 'text', visibility: 'public',++ title: r.title.slice(0, 80), content: `${r.description}\n\n🔴 Caught live on @${username}'s stream — ${base}/@${encodeURIComponent(username)}`, language: 'text', visibility: 'public',stream_id: stream.id, metadata: JSON.stringify({ ai_moment: true, live: true, stream_id: stream.id, offset, username, vod_link: null }),ai_summary: r.description, ai_tags: JSON.stringify(Array.isArray(r.tags) ? r.tags.slice(0, 8) : []),screenshot: { buffer, filename: `live-${stream.id}-${offset}.jpg`, contentType: 'image/jpeg' },@@@ -16,7 +16,7 @@ const { notifyDiscordGoLive } = require('../integrations/discord-webhook');const DEDUPE_MS = 60 * 60 * 1000;const _recent = new Map(); // `${userId}:${slot}` → ts--function _channelUrl(streamer) { return `${(config.baseUrl || 'https://openvibe.live').replace(/\/$/, '')}/${streamer.username}`; }++function _channelUrl(streamer) { return `${(config.baseUrl || 'https://openvibe.live').replace(/\/$/, '')}/@${encodeURIComponent(streamer.username)}`; }function notifyFollowersGoLive(streamer, stream, { force = false } = {}) {if (!streamer || !streamer.id) return;nnew file mode 100644@@@ -0,0 +1,55 @@++'use strict';++++// A channel's page is only ever /@<username> (owner rule, 2026-09-23: a bare /<username> is a 404 and++// never redirects, so usernames and page routes can never compete). This scans Live's server and++// client code for links that put a username straight after the site's origin or a base-URL++// expression without the @, the shape go-live notifications, Discord posts and AI pastes used.++++const assert = require('assert');++const fs = require('fs');++const path = require('path');++++const ROOT = path.join(__dirname, '..');++const DIRS = ['server', 'public/js'];++// `${base}/${username}` or `openvibe.live/${user.username}` (optionally encodeURIComponent/esc-wrapped).++const BARE = /(?:\}|openvibe\.live)\/\$\{(?:encodeURIComponent\(|esc\()?[\w.?]*(?:[uU]sername|handle)\)?\}/g;++++// Every entry names a line whose path is a real route, not a channel page, and why.++const ALLOWED = [++ { file: 'server/integrations/powerchat-checkout.js', contains: 'pcUsername', why: "PowerChat's own tip page (powerchat.live/<user>/tip), not an OpenVibe channel" },++ { file: 'server/integrations/powerchat-routes.js', contains: 'powerchat_username', why: "PowerChat's own tip page" },++ { file: 'public/js/app.js', contains: '/chat-ai/relay/', why: 'API path /chat-ai/relay/<platform>/<username>' },++ { file: 'public/js/chat.js', contains: '/chat/relay-user/', why: 'API path /chat/relay-user/<platform>/<username>' },++];++++function* files(dir) {++ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {++ const p = path.join(dir, e.name);++ if (e.isDirectory()) { if (e.name !== 'node_modules' && e.name !== 'vendor') yield* files(p); }++ else if (e.name.endsWith('.js')) yield p;++ }++}++++const found = [];++for (const d of DIRS) {++ for (const f of files(path.join(ROOT, d))) {++ const lines = fs.readFileSync(f, 'utf8').split('\n');++ lines.forEach((line, i) => {++ for (const m of line.matchAll(BARE)) {++ const where = `${path.relative(ROOT, f)}:${i + 1}`;++ if (!ALLOWED.some(a => where.startsWith(a.file) && line.includes(a.contains))) found.push(`${where} ${m[0]}`);++ }++ });++ }++}++++// The scanner itself catches the shapes Live used.++for (const bad of ['${base}/${username}', '`https://openvibe.live/${streamer.username}`', '${cfg.baseUrl}/${encodeURIComponent(u.username)}']) {++ assert.ok(bad.match(BARE), `the scanner catches ${bad}`);++}++for (const good of ['${base}/@${username}', '/api/users/${username}', '/@${encodeURIComponent(username)}']) {++ assert.ok(!good.match(BARE), `the scanner leaves ${good} alone`);++}++++assert.deepStrictEqual(found, [], `channel links without the @ (use channelPath() on the client, /@\${encodeURIComponent(username)} on the server):\n${found.join('\n')}`);++console.log('channel links: all checks passed');
@@ -200,7 +200,7 @@ async function _maybeLivePaste(stream, image, r, offset) {
@@ -200,7 +200,7 @@ async function _maybeLivePaste(stream, image, r, offset) {
// AI output is never filed under a person (roadmap 33): origin 'ai', the stream rides along as its source.
// AI output is never filed under a person (roadmap 33): origin 'ai', the stream rides along as its source.
const paste = await require('../pastes-client').createPaste({
const paste = await require('../pastes-client').createPaste({
slug: moments.makeSlug ? moments.makeSlug() : undefined, user_id: stream.user_id,
slug: moments.makeSlug ? moments.makeSlug() : undefined, user_id: stream.user_id,
- title: r.title.slice(0, 80), content: `${r.description}\n\n🔴 Caught live on @${username}'s stream — ${base}/${username}`, language: 'text', visibility: 'public',
+ title: r.title.slice(0, 80), content: `${r.description}\n\n🔴 Caught live on @${username}'s stream — ${base}/@${encodeURIComponent(username)}`, language: 'text', visibility: 'public',
stream_id: stream.id, metadata: JSON.stringify({ ai_moment: true, live: true, stream_id: stream.id, offset, username, vod_link: null }),
stream_id: stream.id, metadata: JSON.stringify({ ai_moment: true, live: true, stream_id: stream.id, offset, username, vod_link: null }),
ai_summary: r.description, ai_tags: JSON.stringify(Array.isArray(r.tags) ? r.tags.slice(0, 8) : []),
ai_summary: r.description, ai_tags: JSON.stringify(Array.isArray(r.tags) ? r.tags.slice(0, 8) : []),
screenshot: { buffer, filename: `live-${stream.id}-${offset}.jpg`, contentType: 'image/jpeg' },
screenshot: { buffer, filename: `live-${stream.id}-${offset}.jpg`, contentType: 'image/jpeg' },
@@ -16,7 +16,7 @@ const { notifyDiscordGoLive } = require('../integrations/discord-webhook');
@@ -16,7 +16,7 @@ const { notifyDiscordGoLive } = require('../integrations/discord-webhook');
const DEDUPE_MS = 60 * 60 * 1000;
const DEDUPE_MS = 60 * 60 * 1000;
const _recent = new Map(); // `${userId}:${slot}` → ts
const _recent = new Map(); // `${userId}:${slot}` → ts
-function _channelUrl(streamer) { return `${(config.baseUrl || 'https://openvibe.live').replace(/\/$/, '')}/${streamer.username}`; }
+function _channelUrl(streamer) { return `${(config.baseUrl || 'https://openvibe.live').replace(/\/$/, '')}/@${encodeURIComponent(streamer.username)}`; }
function notifyFollowersGoLive(streamer, stream, { force = false } = {}) {
function notifyFollowersGoLive(streamer, stream, { force = false } = {}) {
if (!streamer || !streamer.id) return;
if (!streamer || !streamer.id) return;
new file mode 100644
new file mode 100644
@@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
+'use strict';
+
+// A channel's page is only ever /@<username> (owner rule, 2026-09-23: a bare /<username> is a 404 and
+// never redirects, so usernames and page routes can never compete). This scans Live's server and
+// client code for links that put a username straight after the site's origin or a base-URL
+// expression without the @, the shape go-live notifications, Discord posts and AI pastes used.
+
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const ROOT = path.join(__dirname, '..');
+const DIRS = ['server', 'public/js'];
+// `${base}/${username}` or `openvibe.live/${user.username}` (optionally encodeURIComponent/esc-wrapped).
+const BARE = /(?:\}|openvibe\.live)\/\$\{(?:encodeURIComponent\(|esc\()?[\w.?]*(?:[uU]sername|handle)\)?\}/g;
+
+// Every entry names a line whose path is a real route, not a channel page, and why.
+const ALLOWED = [
+ { file: 'server/integrations/powerchat-checkout.js', contains: 'pcUsername', why: "PowerChat's own tip page (powerchat.live/<user>/tip), not an OpenVibe channel" },
+ { file: 'server/integrations/powerchat-routes.js', contains: 'powerchat_username', why: "PowerChat's own tip page" },
+ { file: 'public/js/app.js', contains: '/chat-ai/relay/', why: 'API path /chat-ai/relay/<platform>/<username>' },
+ { file: 'public/js/chat.js', contains: '/chat/relay-user/', why: 'API path /chat/relay-user/<platform>/<username>' },
+];
+
+function* files(dir) {
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
+ const p = path.join(dir, e.name);
+ if (e.isDirectory()) { if (e.name !== 'node_modules' && e.name !== 'vendor') yield* files(p); }
+ else if (e.name.endsWith('.js')) yield p;
+ }
+}
+
+const found = [];
+for (const d of DIRS) {
+ for (const f of files(path.join(ROOT, d))) {
+ const lines = fs.readFileSync(f, 'utf8').split('\n');
+ lines.forEach((line, i) => {
+ for (const m of line.matchAll(BARE)) {
+ const where = `${path.relative(ROOT, f)}:${i + 1}`;
+ if (!ALLOWED.some(a => where.startsWith(a.file) && line.includes(a.contains))) found.push(`${where} ${m[0]}`);
+ }
+ });
+ }
+}
+
+// The scanner itself catches the shapes Live used.
+for (const bad of ['${base}/${username}', '`https://openvibe.live/${streamer.username}`', '${cfg.baseUrl}/${encodeURIComponent(u.username)}']) {
+ assert.ok(bad.match(BARE), `the scanner catches ${bad}`);
+}
+for (const good of ['${base}/@${username}', '/api/users/${username}', '/@${encodeURIComponent(username)}']) {
+ assert.ok(!good.match(BARE), `the scanner leaves ${good} alone`);
+}
+
+assert.deepStrictEqual(found, [], `channel links without the @ (use channelPath() on the client, /@\${encodeURIComponent(username)} on the server):\n${found.join('\n')}`);
+console.log('channel links: all checks passed');