openvibers/openvibe.live Commits

Live: go-lives reach followers through Network's consumer only (GOLIVE_NOTIFY and the direct call deleted, C-85) (#93)

OP OpenVibers committed 11 Oct 2026 · 0b7b620bb6 · 12 files, +6 −303

Compatibility register C-85: go-lives reach followers only through Network's `live.stream.started` consumer. Live's direct call, its fallback and `GOLIVE_NOTIFY` go.

**Background, a production bug:**
- Since 2026-10-10 08:47 UTC Live ran `GOLIVE_NOTIFY=events`, skipping its direct call, while Network's consumer ignored every go-live (`ignored:follows_cutover_pending`; its flag was never set). Followers got no go-live alerts for about 17 hours.
- Network's follows are complete (64 = Live's 64), so the flag was set on Network and Network restarted (01:20 UTC). Network#142 removes the flag.

**This PR:**
- **Deleted:** `server/streaming/golive-notify.js` (the direct `POST /internal/events/stream-live`, the bulk-push and Discord fallback, the 60-minute dedupe, `GOLIVE_NOTIFY`) and `server/integrations/discord-webhook.js` (used only by that fallback).
- **Callers:** the four go-live paths (Go-Live API, RTMP, WHIP, the OpenRestream mirror) keep publishing `live.stream.started` with the streams row (`server/events/stream-events.js`) and the page banner (`live-events`), and make no direct call.
- **What alerts followers:** Network's consumer (inbox, push, email, Discord), with its own per-streamer window. A go-live while Network is down waits in Events and is delivered on retry.
- **Docs:** `.env.example` drops `GOLIVE_NOTIFY`; the documentation page describes go-live alerts and drops the Live fallback webhook section and the stream-live row.
- **Tests:**
  - `golive-notify-mode.test.js` is removed.
  - The stream-keys test drops the go-live-call check; the go-live SSE and the event envelopes stay checked for credentials.
  - The SSRF ratchet drops both files.

`ov test`: 81 reached files green.

Changes

ddiff --git a/.env.example b/.env.example
iindex a271dda6..c2ef9a73 100644
---- a/.env.example
++++ b/.env.example
@@@ -63,7 +63,6 @@ OV_OAUTH_CLIENT_SECRET=
# Go-live notifications (compatibility register C-85): `events` leaves them to Network's live.stream.started
# consumer (no direct POST /internal/events/stream-live, no fallback push); only while EVENTS_URL publishes.
# Unset = both paths, as before.
--#GOLIVE_NOTIFY=events
# OpenRestream (roadmap Wave 7): the ingest/restream authority. Unset = off, and every stream slot
# is ingested by Live exactly as before. When set, an admin can switch one slot at a time
ddiff --git a/public/fragments/documentation.html b/public/fragments/documentation.html
iindex d0f30ecd..522dcd63 100644
---- a/public/fragments/documentation.html
++++ b/public/fragments/documentation.html
@@@ -1087,14 +1087,13 @@ https://www.101soundboards.com/sounds/12345 150p 2</code></pre>
<table class="doc-table">
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
<tbody>
--<tr><td>POST</td><td>/internal/events/stream-live</td><td>Notify: stream went live { streamer, stream }</td></tr>
<tr><td>POST</td><td>/internal/push-single</td><td>Send push to one user { userId, title, body, url }</td></tr>
<tr><td>POST</td><td>/internal/push-bulk</td><td>Send push to multiple users { userIds[], title, body, url }</td></tr>
</tbody>
</table>
--<h3>Fallback Webhook (OpenVibe.Live direct)</h3>
--<p>If openvibe.network is unreachable, OpenVibe.Live falls back to its built-in Discord webhook sender. Configure the webhook URL in OpenVibe.Live site settings (<code>discord_webhook_url</code>).</p>
++<h3>Go-live alerts</h3>
++<p>When a stream starts, OpenVibe.Live publishes <code>live.stream.started</code> through OpenVibe.Events, and openvibe.network tells the streamer's followers: inbox, push, email and the Discord alert. Live makes no direct call.</p>
<h3>Multistream / Restream — <code>/api/restream</code></h3>
<p>Relay a live stream out to YouTube / Twitch / Kick / custom RTMP. Manage destinations per slot (max 10); stream keys are masked on read. All <code>requireAuth</code>.</p>
ddiff --git a/server/integrations/discord-webhook.js b/server/integrations/discord-webhook.js
ddeleted file mode 100644
iindex f91b4345..00000000
---- a/server/integrations/discord-webhook.js
++++ /dev/null
@@@ -1,77 +0,0 @@
--'use strict';
--
--const db = require('../db/database');
--
--// Cooldown tracking: streamId → lastNotifyTimestamp
--const _cooldowns = new Map();
--const DEFAULT_COOLDOWN_MS = 15 * 60 * 1000; // 15 minutes
--
--/**
-- * Send a Discord webhook embed when a stream goes live.
-- * @param {{ id: number, username: string, display_name?: string, avatar_url?: string }} streamer
-- * @param {{ id: number, title?: string }} stream
-- */
--async function notifyDiscordGoLive(streamer, stream) {
-- try {
-- const webhookUrl = await db.getSetting('discord_webhook_url');
-- if (!webhookUrl) return;
--
-- // Validate URL format
-- if (!/^https:\/\/discord\.com\/api\/webhooks\/\d+\/[\w-]+$/.test(webhookUrl) &&
-- !/^https:\/\/discordapp\.com\/api\/webhooks\/\d+\/[\w-]+$/.test(webhookUrl)) {
-- return;
-- }
--
-- // Cooldown check per stream
-- const cooldownMs = (await db.getSetting('discord_webhook_cooldown') || 15) * 60 * 1000;
-- const lastNotify = _cooldowns.get(stream.id);
-- if (lastNotify && (Date.now() - lastNotify) < cooldownMs) return;
--
-- const displayName = streamer.display_name || streamer.username;
-- const streamUrl = `https://openvibe.live/@${encodeURIComponent(streamer.username)}`;
-- const title = stream.title || 'Started streaming';
--
-- const embed = {
-- title: `🔴 ${displayName} is live!`,
-- description: title,
-- url: streamUrl,
-- color: 0xef4444, // red
-- timestamp: new Date().toISOString(),
-- footer: { text: 'OpenVibe.Live.com' },
-- };
--
-- if (streamer.avatar_url) {
-- embed.thumbnail = { url: streamer.avatar_url };
-- }
--
-- // Custom message template support
-- const customContent = await db.getSetting('discord_webhook_message') || null;
--
-- const body = {
-- embeds: [embed],
-- };
-- if (customContent) {
-- body.content = customContent
-- .replace(/{username}/g, streamer.username)
-- .replace(/{display_name}/g, displayName)
-- .replace(/{title}/g, title)
-- .replace(/{url}/g, streamUrl);
-- }
--
-- const res = await fetch(webhookUrl, {
-- method: 'POST',
-- headers: { 'Content-Type': 'application/json' },
-- body: JSON.stringify(body),
-- });
--
-- if (res.ok || res.status === 204) {
-- _cooldowns.set(stream.id, Date.now());
-- } else {
-- console.warn(`[discord-webhook] Discord returned ${res.status} for stream ${stream.id}`);
-- }
-- } catch (err) {
-- console.warn('[discord-webhook] Failed to send:', err.message);
-- }
--}
--
--module.exports = { notifyDiscordGoLive };
ddiff --git a/server/openre/mirror.js b/server/openre/mirror.js
iindex 2e63579a..184911f3 100644
---- a/server/openre/mirror.js
++++ b/server/openre/mirror.js
@@@ -102,7 +102,6 @@ async function apply(event) {
if (configId) await db.applyConfigToStream(configId, streamId);
} catch (e) { console.warn('[OpenRestream] control config for mirrored stream failed:', e.message); }
const stream = await db.getStreamById(streamId) || { id: streamId };
-- try { await require('../streaming/golive-notify').notifyFollowersGoLive(user, stream); } catch (e) { console.warn('[OpenRestream] go-live notify failed:', e.message); }
try { require('../streaming/live-events').announceGoLive(stream, user); } catch { /* */ }
console.log(`[OpenRestream] session ${sessionId} mirrored into stream ${streamId} (${user.username}, slot ${slot.id})`);
},
ddiff --git a/server/streaming/golive-notify.js b/server/streaming/golive-notify.js
ddeleted file mode 100644
iindex 53150b0d..00000000
---- a/server/streaming/golive-notify.js
++++ /dev/null
@@@ -1,117 +0,0 @@
--'use strict';
--/**
-- * "X is live" fan-out — ONE place, called from every go-live path (Go-Live page, OBS/RTMP
-- * ingest, WHIP/browser). Sends followers' NETWORK ids to openvibe.network's unified
-- * stream-live event (Discord + inbox + push + email), with a fallback to the plain bulk
-- * push if the network is unreachable.
-- *
-- * Deduped per streamer+slot for 60 minutes: reconnects, deploy restarts and the recorder
-- * re-creating a session must not spam followers.
-- *
-- * GOLIVE_NOTIFY=events (compatibility register C-85): leave go-lives to Network's live.stream.started
-- * consumer. Live then makes neither the direct call nor the fallback push; the event (queued with
-- * the streams row by server/events/stream-events.js, retried until Events has it) is the only path,
-- * and Network sends the inbox, push, email and Discord alerts. It applies only while Live publishes
-- * events; otherwise, and by default, the direct call stays. While both run, this call nearly always
-- * claims Network's per-streamer window first, so the consumer can never show a real `notified`:
-- * this switch is how C-85 gets its evidence. Rollback: unset it.
-- */
--const config = require('../config');
--const db = require('../db/database');
--const { pushBulkNotification, toNetworkIds, toNetworkId, OV_NETWORK_INTERNAL_URL } = require('../utils/notify');
--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(/\/$/, '')}/@${encodeURIComponent(streamer.username)}`; }
--
--/** GOLIVE_NOTIFY=events and Live's events outbox is on: Network's consumer is the only go-live path. */
--function leftToEvents() {
-- if (String(process.env.GOLIVE_NOTIFY || '').trim().toLowerCase() !== 'events') return false;
-- try { return !!require('../events/stream-events').status().enabled; } catch { return false; }
--}
--
--async function notifyFollowersGoLive(streamer, stream, { force = false } = {}) {
-- if (!streamer || !streamer.id) return;
-- if (leftToEvents()) {
-- console.log(`[GoLive] ${streamer.username}: left to Network's live.stream.started consumer (GOLIVE_NOTIFY=events)`);
-- return;
-- }
-- const slot = (stream && (stream.managed_stream_id || stream.slot_slug)) || 'default';
-- // Keyed by STREAMER (not slot): a person flapping between slots is still one person
-- // going live. This in-memory guard is only a fast path — openvibe.network enforces the
-- // persisted per-streamer cooldown + daily cap and reports `skipped` back.
-- const key = String(streamer.id);
-- const now = Date.now();
-- if (!force && _recent.get(key) && now - _recent.get(key) < DEDUPE_MS) {
-- console.log(`[GoLive] ${streamer.username}: followers already notified for slot ${slot} in the last hour — skipped`);
-- return;
-- }
-- _recent.set(key, now);
-- for (const [k, t] of _recent) if (now - t > DEDUPE_MS * 2) _recent.delete(k);
--
-- let followerLiveIds = [];
-- try { followerLiveIds = await db.getFollowerIds(streamer.id) || []; } catch { /* */ }
-- const { ids: followerNetworkIds, unlinked } = await toNetworkIds(followerLiveIds);
-- console.log(`[GoLive] ${streamer.username} (slot ${slot}): ${followerLiveIds.length} follower(s), ${followerNetworkIds.length} reachable on openvibe.network${unlinked ? `, ${unlinked} never linked` : ''}`);
--
-- const payload = {
-- streamer: {
-- id: streamer.id,
-- network_id: await toNetworkId(streamer.id),
-- username: streamer.username,
-- display_name: streamer.display_name || null,
-- avatar_url: streamer.avatar_url || null,
-- },
-- stream: {
-- id: stream?.id || null,
-- title: stream?.title || null,
-- protocol: stream?.protocol || null,
-- managed_stream_id: stream?.managed_stream_id || null,
-- url: _channelUrl(streamer),
-- },
-- follower_network_ids: followerNetworkIds,
-- };
--
-- // Live's service token (network.notifications.push).
-- const principal = require('../net/network-principal');
-- if (!principal.configured()) { await _fallback(streamer, stream, followerNetworkIds); return; }
-- principal.headersFor('/internal/events/stream-live').then((auth) => fetch(`${OV_NETWORK_INTERNAL_URL}/internal/events/stream-live`, {
-- method: 'POST',
-- headers: { 'Content-Type': 'application/json', ...auth },
-- body: JSON.stringify(payload),
-- })).then(async (r) => {
-- if (r.status === 401) principal.tokenRejected('stream-live');
-- if (r.ok) {
-- let d = null; try { d = await r.json(); } catch { /* */ }
-- if (d && d.skipped) { console.log(`[GoLive] ${streamer.username}: network skipped announcement (${d.reason}${d.next_allowed_at ? `, next ${d.next_allowed_at}` : ''})`); return; }
-- console.log(`[GoLive] Unified event sent for ${streamer.username}: notifications ${d?.notifications?.sent ?? '?'}/${d?.notifications?.total ?? '?'}, discord ${d?.discord?.sent ? 'sent' : 'no'}`);
-- } else {
-- console.warn(`[GoLive] Unified event failed (${r.status}), using fallback`);
-- await _fallback(streamer, stream, followerNetworkIds);
-- }
-- }).catch(async err => {
-- console.warn('[GoLive] Unified event error, using fallback:', err.message);
-- await _fallback(streamer, stream, followerNetworkIds);
-- });
--}
--
--/** Fallback: direct Discord webhook + bulk push (if openvibe.network is down/unauthorized). */
--async function _fallback(streamer, stream, followerNetworkIds) {
-- try { await notifyDiscordGoLive(streamer, stream); } catch { /* */ }
-- if (!followerNetworkIds.length) return;
-- pushBulkNotification(followerNetworkIds, {
-- type: 'STREAM_LIVE',
-- title: `${streamer.display_name || streamer.username} is live!`,
-- message: stream?.title || 'Started streaming',
-- icon: '🔴',
-- sender_id: await toNetworkId(streamer.id),
-- sender_name: streamer.display_name || streamer.username,
-- sender_avatar: streamer.avatar_url || null,
-- url: _channelUrl(streamer),
-- rich_content: { thumbnail: streamer.avatar_url || null, context: { stream_id: stream?.id || null, username: streamer.username, title: stream?.title || 'Started streaming' } },
-- }, { alreadyNetworkIds: true });
--}
--
--module.exports = { notifyFollowersGoLive, leftToEvents };
ddiff --git a/server/streaming/routes.js b/server/streaming/routes.js
iindex f88d7d0c..bcc66d5d 100644
---- a/server/streaming/routes.js
++++ b/server/streaming/routes.js
@@@ -113,17 +113,13 @@ async function ownControlConfigId(req, raw) {
return id;
}
--const { notifyDiscordGoLive } = require('../integrations/discord-webhook');
/**
-- * Push "X went live" notification via openvibe.network unified event endpoint.
-- * This lets openvibe.network handle Discord bot alerts + push notifications centrally.
-- * Falls back to direct webhook + bulk push if openvibe.network is unreachable.
++ * Followers hear about a go-live from openvibe.network: Live publishes live.stream.started (server/events/
++ * stream-events.js, with the streams row), and Network's consumer sends the inbox, push, email and Discord alerts
++ * (compatibility register C-85). Live makes no direct call.
*/
--// Go-live fan-out lives in ./golive-notify (shared with the RTMP and WHIP ingest paths,
--// which never notified anyone before), with follower-id translation + 60-min dedupe.
--const { notifyFollowersGoLive } = require('./golive-notify');
const cache = require('openvibe-shared/cache-policy');
function hasOwn(obj, key) {
@@@ -1999,8 +1995,6 @@ router.post('/', requireAuth, async (req, res) => {
});
try { require('../integrations/ai-chatbot-service').startForStream(stream); } catch (aiErr) { console.warn('[AI-Bots] start failed:', aiErr.message); }
-- // Notify followers that this streamer went live (fire-and-forget)
-- await notifyFollowersGoLive(req.user, stream);
// Cross-site on-screen "went live" notification (rate-limited per slot/hour).
try { require('./live-events').announceGoLive(stream, req.user); } catch { /* */ }
ddiff --git a/server/streaming/rtmp-server.js b/server/streaming/rtmp-server.js
iindex 59977ade..bac569ec 100644
---- a/server/streaming/rtmp-server.js
++++ b/server/streaming/rtmp-server.js
@@@ -9,7 +9,6 @@ const EventEmitter = require('events');
const config = require('../config');
const db = require('../db/database');
const recorder = require('./recorder');
--const { notifyDiscordGoLive } = require('../integrations/discord-webhook');
const RTMP_HEARTBEAT_INTERVAL_MS = 30000; // Refresh live stream timestamp while RTMP session is active
@@@ -213,10 +212,7 @@ class RTMPServer extends EventEmitter {
// Discord webhook notification (fire-and-forget)
const stream = db.getStreamById ? await db.getStreamById(streamId) : { id: streamId, title: `${resolvedUser.display_name}'s Stream` };
-- // Unified go-live event (inbox + push + email to followers, Discord via network;
-- // falls back to the webhook). Deduped per slot/hour inside.
-- try { await require('./golive-notify').notifyFollowersGoLive(resolvedUser, stream || { id: streamId }); }
-- catch (e) { console.warn('[RTMP] go-live notify failed:', e.message); await notifyDiscordGoLive(resolvedUser, stream || { id: streamId }); }
++ // Followers hear it from Network (live.stream.started via Events); the page banner comes from live-events.
try { require('./live-events').announceGoLive(stream || { id: streamId }, resolvedUser); } catch { /* */ }
// Start server-side VOD recording via FFmpeg
ddiff --git a/server/streaming/whip-handler.js b/server/streaming/whip-handler.js
iindex c43cb9ae..0009cd0d 100644
---- a/server/streaming/whip-handler.js
++++ b/server/streaming/whip-handler.js
@@@ -14,7 +14,6 @@ const webrtcSFU = require('./webrtc-sfu');
const { verifyToken, resolveNetworkUser } = require('../auth/auth');
const db = require('../db/database');
const recorder = require('./recorder');
--const { notifyDiscordGoLive } = require('../integrations/discord-webhook');
let chatRelayService;
try { chatRelayService = require('../integrations/chat-relay-service'); } catch {}
@@@ -611,8 +610,6 @@ async function autoCreateWhipSession(managedStream, user) {
console.log(`[WHIP] Auto-created live session ${streamId} for WHIP slot ${managedStream.id} (user: ${user.username})`);
// Fire-and-forget side effects
-- try { await require('./golive-notify').notifyFollowersGoLive(user, stream || { id: streamId }); }
-- catch (e) { console.warn('[WHIP] go-live notify failed:', e.message); await notifyDiscordGoLive(user, stream || { id: streamId }); }
try { require('./live-events').announceGoLive(stream || { id: streamId }, user); } catch { /* */ }
if (chatRelayService) {
chatRelayService.startForStream(stream).catch(() => {});
ddiff --git a/test/golive-notify-mode.test.js b/test/golive-notify-mode.test.js
ddeleted file mode 100644
iindex ce61e338..00000000
---- a/test/golive-notify-mode.test.js
++++ /dev/null
@@@ -1,66 +0,0 @@
--'use strict';
--// GOLIVE_NOTIFY=events (compatibility register C-85): with Live's events outbox on, a go-live makes no call to
--// Network (no POST /internal/events/stream-live, no fallback push) and leaves the followers to Network's
--// live.stream.started consumer; the event itself is still queued with the streams row. Unset, or with the
--// outbox off, the direct call stays.
--
--const assert = require('assert');
--
--process.env.OV_OAUTH_CLIENT_SECRET = 'live-secret';
--process.env.OV_NETWORK_INTERNAL_URL = 'http://127.0.0.1:9';
--const { tokenBody } = require('./helpers/network-token-stub');
--
--const calls = [];
--global.fetch = async (url, init = {}) => {
-- if (String(url).endsWith('/oauth/token')) return new Response(JSON.stringify(tokenBody()), { status: 200, headers: { 'content-type': 'application/json' } });
-- calls.push({ url: String(url), method: init.method || 'GET' });
-- return new Response(JSON.stringify({ ok: true, notifications: { sent: 0, total: 0 } }), { status: 200, headers: { 'content-type': 'application/json' } });
--};
--const logs = [];
--const log = console.log;
--console.log = (...a) => { logs.push(a.join(' ')); };
--
--(async () => {
-- try {
-- const db = require('../server/db/database');
-- await db.initDb();
-- await db.getDb().prepare("INSERT INTO users (id, username, display_name, password_hash) OVERRIDING SYSTEM VALUE VALUES (601, 'caster', 'Caster', 'x')").run();
-- const streamEvents = require('../server/events/stream-events');
-- const { notifyFollowersGoLive, leftToEvents } = require('../server/streaming/golive-notify');
-- const settle = () => new Promise((r) => setTimeout(r, 50));
-- const toNetwork = () => calls.filter((c) => c.url.startsWith('http://127.0.0.1:9/')).length;
--
-- // Default: the direct call, as before.
-- delete process.env.GOLIVE_NOTIFY;
-- assert.strictEqual(leftToEvents(), false);
-- await notifyFollowersGoLive({ id: 601, username: 'caster' }, { id: 1, title: 'one' });
-- await settle();
-- assert.ok(toNetwork() >= 1, 'unset: Live calls Network directly');
--
-- // GOLIVE_NOTIFY=events while the outbox is off: still the direct call (nothing else would tell anyone).
-- process.env.GOLIVE_NOTIFY = 'events';
-- assert.strictEqual(streamEvents.status().enabled, false);
-- assert.strictEqual(leftToEvents(), false);
--
-- // With the outbox on: no call at all, and the log says who announces.
-- streamEvents.init({ eventsUrl: 'http://127.0.0.1:9', clientSecret: 's3cret', intervalMs: 60000 });
-- assert.strictEqual(leftToEvents(), true);
-- const before = calls.length;
-- await notifyFollowersGoLive({ id: 601, username: 'caster' }, { id: 2, title: 'two' }, { force: true });
-- await settle();
-- assert.deepStrictEqual(calls.slice(before).filter((c) => /\/internal\//.test(c.url)), [], 'no direct call, no fallback push');
-- assert.ok(logs.some((l) => l.includes("caster: left to Network's live.stream.started consumer (GOLIVE_NOTIFY=events)")));
-- // The event path is intact: a new stream row still queues live.stream.started.
-- await db.createStream({ user_id: 601, title: 'three', protocol: 'rtmp' });
-- assert.strictEqual((await db.getDb().prepare("SELECT COUNT(*) AS c FROM event_outbox WHERE envelope::text ILIKE '%live.stream.started%'").get()).c, 1);
--
-- // Any other value keeps both paths.
-- process.env.GOLIVE_NOTIFY = 'both';
-- assert.strictEqual(leftToEvents(), false);
-- streamEvents._reset();
-- } finally {
-- console.log = log;
-- }
-- console.log('golive notify mode: all checks passed');
-- process.exit(0);
--})().catch((err) => { console.log = log; console.error(err); process.exit(1); });
ddiff --git a/test/security-ssrf.test.js b/test/security-ssrf.test.js
iindex 4f4fae94..d1518375 100644
---- a/test/security-ssrf.test.js
++++ b/test/security-ssrf.test.js
@@@ -351,7 +351,6 @@ async function check(name, fn) {
'server/chat/chat-client.js': 'OpenVibe.Chat typed ingress (configured OV_CHAT_INTERNAL_URL, fixed /internal/chat/* paths)',
'server/chat/chat-delivery.js': 'OpenVibe.Tools anon resolve (configured)',
'server/streaming/calls-authority.js': 'OpenVibe.Chat (configured)',
-- 'server/streaming/golive-notify.js': 'OpenVibe.Network (configured)',
'server/utils/notify.js': 'OpenVibe.Network (configured)',
'server/auth/identity-sync.js': 'OpenVibe.Network (configured)',
'server/auth/account-data.js': 'OpenVibe.Network (configured): export parts and deletion confirmations',
@@@ -373,7 +372,6 @@ async function check(name, fn) {
'server/news/sources/rss-source.js': 'feeds the site owner configures',
'server/integrations/platform-oauth.js': 'Twitch / YouTube / Kick OAuth (fixed hosts)',
'server/integrations/powerchat-oauth.js': 'PowerChat (owner setting)',
-- 'server/integrations/discord-webhook.js': 'Discord webhook (owner-only setting)',
'server/integrations/robotstreamer-service.js': 'RobotStreamer API and the endpoints it returns',
'server/integrations/rs-passthrough-relay.js': 'RobotStreamer SFU (from its API)',
'server/integrations/chat-relay-service.js': 'YouTube API (fixed host); user channel pages via egress.fetchText',
ddiff --git a/test/security-stream-keys.test.js b/test/security-stream-keys.test.js
iindex 02381557..20250c8f 100644
---- a/test/security-stream-keys.test.js
++++ b/test/security-stream-keys.test.js
@@@ -101,20 +101,6 @@ const SEED = `(async () => {
le.subscribe({ on() {} }, { writeHead() {}, write(s) { writes.push(String(s)); } });
await le.announceGoLive(raw, streamer);
out.sse = writes.join('');
-- const posted = [];
-- const realFetch = global.fetch;
-- // Network mints Live's service token, then takes the go-live call; only calls to Network's internal API are kept
-- // (anything else this process sends meanwhile, a Media read from a background job, is not the go-live call).
-- global.fetch = async (url, opts = {}) => {
-- if (String(url).endsWith('/oauth/token')) return { ok: true, status: 200, json: async () => ({ access_token: 'golive-service-token', expires_in: 300, token_type: 'Bearer' }) };
-- if (String(url).startsWith('http://127.0.0.1:9/internal/')) posted.push(String(opts.body || ''));
-- return { ok: true, status: 200, json: async () => ({}) };
-- };
-- try {
-- await require('./server/streaming/golive-notify').notifyFollowersGoLive(streamer, raw, { force: true });
-- for (let i = 0; i < 50 && !posted.length; i++) await new Promise((r) => setTimeout(r, 100));
-- } finally { global.fetch = realFetch; }
-- out.goLive = posted;
out.rawHadKey = raw.managed_stream_key === K.slotMain && streamer.stream_key === K.account;
return { ids: { owner, fan, peer, mod, admin, channel: ch.id, slot, slot2, live, ended, cam }, out };
})()`;
@@@ -145,10 +131,6 @@ function hits(text, needles) {
assert.match(out.sse, /stream-live/);
assert.deepStrictEqual(hits(out.sse, K), []);
});
-- await check('the go-live call to Network carries no credential', () => {
-- assert.ok(out.goLive.length >= 1, 'Network was called');
-- assert.deepStrictEqual(hits(out.goLive.join('\n'), K), []);
-- });
// ── The booted server ──
const keys = crawl.networkKeys(tmp);
ddiff --git a/test/slot-control-profiles.test.js b/test/slot-control-profiles.test.js
iindex ef0e7821..4118b8af 100644
---- a/test/slot-control-profiles.test.js
++++ b/test/slot-control-profiles.test.js
@@@ -56,7 +56,6 @@ require.cache[require.resolve('../server/streaming/broadcast-server')] = { id: '
const recorder = require('../server/streaming/recorder');
recorder.startRecording = () => {};
recorder.stopRecording = () => {};
--require('../server/streaming/golive-notify').notifyFollowersGoLive = () => {};
require('../server/streaming/live-events').announceGoLive = () => {};
const rtmp = require('../server/streaming/rtmp-server');
rtmp.start();