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/.
54 lines
2.5 KiB
TypeScript
54 lines
2.5 KiB
TypeScript
/** Opt-in foundation config. Not loaded by API startup or the existing media worker. */
|
|
export interface FilesIntegrationConfig {
|
|
payloadStore: "r2";
|
|
localPayloadCache: false;
|
|
localMetadataAllowed: true;
|
|
r2: { endpoint: string; region: "auto"; forcePathStyle: true };
|
|
buckets: { matrixMedia: string; filesBlobs: string; posterMedia: string };
|
|
filesUrl: string;
|
|
officeUrl: string;
|
|
/** Disabled until a pad adapter can keep text/revision payloads in R2. */
|
|
padsEnabled: false;
|
|
}
|
|
|
|
type Environment = Readonly<Record<string, string | undefined>>;
|
|
|
|
function httpsOrigin(value: string, name: string): string {
|
|
const url = new URL(value);
|
|
if (url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
throw new Error(`${name} must be an HTTPS origin without credentials, path or query`);
|
|
}
|
|
return url.origin;
|
|
}
|
|
|
|
export function filesIntegrationConfigFromEnv(env: Environment): FilesIntegrationConfig {
|
|
const endpoint = httpsOrigin(env.FILES_R2_ENDPOINT ?? "", "FILES_R2_ENDPOINT");
|
|
if (!/^[a-f0-9]{32}\.r2\.cloudflarestorage\.com$/.test(new URL(endpoint).hostname)) {
|
|
throw new Error("FILES_R2_ENDPOINT must use the account R2 S3 endpoint");
|
|
}
|
|
const buckets = {
|
|
matrixMedia: env.MATRIX_R2_BUCKET ?? "occupymedia-matrix-media",
|
|
filesBlobs: env.FILES_R2_BUCKET ?? "occupymedia-files-blobs",
|
|
posterMedia: env.S3_BUCKET ?? "occupymedia-media-encrypted",
|
|
};
|
|
const names = Object.values(buckets);
|
|
if (names.some(name => !/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/.test(name))) {
|
|
throw new Error("Payload buckets must be valid R2 bucket names");
|
|
}
|
|
// Other product buckets are supplied by deployment, not provider branching here.
|
|
const reserved = env.FILES_RESERVED_BUCKETS?.split(",").map(name => name.trim());
|
|
if (!reserved?.length || reserved.some(name => !/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/.test(name))) {
|
|
throw new Error("FILES_RESERVED_BUCKETS must list other products' payload buckets");
|
|
}
|
|
const exclusive = [...names, ...reserved];
|
|
if (new Set(exclusive).size !== exclusive.length) {
|
|
throw new Error("Every product must have separate payload buckets");
|
|
}
|
|
return {
|
|
payloadStore: "r2", localPayloadCache: false, localMetadataAllowed: true,
|
|
r2: { endpoint, region: "auto", forcePathStyle: true }, buckets,
|
|
filesUrl: httpsOrigin(env.FILES_PUBLIC_URL ?? "https://files.occupymedia.cc", "FILES_PUBLIC_URL"),
|
|
officeUrl: httpsOrigin(env.OFFICE_PUBLIC_URL ?? "https://office.occupymedia.cc", "OFFICE_PUBLIC_URL"),
|
|
padsEnabled: false,
|
|
};
|
|
}
|