T-HUB/apps/api/src/storage/files-config.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

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