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
54
apps/api/src/storage/files-config.ts
Normal file
54
apps/api/src/storage/files-config.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/** 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,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue