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/.
76 lines
5.4 KiB
TypeScript
76 lines
5.4 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import type { PoolClient } from "pg";
|
|
import { describe, expect, it } from "vitest";
|
|
import type { ConnectorConnection } from "@tactical-media-broadcast-hub/core";
|
|
import type { SessionPayload } from "../auth/session.js";
|
|
import { Vault } from "../vault/vault.js";
|
|
import { PgConnectionStore } from "./connection-store.js";
|
|
|
|
const session: SessionPayload = { workspaceId: randomUUID(), userId: randomUUID(), role: "Owner", authzEpoch: 3, issuedAt: 0, expiresAt: Math.floor(Date.now() / 1000) + 600 };
|
|
const connection: ConnectorConnection = { id: randomUUID(), workspaceId: session.workspaceId, provider: "mastodon", channelId: "remote-account", credentialId: { kind: "credential-ref", id: "opaque" }, fields: { instanceUrl: "https://social.example" }, createdAt: new Date() };
|
|
|
|
function fixture(allowed = true, existingCredential = false) {
|
|
const queries: Array<{ sql: string; params: unknown[] }> = [];
|
|
const credentialId = randomUUID();
|
|
const client = {
|
|
async query(sql: string, params: unknown[] = []) {
|
|
queries.push({ sql, params });
|
|
if (sql.includes("FROM memberships")) return { rowCount: allowed ? 1 : 0, rows: [] };
|
|
if (sql.startsWith("UPDATE scheduled_publications")) return { rowCount: 1, rows: [{ id: connection.id, draft_id: connection.id }] };
|
|
if (sql.includes("SELECT id FROM credentials")) return { rows: existingCredential ? [{ id: credentialId }] : [] };
|
|
if (sql.includes("SELECT c.*, cr.ciphertext")) return { rows: [{ id: connection.id, provider: connection.provider, channel_id: connection.channelId, fields: connection.fields, created_at: connection.createdAt, ciphertext: null, key_version: 1 }] };
|
|
return { rowCount: 1, rows: [{ id: connection.id }] };
|
|
},
|
|
} as unknown as PoolClient;
|
|
const store = new PgConnectionStore(async (workspaceId, callback) => {
|
|
expect(workspaceId).toBe(session.workspaceId);
|
|
return callback(client);
|
|
});
|
|
return { store, queries, credentialId };
|
|
}
|
|
|
|
describe("Postgres connection writes", () => {
|
|
it("saves only a vault envelope and identifier-only audit in the same tenant transaction", async () => {
|
|
const { store, queries } = fixture();
|
|
const sealed = await new Vault().seal(session.workspaceId, connection.id, Buffer.from('{"access_token":"never-in-db-plaintext"}'));
|
|
await store.save(session, connection, { provider: connection.provider, remoteId: "remote-account", handle: "@alice" }, sealed, false);
|
|
const credential = queries.find(({ sql }) => sql.startsWith("INSERT INTO credentials"));
|
|
expect(credential?.params).toContain(sealed.ciphertext);
|
|
expect(queries[0]?.sql).toContain("FOR UPDATE");
|
|
expect(JSON.stringify(queries)).not.toContain("never-in-db-plaintext");
|
|
const audit = queries.find(({ sql }) => sql.includes("audit_append"));
|
|
expect(audit?.params.at(-1)).toEqual({ provider: connection.provider });
|
|
});
|
|
it("rotates the credential row in place and blocks a revoked authorization before writes", async () => {
|
|
const { store, queries, credentialId } = fixture(true, true);
|
|
const sealed = await new Vault().seal(session.workspaceId, connection.id, Buffer.from("rotated-secret"));
|
|
await store.save(session, connection, { provider: connection.provider, remoteId: "remote-account", handle: "@alice" }, sealed, true);
|
|
expect(queries.find(({ sql }) => sql.startsWith("UPDATE credentials"))?.params[0]).toBe(credentialId);
|
|
expect(queries.some(({ sql }) => sql.startsWith("INSERT INTO connections"))).toBe(false);
|
|
const denied = fixture(false);
|
|
await expect(denied.store.save(session, connection, { provider: connection.provider, remoteId: "remote-account", handle: "@alice" }, sealed, true)).rejects.toThrow();
|
|
expect(denied.queries.some(({ sql }) => /^(INSERT|UPDATE)/.test(sql))).toBe(false);
|
|
});
|
|
it("locks and revokes locally before provider I/O while preserving publishing history", async () => {
|
|
const { store, queries } = fixture();
|
|
await store.remove(session, connection.id, async (stored) => {
|
|
expect(stored.connection.id).toBe(connection.id);
|
|
expect(queries.some(({ sql }) => sql.includes("FOR UPDATE"))).toBe(true);
|
|
expect(queries.some(({ sql }) => sql.startsWith("DELETE"))).toBe(false);
|
|
});
|
|
expect(queries.filter(({ sql }) => sql.startsWith("DELETE"))).toHaveLength(0);
|
|
expect(queries.some(({ sql }) => sql.startsWith("UPDATE credentials SET revoked_at"))).toBe(true);
|
|
expect(queries.some(({ sql }) => sql.startsWith("UPDATE connections SET disconnected_at"))).toBe(true);
|
|
expect(queries.some(({ sql }) => sql.includes("needs_reauthorization"))).toBe(true);
|
|
expect(queries.some(({ sql }) => sql.startsWith("UPDATE publishing_attempts") && sql.includes("abandoned"))).toBe(true);
|
|
expect(queries.some(({ sql }) => sql.startsWith("UPDATE drafts") && sql.includes("approved_revision = revision"))).toBe(true);
|
|
expect(queries.at(-1)?.sql).toContain("audit_append");
|
|
});
|
|
it("retains revoked local evidence and audits provider revocation failure", async () => {
|
|
const { store, queries } = fixture();
|
|
await expect(store.remove(session, connection.id, async () => { throw new Error("provider offline"); })).resolves.toBeUndefined();
|
|
expect(queries.some(({ sql }) => sql.startsWith("UPDATE credentials SET revoked_at"))).toBe(true);
|
|
expect(queries.at(-1)?.sql).toContain("connection.revoke_failed");
|
|
expect(queries.some(({ sql }) => sql.startsWith("DELETE"))).toBe(false);
|
|
});
|
|
});
|