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,25 @@
# Publishing execution
The API and scheduler share this built PostgreSQL repository. Callers own the
`pg.Pool` and its lifecycle. `scheduleAttempt` and `cancelAttempt` run inside the
API's existing tenant transaction; other methods establish their own RLS-bound
transaction. Mutation order is workspace, draft, schedule, attempt, audit.
A business attempt gets a positive decimal attempt number and one provider key.
Transport retries, Temporal replay and lease recovery retain that key.
`retryLater` requires proof that the attempt has no unresolved delivery:
429/refusal of the current request cannot disprove an earlier ambiguous send.
Use `retryDeduplicated` for that case; it retains dispatch evidence and the
original dedupe deadline. Both settlements are owner/epoch/expiry fenced.
Unknown work cannot get a new key or send automatically.
The SDK around-send hook holds `withDispatchGate` across the bounded request,
after rate-limiter waits. Provider evidence is appended separately before
finalization, including evidence from stale owners. A current owner can finalize
a receipt after authorization changes; the audit records those changes. Receipt
recovery uses a separate workflow ID and never needs another provider send.
Runtime boot never executes owner DDL. Apply migrations through the API's offline
`migrate` CLI with an explicit `SUPER_DATABASE_URL`; API writes and scheduler
startup require completed 019, FORCE RLS, valid indexes and the `sp_app` role.
018 is retained unchanged because local evidence records its application.

View file

@ -0,0 +1,27 @@
{
"name": "@tactical-media-broadcast-hub/publishing-execution",
"private": true,
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": "./dist/index.js"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"lint": "eslint src",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "vitest run"
},
"dependencies": {
"@tactical-media-broadcast-hub/core": "workspace:*",
"pg": "^8.23.1"
},
"devDependencies": {
"@types/node": "^22.20.5",
"typescript": "^5.5.4",
"vitest": "^2.0.5",
"@types/pg": "^8.23.1"
}
}

View file

@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest";
import { publishingIdempotencyKey } from "@tactical-media-broadcast-hub/core";
import { canonicalPostHash } from "./approval.js";
describe("approval and attempt identity", () => {
it("sorts nested keys, excludes execution metadata, and preserves target and array order", () => {
const post = { text: "hi", mediaAssetIds: ["a", "b"], providerMetadata: { connectionId: "target", nested: { z: 2, a: 1 } } };
const reordered = { providerMetadata: { nested: { a: 1, z: 2 }, connectionId: "target", draftId: "injected", idempotencyKey: "key" }, mediaAssetIds: ["a", "b"], text: "hi" };
expect(canonicalPostHash(post)).toBe(canonicalPostHash(reordered));
expect(canonicalPostHash(post)).not.toBe(canonicalPostHash({ ...post, mediaAssetIds: ["b", "a"] }));
expect(canonicalPostHash(post)).not.toBe(canonicalPostHash({ ...post, providerMetadata: { ...post.providerMetadata, connectionId: "other" } }));
});
it("uses decimal bigint attempts without precision loss", () => {
expect(publishingIdempotencyKey({ workspaceId: "ws", draftId: "draft", attemptNo: "9007199254740993" })).toBe("wp3:ws:draft:a9007199254740993:publish");
expect(() => publishingIdempotencyKey({ workspaceId: "ws", draftId: "draft", attemptNo: Number.MAX_SAFE_INTEGER + 1 })).toThrow();
for (const attemptNo of ["0", "01", "-1", "1.1"]) expect(() => publishingIdempotencyKey({ workspaceId: "ws", draftId: "draft", attemptNo })).toThrow();
});
});

View file

@ -0,0 +1,13 @@
import { createHash } from "node:crypto";
function canonical(value: unknown): unknown {
if (Array.isArray(value)) return value.map(canonical);
if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => [key, canonical(item)]));
return value;
}
/** The target lives in providerMetadata.connectionId; execution keys are excluded. */
export function canonicalPostHash<T extends { providerMetadata: Record<string, unknown> }>(post: T): string {
const metadata = { ...post.providerMetadata };
delete metadata.idempotencyKey;
delete metadata.draftId;
return createHash("sha256").update(JSON.stringify(canonical({ ...post, providerMetadata: metadata }))).digest("hex");
}

View file

@ -0,0 +1,3 @@
export * from "./publishing.repository.js";
export * from "./approval.js";
export * from "./schema.js";

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
import type { Pool } from "pg";
/** No runtime DDL: code refuses an incomplete or incorrectly privileged schema. */
export async function assertPublishingSchema(pool: Pool): Promise<void> {
const result = await pool.query<{ ready: boolean }>(`SELECT
EXISTS (SELECT 1 FROM schema_migrations WHERE name = '019_publishing_integration.sql')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'publishing_attempts' AND column_name = 'first_dispatch_started_at')
AND (SELECT count(*) = 2 AND bool_and(relrowsecurity AND relforcerowsecurity) FROM pg_class WHERE oid IN ('publishing_attempts'::regclass, 'publishing_receipts'::regclass))
AND NOT EXISTS (SELECT 1 FROM pg_index WHERE indrelid IN ('publishing_attempts'::regclass, 'publishing_receipts'::regclass) AND NOT indisvalid)
AND current_user = 'sp_app'
AND NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = current_user AND (rolsuper OR rolbypassrls)) AS ready`);
if (result.rows[0]?.ready !== true) throw new Error("PUBLISHING_SCHEMA_NOT_READY");
}

View file

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}