Tactical Media Broadcast Hub — initial release

Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
This commit is contained in:
glass beadian 2026-10-10 11:56:42 +00:00
commit aa5e262f3a
880 changed files with 111042 additions and 0 deletions

View file

@ -0,0 +1,39 @@
import { BlockList, isIP } from "node:net";
export type EndpointUse = "hls" | "output";
export interface ValidatedEndpoint { url: URL; pinnedAddress: string }
/** WP7 supplies DNS pinning, redirect and HLS child-URI validation. */
export interface EndpointValidator {
validate(raw: string, use: EndpointUse): Promise<ValidatedEndpoint>;
}
const blocked = new BlockList();
for (const [subnet, prefix] of [
["0.0.0.0", 8], ["10.0.0.0", 8], ["100.64.0.0", 10], ["127.0.0.0", 8],
["169.254.0.0", 16], ["172.16.0.0", 12], ["192.0.0.0", 24], ["192.168.0.0", 16],
["192.0.2.0", 24], ["198.18.0.0", 15], ["198.51.100.0", 24],
["203.0.113.0", 24], ["224.0.0.0", 4], ["240.0.0.0", 4],
] as const) blocked.addSubnet(subnet, prefix, "ipv4");
for (const [subnet, prefix] of [
["::", 128], ["::1", 128], ["fc00::", 7], ["fe80::", 10], ["ff00::", 8],
["2001::", 32], ["2001:db8::", 32], ["2002::", 16], ["64:ff9b::", 96],
] as const) blocked.addSubnet(subnet, prefix, "ipv6");
export function assertPublicAddress(address: string): void {
const normalized = address.replace(/^\[|\]$/g, "").toLowerCase();
const family = isIP(normalized);
if (family === 0) throw new Error("unresolved endpoint address");
// Node's BlockList normalizes IPv4 to mapped IPv6 internally; reject mapped literals outright.
if (normalized.startsWith("::ffff:")) throw new Error("mapped endpoint rejected");
if (blocked.check(normalized, family === 4 ? "ipv4" : "ipv6")) throw new Error("private endpoint rejected");
}
export function validateEndpointShape(raw: string, use: EndpointUse): URL {
let url: URL;
try { url = new URL(raw); } catch { throw new Error("invalid endpoint URL"); }
const schemes = use === "hls" ? ["https:"] : ["rtmp:", "rtmps:"];
if (!schemes.includes(url.protocol) || !url.hostname || url.username || url.password || url.search || url.hash) throw new Error("endpoint scheme or credentials rejected");
if (isIP(url.hostname.replace(/^\[|\]$/g, ""))) assertPublicAddress(url.hostname);
return url;
}

View file

@ -0,0 +1,122 @@
import { spawn, type ChildProcess, type StdioOptions } from "node:child_process";
import { OUTPUT_PROFILES, type DesiredChannel } from "../types";
import { assertPublicAddress, validateEndpointShape, type EndpointValidator } from "./endpoints";
/** The vault broker delivers a key directly to the push relay's inherited fd. */
export interface VaultHandleBroker {
deliverPushTarget(handleId: string, context: { workspaceId: string; channelId: string; assignmentEpoch: number }, relayFd: number): Promise<void>;
}
export interface LocalRelayPipes { ingestFd: number; pushFd: number; close(): Promise<void> }
/** WP7 implementation fetches HLS through pinned egress and pushes RTMP with the vault key. */
export interface EgressRelayFactory {
prepare(config: DesiredChannel, validator: EndpointValidator, vault: VaultHandleBroker): Promise<LocalRelayPipes>;
}
export interface StderrRedactor { redact(chunk: string): string }
/** Media children get a minimal environment: no worker secrets, no inherited config. */
export function mediaEnv(): NodeJS.ProcessEnv { return { PATH: process.env.PATH ?? "/usr/bin:/bin" }; }
export interface ManagedProcess {
readonly child: ChildProcess;
/** Resolves on spawn, rejects if the binary cannot start. */
readonly started: Promise<void>;
/** Resolves once the process has exited (never rejects). */
readonly exited: Promise<{ code: number | null; signal: NodeJS.Signals | null }>;
kill(signal?: NodeJS.Signals): void;
/** SIGTERM, then SIGKILL after `graceMs`; resolves on exit. */
terminate(graceMs?: number): Promise<void>;
}
/**
* Shared spawn path for every media child (relay, ingest, compositor, encoder,
* frame gate): argv only, never a shell; minimal env; stderr is redacted and
* truncated before it reaches the logger.
*/
export function spawnManaged(
command: string, args: readonly string[], stdio: StdioOptions,
redactor: StderrRedactor, onStderr: (message: string) => void, cwd?: string,
): ManagedProcess {
const child = spawn(command, args, { stdio, shell: false, env: mediaEnv(), ...(cwd === undefined ? {} : { cwd }) });
child.stderr?.on("data", (chunk: Buffer) => onStderr(redactor.redact(chunk.toString("utf8").slice(0, 4096))));
const started = new Promise<void>((resolve, reject) => {
child.once("spawn", () => resolve());
child.once("error", reject);
});
started.catch(() => undefined);
const exited = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve) => {
child.once("exit", (code, signal) => resolve({ code, signal }));
child.once("error", () => { if (child.pid === undefined) resolve({ code: null, signal: null }); });
});
const kill = (signal: NodeJS.Signals = "SIGTERM"): void => { if (child.exitCode === null && child.signalCode === null) child.kill(signal); };
return {
child, started, exited, kill,
async terminate(graceMs = 2_000) {
kill("SIGTERM");
const timer = setTimeout(() => kill("SIGKILL"), graceMs);
await exited;
clearTimeout(timer);
},
};
}
export function ffmpegArgs(config: DesiredChannel): string[] {
const args = ["-hide_banner", "-nostdin", "-loglevel", "warning", "-f", "mpegts", "-i", "pipe:3"];
if (config.mode === "passthrough") args.push("-c", "copy");
else {
const profile = OUTPUT_PROFILES[config.profile];
args.push("-vf", `scale=${profile.width}:${profile.height},fps=${profile.fps}`,
"-c:v", "libx264", "-preset", "veryfast", "-b:v", profile.videoBitrate,
"-c:a", "aac", "-b:a", "128k");
}
args.push("-f", "flv", "pipe:4");
return args;
}
export class FfmpegRelayJob {
private child: ChildProcess | null = null;
private pipes: LocalRelayPipes | null = null;
private stopping = false;
constructor(
private readonly validator: EndpointValidator,
private readonly egress: EgressRelayFactory,
private readonly vault: VaultHandleBroker,
private readonly redactor: StderrRedactor,
private readonly onStderr: (message: string) => void,
private readonly onExit: (reason: string) => void,
) {}
async start(config: DesiredChannel): Promise<void> {
if (!config.source || !config.output) throw new Error("running channel needs source and output");
validateEndpointShape(config.source.url, "hls");
validateEndpointShape(config.output.endpointUrl, "output");
const source = await this.validator.validate(config.source.url, "hls");
const output = await this.validator.validate(config.output.endpointUrl, "output");
assertPublicAddress(source.pinnedAddress);
assertPublicAddress(output.pinnedAddress);
this.pipes = await this.egress.prepare(config, this.validator, this.vault);
this.stopping = false;
const managed = spawnManaged("ffmpeg", ffmpegArgs(config),
["ignore", "ignore", "pipe", this.pipes.ingestFd, this.pipes.pushFd], this.redactor, this.onStderr);
const child = managed.child;
this.child = child;
let spawned = false;
void managed.started.then(() => { spawned = true; }, () => undefined);
child.on("error", (error) => { if (spawned) this.onExit(this.redactor.redact(error.message)); });
const started = managed.started;
child.once("exit", (code, signal) => {
void this.pipes?.close();
this.pipes = null;
this.child = null;
if (!this.stopping) this.onExit(`ffmpeg exited code=${code} signal=${signal}`);
});
await started;
}
async stop(): Promise<void> {
this.stopping = true;
this.child?.kill("SIGTERM");
await this.pipes?.close();
this.pipes = null;
this.child = null;
}
}

View file

@ -0,0 +1,56 @@
import assert from "node:assert/strict";
import { resolve } from "node:path";
import test from "node:test";
import { pathToFileURL } from "node:url";
import { assertPublicAddress, validateEndpointShape } from "./endpoints";
import { ffmpegArgs } from "./ffmpeg";
import type { DesiredChannel } from "../types";
const channel: DesiredChannel = {
workspaceId: "workspace-1", channelId: "channel-1", workerId: "worker-1",
assignmentEpoch: 1, desiredVersion: 1, desiredStatus: "RUNNING",
source: { kind: "hls", url: "https://mock.local/hls/live-1/master.m3u8" },
output: { endpointUrl: "rtmp://mock.local/live", credentialHandleId: "vault-handle" },
mode: "passthrough", profile: "720p30",
};
test("relay argv uses only local inherited pipes and mode profile", () => {
const passthrough = ffmpegArgs(channel);
assert.deepEqual(passthrough.slice(-3), ["-f", "flv", "pipe:4"]);
assert.ok(passthrough.includes("copy"));
assert.ok(!passthrough.join(" ").includes("mock.local"));
assert.ok(!passthrough.join(" ").includes("vault-handle"));
const normalized = ffmpegArgs({ ...channel, mode: "normalize", profile: "1080p60" });
assert.ok(normalized.includes("scale=1920:1080,fps=60"));
});
test("endpoint shape and literal/private pin denial", () => {
assert.equal(validateEndpointShape(channel.source?.url ?? "", "hls").protocol, "https:");
assert.throws(() => validateEndpointShape("file:///etc/passwd", "hls"));
assert.throws(() => validateEndpointShape("https://127.0.0.1/live.m3u8", "hls"));
assert.throws(() => validateEndpointShape("rtmp://169.254.169.254/live", "output"));
assert.throws(() => validateEndpointShape("rtmp://user:secret@example.com/live", "output"));
for (const address of ["10.0.0.1", "172.20.1.2", "192.168.1.1", "100.64.0.1", "::1", "fc00::1", "::ffff:127.0.0.1", "::ffff:7f00:1"]) {
assert.throws(() => assertPublicAddress(address), address);
}
assert.doesNotThrow(() => assertPublicAddress("8.8.8.8"));
});
test("mock livestream source and single-live simulator fixture feed the relay contract", async () => {
const mockPath = pathToFileURL(resolve(__dirname, "../../../../packages/connector-mock-livestream/src/index.ts")).href;
const fixturePath = pathToFileURL(resolve(__dirname, "../../../../packages/testing/src/scenarios.ts")).href;
const mockModule: unknown = await import(mockPath);
const fixtures: unknown = await import(fixturePath);
const mock = mockModule as { createMockLivestreamConnector(): {
createLiveStream(connection: null, request: { channelId: string; title: string; permanent: boolean; saveReplay: boolean }): Promise<{ remoteId: string }>;
getStreamPlaybackSources(connection: null, request: { remoteId: string }): Promise<Array<{ url: string; kind: string }>>;
} };
const scenarios = fixtures as { SCENARIOS: Record<string, { steps: Array<{ source: string; action: string }> }> };
assert.deepEqual(scenarios.SCENARIOS["single-live"]?.steps.map((step) => [step.source, step.action]), [["A", "go-live"]]);
const connector = mock.createMockLivestreamConnector();
const live = await connector.createLiveStream(null, { channelId: "A", title: "test", permanent: true, saveReplay: false });
const sources = await connector.getStreamPlaybackSources(null, { remoteId: live.remoteId });
assert.equal(sources[0]?.kind, "hls");
assert.equal(validateEndpointShape(sources[0]?.url ?? "", "hls").hostname, "mock.local");
assert.ok(!ffmpegArgs({ ...channel, source: { kind: "hls", url: sources[0]?.url ?? "" } }).join(" ").includes("mock.local"));
});