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

59
apps/api/src/db/client.ts Normal file
View file

@ -0,0 +1,59 @@
import { Pool, type PoolClient } from "pg";
import pino from "pino";
const logger = pino({ level: process.env.LOG_LEVEL ?? "info" });
export const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: Number(process.env.PG_POOL_MAX ?? 10),
});
/**
* Tenant-scoped transaction: sets app.workspace_id for the transaction so
* RLS policies and SECURITY DEFINER assertions see the tenant context.
* The setting is transaction-local (SET LOCAL) — it cannot leak.
*/
export async function withTenant<T>(
workspaceId: string,
fn: (client: PoolClient) => Promise<T>,
): Promise<T> {
const client = await pool.connect();
try {
await client.query("BEGIN");
await client.query("SELECT set_config('app.workspace_id', $1, true)", [workspaceId]);
const result = await fn(client);
await client.query("COMMIT");
return result;
} catch (err) {
await client.query("ROLLBACK").catch(() => undefined);
throw err;
} finally {
client.release();
}
}
/** System-level transaction (no tenant context): RLS sees NULL context and returns nothing tenant-scoped. */
export async function withSystem<T>(fn: (client: PoolClient) => Promise<T>): Promise<T> {
const client = await pool.connect();
try {
await client.query("BEGIN");
const result = await fn(client);
await client.query("COMMIT");
return result;
} catch (err) {
await client.query("ROLLBACK").catch(() => undefined);
throw err;
} finally {
client.release();
}
}
export async function checkDatabase(): Promise<boolean> {
try {
await pool.query("SELECT 1");
return true;
} catch (err) {
logger.warn({ err: String(err) }, "db: unreachable");
return false;
}
}

View file

@ -0,0 +1,6 @@
import { resolve } from "node:path";
import { runMigrations } from "./migrate.js";
const index = process.argv.indexOf("--dir");
void runMigrations(resolve(index < 0 ? "migrations" : process.argv[index + 1] ?? "migrations"))
.then((files) => console.log({ applied: files }))
.catch(() => { console.error("Migration failed; schema remains unready. Inspect the phase and database records before retrying."); process.exitCode = 1; });

View file

@ -0,0 +1,12 @@
import { describe, expect, it } from "vitest";
import { migrationPhases } from "./migrate.js";
describe("migration stage grammar", () => {
it("leaves 001–018 transactional and parses explicit stages", () => {
expect(migrationPhases("SELECT 1;")).toEqual([{ name: "", mode: "transaction", sql: "SELECT 1;" }]);
expect(migrationPhases("-- comment\n-- wp3:phase expand transaction\nSELECT 1;\n-- wp3:phase fill batch\nUPDATE t SET x=1;\n-- wp3:phase idx concurrent\nCREATE INDEX CONCURRENTLY t_idx ON t (x);").map((p) => [p.name, p.mode])).toEqual([["expand", "transaction"], ["fill", "batch"], ["idx", "concurrent"]]);
});
it("refuses duplicate phases and SQL before the first stage", () => {
expect(() => migrationPhases("-- wp3:phase expand transaction\nSELECT 1;\n-- wp3:phase expand transaction\nSELECT 2;")).toThrow(/duplicate/);
expect(() => migrationPhases("SELECT 0;\n-- wp3:phase expand transaction\nSELECT 1;")).toThrow(/before/);
});
});

View file

@ -0,0 +1,77 @@
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import { Pool, type PoolClient } from "pg";
export interface MigrationPhase { name: string; mode: "transaction" | "batch" | "concurrent"; sql: string }
/** Only explicit whole-line WP3 markers are accepted; old migrations stay transactional. */
export function migrationPhases(sql: string): MigrationPhase[] {
const marker = /^-- wp3:phase ([a-z][a-z0-9_]*) (transaction|batch|concurrent)\s*$/gm;
const matches = [...sql.matchAll(marker)];
if (matches.length === 0) return [{ name: "", mode: "transaction", sql }];
if (sql.slice(0, matches[0]?.index).split("\n").some((line) => line.trim() && !line.trim().startsWith("--"))) throw new Error("SQL before migration phase");
const phases = matches.map((match, index): MigrationPhase => ({ name: match[1]!, mode: match[2] as MigrationPhase["mode"], sql: sql.slice(match.index! + match[0].length, matches[index + 1]?.index).trim() }));
if (new Set(phases.map((p) => p.name)).size !== phases.length) throw new Error("duplicate migration phase");
return phases;
}
async function concurrentIndex(client: PoolClient, sql: string): Promise<void> {
const parsed = /^CREATE (UNIQUE )?INDEX CONCURRENTLY ([a-z][a-z0-9_]*) ON ([a-z][a-z0-9_]*) ([\s\S]+);$/.exec(sql);
if (!parsed) throw new Error("concurrent phase must contain exactly one CREATE INDEX");
const [, unique, name, table, tail] = parsed;
const found = await client.query<{ indisvalid: boolean; definition: string }>("SELECT i.indisvalid, pg_get_indexdef(i.indexrelid) AS definition FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'public' AND c.relname = $1", [name]);
const existing = found.rows[0];
if (existing) {
const expected = `CREATE ${unique ?? ""}INDEX ${name} ON public.${table} USING btree ${tail}`;
const normalize = (text: string): string => text.replace(/;$/, "").replace(/\s+/g, " ").trim();
if (normalize(existing.definition) !== normalize(expected)) throw new Error(`index ${name} definition differs`);
if (existing.indisvalid) return;
await client.query(`DROP INDEX CONCURRENTLY public.${name}`);
}
await client.query(sql);
}
/** Owner-only offline runner; runtime applications never call this. */
export async function runMigrations(dir: string): Promise<string[]> {
if (!process.env.SUPER_DATABASE_URL) throw new Error("SUPER_DATABASE_URL is required for migrations");
const files = (await readdir(dir)).filter((f) => f.endsWith(".sql")).sort();
const applied: string[] = [];
const pool = new Pool({ connectionString: process.env.SUPER_DATABASE_URL });
const client = await pool.connect();
try {
await client.query("SELECT pg_advisory_lock(193674839, 3)");
await client.query("SET lock_timeout = '5s'");
await client.query("CREATE TABLE IF NOT EXISTS schema_migrations (name text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())");
for (const file of files) {
if ((await client.query("SELECT 1 FROM schema_migrations WHERE name = $1", [file])).rowCount !== 0) continue;
for (const phase of migrationPhases(await readFile(join(dir, file), "utf8"))) {
const key = phase.name ? `${file}:${phase.name}` : file;
if ((await client.query("SELECT 1 FROM schema_migrations WHERE name = $1", [key])).rowCount !== 0) continue;
try {
if (phase.mode === "concurrent") {
await concurrentIndex(client, phase.sql);
await client.query("INSERT INTO schema_migrations(name) VALUES ($1)", [key]);
} else {
for (;;) {
await client.query("BEGIN");
const result = await client.query(phase.sql);
if (phase.mode === "batch" && (result.rowCount ?? 0) > 0) { await client.query("COMMIT"); continue; }
await client.query("INSERT INTO schema_migrations(name) VALUES ($1)", [key]);
await client.query("COMMIT");
break;
}
}
} catch (error) {
await client.query("ROLLBACK").catch(() => undefined);
throw new Error(`migration ${key} failed`, { cause: error });
}
}
await client.query("INSERT INTO schema_migrations(name) VALUES ($1) ON CONFLICT DO NOTHING", [file]);
applied.push(file);
}
} finally {
await client.query("SELECT pg_advisory_unlock(193674839, 3)").catch(() => undefined);
client.release();
await pool.end();
}
return applied;
}

View file

@ -0,0 +1,326 @@
import { randomUUID } from "node:crypto";
import { join } from "node:path";
import { Pool, type PoolClient } from "pg";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { runMigrations } from "./migrate.js";
/**
* Migration 013 (streaming S1) against a live PostgreSQL: the same-channel
* invariant fails at the DB constraint, the production CHECKs hold, and FORCE
* RLS denies every new table across tenants. Assertions run as sp_app
* (DATABASE_URL); setup/cleanup as owner. Skipped without DATABASE_URL.
*/
const dbAvailable = process.env.DATABASE_URL !== undefined;
const ownerPool = new Pool({ connectionString: process.env.SUPER_DATABASE_URL ?? process.env.DATABASE_URL });
const appPool = new Pool({ connectionString: process.env.DATABASE_URL });
const FK_VIOLATION = "23503";
const UNIQUE_VIOLATION = "23505";
const CHECK_VIOLATION = "23514";
const RLS_VIOLATION = "42501";
const ASSET = "a".repeat(64);
async function asTenant<T>(workspaceId: string | null, fn: (client: PoolClient) => Promise<T>): Promise<T> {
const client = await appPool.connect();
try {
await client.query("BEGIN");
if (workspaceId !== null) await client.query("SELECT set_config('app.workspace_id', $1, true)", [workspaceId]);
const result = await fn(client);
await client.query("COMMIT");
return result;
} catch (error) {
await client.query("ROLLBACK").catch(() => undefined);
throw error;
} finally {
client.release();
}
}
async function one(client: PoolClient, sql: string, params: unknown[]): Promise<string> {
const result = await client.query(sql, params);
return String(result.rows[0]?.id);
}
interface Channel { id: string; hls: string; playlist: string; scene: string; spare: string; slot: string; overlay: string; revision: string }
async function seedChannel(client: PoolClient, ws: string, assetId: string): Promise<Channel> {
const id = await one(client, "INSERT INTO virtual_channels (workspace_id, slug, name) VALUES ($1, $2, 'Channel') RETURNING id", [ws, `ch-${randomUUID().slice(0, 8)}`]);
const hls = await one(client,
"INSERT INTO virtual_channel_sources (workspace_id, virtual_channel_id, kind, label, remote_ref, hls_url) VALUES ($1, $2, 'hls', 'Cam 1', $3, 'https://cdn.example.com/a.m3u8') RETURNING id",
[ws, id, `ref-${randomUUID()}`]);
const playlist = await one(client,
"INSERT INTO virtual_channel_sources (workspace_id, virtual_channel_id, kind, label, playlist_loop) VALUES ($1, $2, 'playlist', 'Fallback reel', true) RETURNING id",
[ws, id]);
await client.query("INSERT INTO playlist_items (workspace_id, virtual_channel_id, source_id, position, video_asset_id) VALUES ($1, $2, $3, 0, $4)", [ws, id, playlist, assetId]);
const scene = await one(client, "INSERT INTO production_scenes (workspace_id, virtual_channel_id, label, layout) VALUES ($1, $2, 'Main', '2x2') RETURNING id", [ws, id]);
const spare = await one(client, "INSERT INTO production_scenes (workspace_id, virtual_channel_id, label, layout) VALUES ($1, $2, 'Spare', 'single') RETURNING id", [ws, id]);
const slot = await one(client,
"INSERT INTO scene_slots (workspace_id, virtual_channel_id, scene_id, slot_index, source_id, audio_mode, gain_db) VALUES ($1, $2, $3, 0, $4, 'primary', -6) RETURNING id",
[ws, id, scene, hls]);
await client.query("INSERT INTO production_buses (workspace_id, virtual_channel_id, bus, scene_id) VALUES ($1, $2, 'program', $3), ($1, $2, 'preview', $3)", [ws, id, scene]);
const overlay = await one(client, "INSERT INTO overlay_slots (workspace_id, virtual_channel_id, scene_id, kind) VALUES ($1, $2, $3, 'lower_third') RETURNING id", [ws, id, scene]);
const revision = await one(client,
"INSERT INTO overlay_revisions (workspace_id, virtual_channel_id, overlay_slot_id, template_id, render_state, asset_id) VALUES ($1, $2, $3, 'lower-third-v1', 'rendered', $4) RETURNING id",
[ws, id, overlay, ASSET]);
await client.query("UPDATE overlay_slots SET current_revision_id = $2 WHERE id = $1", [overlay, revision]);
return { id, hls, playlist, scene, spare, slot, overlay, revision };
}
describe.skipIf(!dbAvailable)("production schema 013 (live DB)", () => {
let wsA: string;
let wsB: string;
let assetA: string;
let assetB: string;
let a1: Channel;
let a2: Channel;
let b1: Channel;
beforeAll(async () => {
await runMigrations(process.env.MIGRATIONS_DIR ?? join(process.cwd(), "migrations"));
const insert = "INSERT INTO workspaces (name, slug) VALUES ($1, $2) RETURNING id";
wsA = String((await ownerPool.query(insert, ["Prod A", `prod-a-${randomUUID()}`])).rows[0]?.id);
wsB = String((await ownerPool.query(insert, ["Prod B", `prod-b-${randomUUID()}`])).rows[0]?.id);
const asset = "INSERT INTO video_assets (workspace_id, storage_key, mime_type, size_bytes) VALUES ($1, 'sha256/x.mp4', 'video/mp4', 1) RETURNING id";
assetA = await asTenant(wsA, (c) => one(c, asset, [wsA]));
assetB = await asTenant(wsB, (c) => one(c, asset, [wsB]));
a1 = await asTenant(wsA, (c) => seedChannel(c, wsA, assetA));
a2 = await asTenant(wsA, (c) => seedChannel(c, wsA, assetA));
b1 = await asTenant(wsB, (c) => seedChannel(c, wsB, assetB));
});
afterAll(async () => {
if (wsA !== undefined && wsB !== undefined) await ownerPool.query("DELETE FROM workspaces WHERE id IN ($1, $2)", [wsA, wsB]);
await appPool.end();
await ownerPool.end();
});
describe("same-channel invariant fails at the DB constraint", () => {
it("a slot cannot reference another channel's source (same workspace)", async () => {
await expect(asTenant(wsA, (c) => c.query(
"INSERT INTO scene_slots (workspace_id, virtual_channel_id, scene_id, slot_index, source_id) VALUES ($1, $2, $3, 1, $4)",
[wsA, a1.id, a1.scene, a2.hls]))).rejects.toMatchObject({ code: FK_VIOLATION });
});
it("a slot cannot claim another channel to smuggle a foreign scene or source", async () => {
await expect(asTenant(wsA, (c) => c.query(
"INSERT INTO scene_slots (workspace_id, virtual_channel_id, scene_id, slot_index, source_id) VALUES ($1, $2, $3, 1, $4)",
[wsA, a2.id, a1.scene, a2.hls]))).rejects.toMatchObject({ code: FK_VIOLATION });
});
it("a slot cannot be re-pointed to any source in place, nor moved to another scene", async () => {
await expect(asTenant(wsA, (c) => c.query("UPDATE scene_slots SET source_id = $2 WHERE id = $1", [a1.slot, a2.hls])))
.rejects.toMatchObject({ code: CHECK_VIOLATION });
await expect(asTenant(wsA, (c) => c.query("UPDATE scene_slots SET source_id = $2 WHERE id = $1", [a1.slot, a1.playlist])))
.rejects.toThrow(/source_id is immutable/);
await expect(asTenant(wsA, (c) => c.query("UPDATE scene_slots SET scene_id = $2 WHERE id = $1", [a1.slot, a1.spare])))
.rejects.toThrow(/scene_id is immutable/);
});
it("a bus cannot hold another channel's scene, by insert or by TAKE-style update", async () => {
await expect(asTenant(wsA, (c) => c.query("UPDATE production_buses SET scene_id = $2 WHERE virtual_channel_id = $1 AND bus = 'program'", [a1.id, a2.scene])))
.rejects.toMatchObject({ code: FK_VIOLATION });
const moved = await asTenant(wsA, (c) => c.query("UPDATE production_buses SET scene_id = $2 WHERE virtual_channel_id = $1 AND bus = 'preview'", [a1.id, a1.spare]));
expect(moved.rowCount).toBe(1);
});
it("overlay slots and revisions stay inside their channel", async () => {
await expect(asTenant(wsA, (c) => c.query(
"INSERT INTO overlay_slots (workspace_id, virtual_channel_id, scene_id, kind) VALUES ($1, $2, $3, 'logo')",
[wsA, a1.id, a2.scene]))).rejects.toMatchObject({ code: FK_VIOLATION });
await expect(asTenant(wsA, (c) => c.query(
"INSERT INTO overlay_revisions (workspace_id, virtual_channel_id, overlay_slot_id, template_id) VALUES ($1, $2, $3, 'logo')",
[wsA, a1.id, a2.overlay]))).rejects.toMatchObject({ code: FK_VIOLATION });
await expect(asTenant(wsA, (c) => c.query("UPDATE overlay_slots SET current_revision_id = $2 WHERE id = $1", [a1.overlay, a2.revision])))
.rejects.toMatchObject({ code: FK_VIOLATION });
});
it("a playlist item cannot name another channel's source or another tenant's asset", async () => {
await expect(asTenant(wsA, (c) => c.query(
"INSERT INTO playlist_items (workspace_id, virtual_channel_id, source_id, position, video_asset_id) VALUES ($1, $2, $3, 1, $4)",
[wsA, a1.id, a2.playlist, assetA]))).rejects.toMatchObject({ code: FK_VIOLATION });
await expect(asTenant(wsA, (c) => c.query(
"INSERT INTO playlist_items (workspace_id, virtual_channel_id, source_id, position, video_asset_id) VALUES ($1, $2, $3, 1, $4)",
[wsA, a1.id, a1.playlist, assetB]))).rejects.toMatchObject({ code: FK_VIOLATION });
});
});
describe("production constraints", () => {
it("allows at most one primary audio slot per scene", async () => {
await expect(asTenant(wsA, (c) => c.query(
"INSERT INTO scene_slots (workspace_id, virtual_channel_id, scene_id, slot_index, source_id, audio_mode) VALUES ($1, $2, $3, 1, $4, 'primary')",
[wsA, a1.id, a1.scene, a1.playlist]))).rejects.toMatchObject({ code: UNIQUE_VIOLATION });
await asTenant(wsA, (c) => c.query(
"INSERT INTO scene_slots (workspace_id, virtual_channel_id, scene_id, slot_index, source_id, audio_mode) VALUES ($1, $2, $3, 0, $4, 'primary'), ($1, $2, $5, 1, $4, 'on')",
[wsA, a1.id, a1.spare, a1.playlist, a1.scene]));
});
it("gain_db is -24..0 in 3 dB steps", async () => {
for (const bad of [-4, 3, -27, -1]) {
await expect(asTenant(wsA, (c) => c.query("UPDATE scene_slots SET gain_db = $2 WHERE id = $1", [a1.slot, bad])))
.rejects.toMatchObject({ code: CHECK_VIOLATION });
}
for (const good of [-24, -3, 0]) await asTenant(wsA, (c) => c.query("UPDATE scene_slots SET gain_db = $2 WHERE id = $1", [a1.slot, good]));
});
it("geometry is all-or-nothing, positive, and inside the frame", async () => {
const set = (sql: string) => asTenant(wsA, (c) => c.query(`UPDATE scene_slots SET ${sql} WHERE id = $1`, [a1.slot]));
await expect(set("x = 0, y = 0, w = 100")).rejects.toMatchObject({ code: CHECK_VIOLATION });
await expect(set("x = 0, y = 0, w = 0, h = 100")).rejects.toMatchObject({ code: CHECK_VIOLATION });
await expect(set("x = -1, y = 0, w = 100, h = 100")).rejects.toMatchObject({ code: CHECK_VIOLATION });
await expect(set("x = 9000, y = 0, w = 2000, h = 100")).rejects.toMatchObject({ code: CHECK_VIOLATION });
await set("x = 7000, y = 7000, w = 3000, h = 3000");
});
it("layouts, buses and source kinds are closed enums; playlist is a source kind", async () => {
await expect(asTenant(wsA, (c) => c.query("UPDATE production_scenes SET layout = '5x5' WHERE id = $1", [a1.spare])))
.rejects.toMatchObject({ code: CHECK_VIOLATION });
await expect(asTenant(wsA, (c) => c.query(
"INSERT INTO production_buses (workspace_id, virtual_channel_id, bus) VALUES ($1, $2, 'aux')", [wsA, a1.id])))
.rejects.toMatchObject({ code: CHECK_VIOLATION });
await expect(asTenant(wsA, (c) => c.query(
"INSERT INTO production_buses (workspace_id, virtual_channel_id, bus) VALUES ($1, $2, 'program')", [wsA, a1.id])))
.rejects.toMatchObject({ code: UNIQUE_VIOLATION });
await expect(asTenant(wsA, (c) => c.query(
"INSERT INTO virtual_channel_sources (workspace_id, virtual_channel_id, kind, label, hls_url) VALUES ($1, $2, 'rtmp', 'X', 'https://a.example/x')",
[wsA, a1.id]))).rejects.toMatchObject({ code: CHECK_VIOLATION });
const kinds = await asTenant(wsA, (c) => c.query("SELECT kind FROM virtual_channel_sources WHERE virtual_channel_id = $1 ORDER BY kind", [a1.id]));
expect(kinds.rows.map((r: { kind: string }) => r.kind)).toEqual(["hls", "playlist"]);
});
it("labels with filtergraph metacharacters are rejected by the schema too", async () => {
for (const label of ["a;b", "x[0]", "a,b", "it's", "movie=/etc/passwd"]) {
await expect(asTenant(wsA, (c) => c.query("UPDATE production_scenes SET label = $2 WHERE id = $1", [a1.spare, label])))
.rejects.toMatchObject({ code: CHECK_VIOLATION });
}
});
it("overlay kinds carry exactly their own parameters; rendered revisions carry a sha256 asset", async () => {
await expect(asTenant(wsA, (c) => c.query(
"INSERT INTO overlay_slots (workspace_id, virtual_channel_id, scene_id, kind) VALUES ($1, $2, $3, 'chyron')", [wsA, a1.id, a1.scene])))
.rejects.toMatchObject({ code: CHECK_VIOLATION });
await expect(asTenant(wsA, (c) => c.query(
"INSERT INTO overlay_slots (workspace_id, virtual_channel_id, scene_id, kind, chyron_speed_px_s, chyron_strip_width_px) VALUES ($1, $2, $3, 'chyron', 401, 100)",
[wsA, a1.id, a1.scene]))).rejects.toMatchObject({ code: CHECK_VIOLATION });
await asTenant(wsA, (c) => c.query(
"INSERT INTO overlay_slots (workspace_id, virtual_channel_id, scene_id, kind, chyron_speed_px_s, chyron_strip_width_px) VALUES ($1, $2, $3, 'chyron', 120, 16384)",
[wsA, a1.id, a1.scene]));
await expect(asTenant(wsA, (c) => c.query(
"INSERT INTO overlay_slots (workspace_id, virtual_channel_id, scene_id, kind) VALUES ($1, $2, $3, 'clock')", [wsA, a1.id, a1.scene])))
.rejects.toMatchObject({ code: CHECK_VIOLATION });
await expect(asTenant(wsA, (c) => c.query(
"INSERT INTO overlay_revisions (workspace_id, virtual_channel_id, overlay_slot_id, template_id, render_state, asset_id) VALUES ($1, $2, $3, 'lt', 'rendered', 'movie=x')",
[wsA, a1.id, a1.overlay]))).rejects.toMatchObject({ code: CHECK_VIOLATION });
await expect(asTenant(wsA, (c) => c.query(
"INSERT INTO overlay_revisions (workspace_id, virtual_channel_id, overlay_slot_id, template_id, render_state) VALUES ($1, $2, $3, 'lt', 'rendered')",
[wsA, a1.id, a1.overlay]))).rejects.toMatchObject({ code: CHECK_VIOLATION });
});
});
describe("lifecycle", () => {
it("deleting a source empties its slots (SET NULL keeps tenant + channel); a scene on a bus cannot be deleted", async () => {
const channel = await asTenant(wsA, (c) => seedChannel(c, wsA, assetA));
await asTenant(wsA, (c) => c.query("DELETE FROM virtual_channel_sources WHERE id = $1", [channel.hls]));
const slot = await asTenant(wsA, (c) => c.query("SELECT workspace_id, virtual_channel_id, source_id FROM scene_slots WHERE id = $1", [channel.slot]));
expect(slot.rows[0]).toEqual({ workspace_id: wsA, virtual_channel_id: channel.id, source_id: null });
await expect(asTenant(wsA, (c) => c.query("DELETE FROM production_scenes WHERE id = $1", [channel.scene])))
.rejects.toMatchObject({ code: FK_VIOLATION });
});
it("deleting a rendered revision clears the slot's current revision; deleting a channel cascades everything", async () => {
const channel = await asTenant(wsA, (c) => seedChannel(c, wsA, assetA));
await asTenant(wsA, (c) => c.query("DELETE FROM overlay_revisions WHERE id = $1", [channel.revision]));
const overlay = await asTenant(wsA, (c) => c.query("SELECT current_revision_id FROM overlay_slots WHERE id = $1", [channel.overlay]));
expect(overlay.rows[0]?.current_revision_id).toBeNull();
await asTenant(wsA, (c) => c.query("DELETE FROM virtual_channels WHERE id = $1", [channel.id]));
const left = await asTenant(wsA, (c) => c.query(
`SELECT (SELECT count(*) FROM virtual_channel_sources WHERE virtual_channel_id = $1)
+ (SELECT count(*) FROM production_scenes WHERE virtual_channel_id = $1)
+ (SELECT count(*) FROM scene_slots WHERE virtual_channel_id = $1)
+ (SELECT count(*) FROM production_buses WHERE virtual_channel_id = $1)
+ (SELECT count(*) FROM overlay_slots WHERE virtual_channel_id = $1)
+ (SELECT count(*) FROM overlay_revisions WHERE virtual_channel_id = $1)
+ (SELECT count(*) FROM playlist_items WHERE virtual_channel_id = $1) AS n`, [channel.id]));
expect(Number(left.rows[0]?.n)).toBe(0);
});
it("a playlisted video asset cannot be deleted directly, but a workspace purge removes everything", async () => {
await expect(asTenant(wsA, (c) => c.query("DELETE FROM video_assets WHERE id = $1", [assetA])))
.rejects.toMatchObject({ code: FK_VIOLATION });
const ws = String((await ownerPool.query("INSERT INTO workspaces (name, slug) VALUES ('Purge', $1) RETURNING id", [`purge-${randomUUID()}`])).rows[0]?.id);
const asset = await asTenant(ws, (c) => one(c,
"INSERT INTO video_assets (workspace_id, storage_key, mime_type, size_bytes) VALUES ($1, 'sha256/p.mp4', 'video/mp4', 1) RETURNING id", [ws]));
await asTenant(ws, (c) => seedChannel(c, ws, asset));
await ownerPool.query("DELETE FROM workspaces WHERE id = $1", [ws]);
const left = await ownerPool.query(
`SELECT (SELECT count(*) FROM virtual_channels WHERE workspace_id = $1)
+ (SELECT count(*) FROM playlist_items WHERE workspace_id = $1)
+ (SELECT count(*) FROM production_buses WHERE workspace_id = $1)
+ (SELECT count(*) FROM overlay_revisions WHERE workspace_id = $1) AS n`, [ws]);
expect(Number(left.rows[0]?.n)).toBe(0);
});
it("workspace and channel ownership columns are immutable", async () => {
await expect(asTenant(wsA, (c) => c.query("UPDATE production_scenes SET virtual_channel_id = $2 WHERE id = $1", [a1.spare, a2.id])))
.rejects.toThrow(/immutable/);
await expect(asTenant(wsA, (c) => c.query("UPDATE production_buses SET bus = 'preview' WHERE virtual_channel_id = $1 AND bus = 'program'", [a1.id])))
.rejects.toThrow(/bus is immutable/);
await expect(asTenant(wsA, (c) => c.query("UPDATE virtual_channels SET workspace_id = $2 WHERE id = $1", [a1.id, wsB])))
.rejects.toThrow(/immutable|row-level security/);
});
});
describe("RLS denial suite (FORCE, NULLIF tenant isolation)", () => {
const tables = [
"virtual_channels", "virtual_channel_sources", "playlist_items", "production_scenes",
"scene_slots", "production_buses", "overlay_slots", "overlay_revisions",
] as const;
it.each(tables)("%s has RLS enabled and forced", async (table) => {
const flags = await ownerPool.query("SELECT relrowsecurity, relforcerowsecurity FROM pg_class WHERE oid = $1::regclass", [table]);
expect(flags.rows[0]).toEqual({ relrowsecurity: true, relforcerowsecurity: true });
});
it.each(tables)("%s: tenant B sees, updates and deletes none of A's rows", async (table) => {
const seen = await asTenant(wsB, (c) => c.query(`SELECT count(*)::int AS n FROM ${table} WHERE workspace_id = $1`, [wsA]));
expect(seen.rows[0]?.n).toBe(0);
const updated = await asTenant(wsB, (c) => c.query(`UPDATE ${table} SET updated_at = now() WHERE workspace_id = $1`, [wsA]));
expect(updated.rowCount).toBe(0);
const deleted = await asTenant(wsB, (c) => c.query(`DELETE FROM ${table} WHERE workspace_id = $1`, [wsA]));
expect(deleted.rowCount).toBe(0);
const ownStillThere = await asTenant(wsA, (c) => c.query(`SELECT count(*)::int AS n FROM ${table}`));
expect(ownStillThere.rows[0]?.n).toBeGreaterThan(0);
});
it.each(tables)("%s: no tenant context (and an empty one) sees nothing", async (table) => {
const none = await asTenant(null, (c) => c.query(`SELECT count(*)::int AS n FROM ${table}`));
expect(none.rows[0]?.n).toBe(0);
const empty = await asTenant("", (c) => c.query(`SELECT count(*)::int AS n FROM ${table}`));
expect(empty.rows[0]?.n).toBe(0);
});
it("tenant B cannot write rows into A's workspace (WITH CHECK) on any table", async () => {
const attempts: Array<[string, unknown[]]> = [
["INSERT INTO virtual_channels (workspace_id, slug, name) VALUES ($1, 'x-rls', 'X')", [wsA]],
["INSERT INTO virtual_channel_sources (workspace_id, virtual_channel_id, kind, label, playlist_loop) VALUES ($1, $2, 'playlist', 'X', false)", [wsA, a1.id]],
["INSERT INTO playlist_items (workspace_id, virtual_channel_id, source_id, position, video_asset_id) VALUES ($1, $2, $3, 9, $4)", [wsA, a1.id, a1.playlist, assetA]],
["INSERT INTO production_scenes (workspace_id, virtual_channel_id, label, layout) VALUES ($1, $2, 'X', 'single')", [wsA, a1.id]],
["INSERT INTO scene_slots (workspace_id, virtual_channel_id, scene_id, slot_index) VALUES ($1, $2, $3, 3)", [wsA, a1.id, a1.scene]],
["INSERT INTO production_buses (workspace_id, virtual_channel_id, bus) VALUES ($1, $2, 'program')", [wsA, a1.id]],
["INSERT INTO overlay_slots (workspace_id, virtual_channel_id, scene_id, kind) VALUES ($1, $2, $3, 'logo')", [wsA, a1.id, a1.scene]],
["INSERT INTO overlay_revisions (workspace_id, virtual_channel_id, overlay_slot_id, template_id) VALUES ($1, $2, $3, 'lt')", [wsA, a1.id, a1.overlay]],
];
for (const [sql, params] of attempts) {
await expect(asTenant(wsB, (c) => c.query(sql, params)), sql).rejects.toMatchObject({ code: RLS_VIOLATION });
}
});
it("tenant B cannot point its own rows at A's channel, scene or source", async () => {
await expect(asTenant(wsB, (c) => c.query(
"INSERT INTO scene_slots (workspace_id, virtual_channel_id, scene_id, slot_index, source_id) VALUES ($1, $2, $3, 2, $4)",
[wsB, b1.id, b1.scene, a1.hls]))).rejects.toMatchObject({ code: FK_VIOLATION });
await expect(asTenant(wsB, (c) => c.query(
"UPDATE production_buses SET scene_id = $2 WHERE virtual_channel_id = $1 AND bus = 'program'", [b1.id, a1.scene])))
.rejects.toMatchObject({ code: FK_VIOLATION });
});
});
});

View file

@ -0,0 +1,68 @@
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Pool } from "pg";
import { runMigrations } from "./migrate.js";
import { assertPublishingSchema } from "@tactical-media-broadcast-hub/publishing-execution";
const ownerUrl = process.env.PUBLISHING_IT_SUPER_DATABASE_URL;
const appUrl = process.env.PUBLISHING_IT_DATABASE_URL;
describe.skipIf(!ownerUrl || !appUrl)("publishing migration upgrade and resumability (local DB)", () => {
let admin: Pool; let owner: Pool; let app: Pool; let dir: string;
let database: string; let previousOwner: string | undefined;
beforeAll(async () => {
const url = new URL(ownerUrl!); url.pathname = "/postgres";
admin = new Pool({ connectionString: url.toString() });
database = `wp3_migration_sol_${Date.now()}`;
await admin.query(`CREATE DATABASE ${database}`);
url.pathname = `/${database}`; previousOwner = process.env.SUPER_DATABASE_URL;
process.env.SUPER_DATABASE_URL = url.toString(); owner = new Pool({ connectionString: url.toString() });
const runtimeUrl = new URL(appUrl!); runtimeUrl.pathname = `/${database}`; app = new Pool({ connectionString: runtimeUrl.toString() });
dir = await mkdtemp(join(tmpdir(), "wp3-migrations-sol-"));
for (const file of (await readdir("migrations")).filter((name) => name.endsWith(".sql") && name < "018")) await writeFile(join(dir, file), await readFile(join("migrations", file)));
await runMigrations(dir);
}, 30_000);
afterAll(async () => {
if (previousOwner === undefined) delete process.env.SUPER_DATABASE_URL; else process.env.SUPER_DATABASE_URL = previousOwner;
await owner?.end(); await app?.end();
if (admin && database) await admin.query(`DROP DATABASE ${database}`);
await admin?.end();
});
it("upgrades legacy schedules without inventing authorization and resumes 019 after failed cutover", async () => {
const user = (await owner.query("INSERT INTO users(external_id, username, display_name) VALUES('migration-sol', 'migration-sol', 'Migration') RETURNING id")).rows[0].id as string;
const workspace = (await owner.query("INSERT INTO workspaces(name,slug) VALUES('Migration','migration-sol') RETURNING id")).rows[0].id as string;
const connection = (await owner.query("INSERT INTO connections(workspace_id,provider,channel_id) VALUES($1,'mastodon','channel') RETURNING id", [workspace])).rows[0].id as string;
const keys = ["legacy-pending", "legacy-flight", "legacy-done"];
for (const [index, key] of keys.entries()) {
const draft = (await owner.query("INSERT INTO drafts(workspace_id,author_user_id,post,state) VALUES($1,$2,'{}','scheduled') RETURNING id", [workspace, user])).rows[0].id as string;
await owner.query("INSERT INTO scheduled_publications(workspace_id,draft_id,connection_id,publish_at,idempotency_key,state) VALUES($1,$2,$3,now(),$4,$5)", [workspace, draft, connection, key, index === 1 ? "in_flight" : "pending"]);
if (index === 2) await owner.query("INSERT INTO publications(workspace_id,draft_id,connection_id,idempotency_key,remote_id,published_at) VALUES($1,$2,$3,$4,'remote',now())", [workspace, draft, connection, key]);
}
await writeFile(join(dir, "018_publishing_execution.sql"), await readFile("migrations/018_publishing_execution.sql"));
await runMigrations(dir);
const upgraded = await owner.query("SELECT state, scheduled_by_user_id, execution_version FROM scheduled_publications ORDER BY idempotency_key");
expect(upgraded.rows.map((row) => row.state)).toEqual(["done", "failed", "needs_reauthorization"]);
expect(upgraded.rows.every((row) => row.scheduled_by_user_id === null && row.execution_version === 0)).toBe(true);
const original = await readFile("migrations/019_publishing_integration.sql", "utf8");
await writeFile(join(dir, "019_publishing_integration.sql"), original.replace("DROP FUNCTION publishing_due_schedules(integer);", "SELECT nonexistent_cutover_function();"));
await expect(runMigrations(dir)).rejects.toThrow("cutover failed");
expect((await owner.query("SELECT name FROM schema_migrations WHERE name = '019_publishing_integration.sql'")).rowCount).toBe(0);
await expect(assertPublishingSchema(app)).rejects.toThrow();
expect((await owner.query("SELECT name FROM schema_migrations WHERE name LIKE '019%:%'")).rowCount).toBe(3);
await writeFile(join(dir, "019_publishing_integration.sql"), original);
await expect(runMigrations(dir)).resolves.toEqual(["019_publishing_integration.sql"]);
await expect(assertPublishingSchema(app)).resolves.toBeUndefined();
await expect(runMigrations(dir)).resolves.toEqual([]);
const role = (await app.query("SELECT current_user, rolsuper, rolbypassrls FROM pg_roles WHERE rolname = current_user")).rows[0];
expect(role).toEqual({ current_user: "sp_app", rolsuper: false, rolbypassrls: false });
}, 30_000);
it("repairs an invalid concurrent index only when its definition matches", async () => {
const file = "020_test_concurrent.sql";
await writeFile(join(dir, file), "-- wp3:phase expand transaction\nCREATE TABLE migration_index_test(v integer); INSERT INTO migration_index_test VALUES(1),(1);\n-- wp3:phase idx concurrent\nCREATE UNIQUE INDEX CONCURRENTLY migration_index_test_idx ON migration_index_test (v);");
await expect(runMigrations(dir)).rejects.toThrow("idx failed");
expect((await owner.query("SELECT indisvalid FROM pg_index WHERE indexrelid = 'migration_index_test_idx'::regclass")).rows[0]?.indisvalid).toBe(false);
await owner.query("DELETE FROM migration_index_test WHERE ctid NOT IN (SELECT min(ctid) FROM migration_index_test GROUP BY v)");
await expect(runMigrations(dir)).resolves.toEqual([file]);
expect((await owner.query("SELECT indisvalid FROM pg_index WHERE indexrelid = 'migration_index_test_idx'::regclass")).rows[0]?.indisvalid).toBe(true);
}, 30_000);
});

View file

@ -0,0 +1,176 @@
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { join } from "node:path";
import { Pool } from "pg";
import { pool, withTenant, withSystem } from "./client.js";
import { runMigrations } from "./migrate.js";
import { PublishingExecutionRepository } from "@tactical-media-broadcast-hub/publishing-execution";
/**
* Audit-finding acceptance tests against a live PostgreSQL.
* Migrations + cleanup run via the owner pool (SUPER_DATABASE_URL);
* the assertions run as sp_app so FORCE RLS binds honestly.
* Skipped without DATABASE_URL.
*/
const dbAvailable = process.env.DATABASE_URL !== undefined;
const ownerPool = new Pool({ connectionString: process.env.SUPER_DATABASE_URL ?? process.env.DATABASE_URL });
async function asOwner<T>(fn: (client: import("pg").PoolClient) => Promise<T>): Promise<T> {
const client = await ownerPool.connect();
try {
return await fn(client);
} finally {
client.release();
}
}
describe.skipIf(!dbAvailable)("tenancy + audit chain (live DB)", () => {
let wsA: string;
let wsB: string;
beforeAll(async () => {
await runMigrations(process.env.MIGRATIONS_DIR ?? join(process.cwd(), "migrations"));
await asOwner(async (client) => {
await client.query("DELETE FROM audit_log");
await client.query("DELETE FROM memberships");
await client.query("DELETE FROM connections");
await client.query("DELETE FROM credentials");
await client.query("DELETE FROM workspaces");
const a = await client.query("INSERT INTO workspaces (name, slug) VALUES ('A', $1) RETURNING id", [`a-${Date.now()}`]);
const b = await client.query("INSERT INTO workspaces (name, slug) VALUES ('B', $1) RETURNING id", [`b-${Date.now()}`]);
wsA = a.rows[0].id as string;
wsB = b.rows[0].id as string;
});
});
afterAll(async () => {
await asOwner(async (client) => {
await client.query("DELETE FROM workspaces WHERE id IN ($1, $2)", [wsA, wsB]);
});
await pool.end();
await ownerPool.end();
});
it("RLS isolates tenants: A sees only A's connections (FORCE applies to owner)", async () => {
await withTenant(wsA, async (client) => {
await client.query("INSERT INTO connections (workspace_id, provider, channel_id) VALUES ($1, 'mock-social', 'ch-a')", [wsA]);
});
await withTenant(wsB, async (client) => {
await client.query("INSERT INTO connections (workspace_id, provider, channel_id) VALUES ($1, 'mock-social', 'ch-b')", [wsB]);
});
const seenFromA = await withTenant(wsA, async (client) => {
return (await client.query("SELECT channel_id FROM connections")).rows.map((r) => r.channel_id);
});
expect(seenFromA).toEqual(["ch-a"]);
const seenFromSystem = await withSystem(async (client) => {
return (await client.query("SELECT count(*)::int AS n FROM connections")).rows[0]?.n;
});
expect(seenFromSystem).toBe(0);
});
it("SECURITY DEFINER tenant assertion: cross-tenant call rejected (finding #3)", async () => {
await expect(
withTenant(wsB, async (client) => {
await client.query("SELECT audit_append($1, NULL, 'system', 'x', 'y', 'z', NULL)", [wsA]);
}),
).rejects.toThrow(/tenant context mismatch/);
});
it("audit branch: tenant context => tenant chain even for service callers (findings #1/#6)", async () => {
await withTenant(wsA, async (client) => {
const r = await client.query("SELECT audit_append($1, NULL, 'sp_scheduler', 'job.ran', 'job', 'j1', NULL) AS id", [wsA]);
expect(r.rows[0]?.id).toBeTruthy();
const row = await client.query("SELECT workspace_id, actor_role FROM audit_log WHERE id = $1", [r.rows[0].id]);
expect(row.rows[0]?.workspace_id).toBe(wsA);
expect(row.rows[0]?.actor_role).toBe("sp_scheduler");
// system append inside tenant context must be refused
await expect(client.query("SELECT audit_append_system(NULL, 'x', 'y', 'z', NULL)")).rejects.toThrow();
});
});
it("membership change bumps authz_epoch => sessions fail closed (finding #2)", async () => {
const before = Number(await withSystem(async (c) => (await c.query("SELECT authz_epoch FROM workspaces WHERE id = $1", [wsA])).rows[0]?.authz_epoch));
await asOwner(async (c) => {
const u = await c.query("INSERT INTO users (external_id, email, display_name) VALUES ($1, 'e@x', 'E') RETURNING id", [`ext-${Date.now()}`]);
await withTenant(wsA, async (tenantClient) => {
await tenantClient.query("INSERT INTO memberships (workspace_id, user_id, role) VALUES ($1, $2, 'Publisher')", [wsA, u.rows[0].id]);
});
});
const after = Number(await withSystem(async (c) => (await c.query("SELECT authz_epoch FROM workspaces WHERE id = $1", [wsA])).rows[0]?.authz_epoch));
expect(after).toBeGreaterThan(before);
});
it("hash chain detects tampering", async () => {
await withTenant(wsA, async (client) => {
await client.query("SELECT audit_append($1, NULL, 'system', 't.1', 't', '1', NULL)", [wsA]);
await client.query("SELECT audit_append($1, NULL, 'system', 't.2', 't', '2', NULL)", [wsA]);
});
const ok = await withTenant(wsA, async (c) => (await c.query("SELECT audit_verify_chain($1) AS ok", [wsA])).rows[0]?.ok);
expect(ok).toBe(true);
// direct tamper (as owner, inside the tenant context, bypassing the append fn)
await withTenant(wsA, async (c) => {
await c.query("UPDATE audit_log SET action = 'forged' WHERE action = 't.1'");
});
const broken = await withTenant(wsA, async (c) => (await c.query("SELECT audit_verify_chain($1) AS ok", [wsA])).rows[0]?.ok);
expect(broken).toBe(false);
});
it("verifies a legacy v1 row followed by v2 and detects a changed v2 diff", async () => {
await withTenant(wsB, async (client) => {
await client.query(
`WITH stamp AS MATERIALIZED (SELECT clock_timestamp() AS at)
INSERT INTO audit_log (workspace_id, action, target_type, target_id, diff, at, prev_hash, hash, hash_version)
SELECT $1, 'legacy', 'test', 'legacy', '{"value":1}'::jsonb, stamp.at, NULL,
audit_hash(NULL, NULL, 'legacy', 'legacy', stamp.at), 1 FROM stamp`,
[wsB],
);
await client.query("SELECT audit_append($1, NULL, 'system', 'modern', 'test', 'modern', $2)", [wsB, { value: 2 }]);
const verified = await client.query("SELECT audit_verify_chain($1) AS ok", [wsB]);
expect(verified.rows[0]?.ok).toBe(true);
await client.query("UPDATE audit_log SET diff = $2 WHERE workspace_id = $1 AND action = 'modern'", [wsB, { value: 3 }]);
const broken = await client.query("SELECT audit_verify_chain($1) AS ok", [wsB]);
expect(broken.rows[0]?.ok).toBe(false);
});
});
it("scheduler claim refuses a pending schedule whose draft is not approved", async () => {
const repository = new PublishingExecutionRepository(pool);
try {
const user = await asOwner(async (client) => {
const inserted = await client.query(
"INSERT INTO users (external_id, email, display_name) VALUES ($1, 'claim@example.com', 'Claim') RETURNING id",
[`claim-${Date.now()}`],
);
return inserted.rows[0]?.id as string;
});
const key = `claim-${Date.now()}`;
await withTenant(wsA, async (client) => {
const connection = await client.query(
"INSERT INTO connections (workspace_id, provider, channel_id) VALUES ($1, 'mock-social', 'claim') RETURNING id",
[wsA],
);
const draft = await client.query(
"INSERT INTO drafts (workspace_id, author_user_id, post, state) VALUES ($1, $2, $3, 'in_review') RETURNING id",
[wsA, user, { text: "unapproved", mediaAssetIds: [], visibility: "public", providerMetadata: {} }],
);
await client.query(
"INSERT INTO scheduled_publications (workspace_id, draft_id, connection_id, publish_at, idempotency_key) VALUES ($1, $2, $3, now(), $4)",
[wsA, draft.rows[0]?.id, connection.rows[0]?.id, key],
);
});
const schedule = await withTenant(wsA, (client) => client.query("SELECT id, generation FROM scheduled_publications WHERE idempotency_key = $1", [key]));
await expect(repository.claim({ workspaceId: wsA, scheduleId: String(schedule.rows[0]?.id), generation: String(schedule.rows[0]?.generation), idempotencyKey: key })).resolves.toMatchObject({ kind: "failed", code: "EXECUTION_FENCE_MISSING" });
const state = await withTenant(wsA, (client) => client.query("SELECT state FROM scheduled_publications WHERE idempotency_key = $1", [key]));
expect(state.rows[0]?.state).toBe("pending");
} finally {
// The shared repository uses the caller-owned pool, closed by suite cleanup.
}
});
it("workspace_id is immutable on tenant rows (finding #8)", async () => {
await expect(
withTenant(wsA, async (c) => {
await c.query("UPDATE connections SET workspace_id = $1 WHERE channel_id = 'ch-a'", [wsB]);
}),
).rejects.toThrow(/workspace_id is immutable/);
});
});