T-HUB/packages/connector-sdk/src/manifest.ts
glass beadian aa5e262f3a 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/.
2026-10-10 11:56:42 +00:00

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;
}