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/.
109 lines
4.4 KiB
TypeScript
109 lines
4.4 KiB
TypeScript
import type { PlatformConnector } from "./types.js";
|
|
import type { ConnectorCapabilities } from "@tactical-media-broadcast-hub/core";
|
|
|
|
export interface ConnectionField {
|
|
name: string;
|
|
label: string;
|
|
required: boolean;
|
|
/** Field is secret — UI masks it, vault stores it. */
|
|
secret: boolean;
|
|
/** Optional validation pattern (regex source). */
|
|
pattern?: string;
|
|
placeholder?: string;
|
|
}
|
|
|
|
export interface ConnectorManifest {
|
|
/** Unique provider id, e.g. "mock-social". Dots/kebab-case, no spaces. */
|
|
id: string;
|
|
displayName: string;
|
|
capabilities: ConnectorCapabilities;
|
|
/** Fields the UI renders when adding a connection (e.g. instanceUrl). */
|
|
connectionFields: ConnectionField[];
|
|
/** OAuth flavor for the generic authorization flow. */
|
|
oauth?: {
|
|
kind: "oauth2-redirect" | "token" | "per-instance-oauth2";
|
|
authorizeUrlTemplate?: string;
|
|
scopes?: string[];
|
|
pkce?: boolean;
|
|
/** False when this deployment cannot support a redirect connect flow. */
|
|
connectEnabled?: boolean;
|
|
connectNote?: string;
|
|
/** Token providers may require a publishing target besides the token. */
|
|
targetField?: string;
|
|
};
|
|
/**
|
|
* Streams-column capability (WP3 §2.2): the connector implements
|
|
* `search()` for keyword/hashtag columns. Optional so existing manifests
|
|
* compile untouched; boolean when present, and must agree with the method
|
|
* actually on the connector (validated in `assertValidManifest`).
|
|
*/
|
|
search?: boolean;
|
|
/**
|
|
* Streams-column capability (WP3 §2.2): the connector implements
|
|
* `getUserFeed()` for account-list columns. Same optionality/consistency
|
|
* rules as `search`.
|
|
*/
|
|
authorFeed?: boolean;
|
|
/** Base route this connector's webhooks are served under (verification is connector-side). */
|
|
webhookRoute?: string;
|
|
docsUrl?: string;
|
|
icon?: string;
|
|
}
|
|
|
|
/** Streams-column capability flags (WP3 §2.2) and the method each gates. */
|
|
export const STREAMS_COLUMN_FLAGS = [
|
|
{ flag: "search", method: "search" },
|
|
{ flag: "authorFeed", method: "getUserFeed" },
|
|
] as const satisfies ReadonlyArray<{ flag: "search" | "authorFeed"; method: "search" | "getUserFeed" }>;
|
|
|
|
/**
|
|
* Validate a manifest. When a connector is supplied, streams-column capability
|
|
* flags are also cross-checked against the connector surface: a declared flag
|
|
* without the method, or a method behind an undeclared flag, is an error —
|
|
* mirrors never lie (same rule the contract kits enforce).
|
|
*/
|
|
export function assertValidManifest(m: ConnectorManifest, connector?: PlatformConnector): void {
|
|
if (!/^[a-z0-9][a-z0-9.-]*$/.test(m.id)) {
|
|
throw new Error(`manifest.id "${m.id}" must be kebab-case`);
|
|
}
|
|
if (m.id.length < 2) throw new Error("manifest.id too short");
|
|
if (!m.displayName) throw new Error("manifest.displayName required");
|
|
if (!m.capabilities) throw new Error("manifest.capabilities required");
|
|
for (const field of m.connectionFields) {
|
|
if (field.secret && field.name === "password") {
|
|
throw new Error("connectionFields must not include a password field — tokens only");
|
|
}
|
|
}
|
|
const caps = Object.entries(m.capabilities).filter(([, v]) => v === true).length;
|
|
if (caps === 0) throw new Error("manifest must declare at least one capability");
|
|
for (const { flag, method } of STREAMS_COLUMN_FLAGS) {
|
|
const value = m[flag];
|
|
if (value !== undefined && typeof value !== "boolean") {
|
|
throw new Error(`manifest.${flag} must be a boolean when present`);
|
|
}
|
|
if (connector === undefined) continue;
|
|
const implemented = typeof connector[method] === "function";
|
|
if (value === true && !implemented) {
|
|
throw new Error(`manifest.${flag} declared but connector.${method}() is not implemented`);
|
|
}
|
|
if (value !== true && implemented) {
|
|
throw new Error(
|
|
`connector.${method}() implemented but manifest.${flag} is not true — undeclared capability`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Capability strings for `/feeds` meta: the existing capability strings plus
|
|
* the streams-column flags when declared ("search", "authorFeed"), so the UI
|
|
* gates keyword/author affordances with zero new endpoints (WP3 §2.2).
|
|
*/
|
|
export function manifestCapabilityStrings(m: ConnectorManifest): string[] {
|
|
const strings = Object.entries(m.capabilities)
|
|
.filter(([, v]) => v === true)
|
|
.map(([k]) => k);
|
|
if (m.search === true) strings.push("search");
|
|
if (m.authorFeed === true) strings.push("authorFeed");
|
|
return strings;
|
|
}
|