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:
commit
aa5e262f3a
880 changed files with 111042 additions and 0 deletions
109
packages/connector-sdk/src/manifest.ts
Normal file
109
packages/connector-sdk/src/manifest.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue