Barazo AppView backend barazo.forum
at main 131 lines 3.7 kB view raw
1import { z } from 'zod/v4' 2 3const portSchema = z 4 .string() 5 .default('3000') 6 .transform((val) => Number(val)) 7 .pipe(z.number().int().min(1).max(65535)) 8 9const intFromString = (defaultVal: string) => 10 z 11 .string() 12 .default(defaultVal) 13 .transform((val) => Number(val)) 14 .pipe(z.number().int().min(0)) 15 16const positiveIntFromString = (defaultVal: string) => 17 z 18 .string() 19 .default(defaultVal) 20 .transform((val) => Number(val)) 21 .pipe(z.number().int().positive()) 22 23const baseEnvSchema = z.object({ 24 // Required 25 DATABASE_URL: z.url(), 26 VALKEY_URL: z.url(), 27 TAP_URL: z.url(), 28 TAP_ADMIN_PASSWORD: z.string().min(1), 29 30 // Server 31 HOST: z.string().default('0.0.0.0'), 32 PORT: portSchema, 33 LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'), 34 35 // CORS 36 CORS_ORIGINS: z.string().default('http://localhost:3001'), 37 38 // Community 39 COMMUNITY_MODE: z.enum(['single', 'multi']).default('single'), 40 HOSTING_MODE: z.enum(['saas', 'selfhosted']).default('selfhosted'), 41 COMMUNITY_DID: z.string().optional(), 42 COMMUNITY_NAME: z.string().default('Barazo Community'), 43 44 // Rate Limiting (requests per minute) 45 RATE_LIMIT_AUTH: intFromString('10'), 46 RATE_LIMIT_WRITE: intFromString('10'), 47 RATE_LIMIT_READ_ANON: intFromString('100'), 48 RATE_LIMIT_READ_AUTH: intFromString('300'), 49 50 // Encryption (KEK for sensitive data at rest) 51 AI_ENCRYPTION_KEY: z.string().min(32), 52 53 // OAuth 54 OAUTH_CLIENT_ID: z.string().min(1), 55 OAUTH_REDIRECT_URI: z.string().min(1), 56 SESSION_SECRET: z.string().min(32), 57 OAUTH_SESSION_TTL: positiveIntFromString('604800'), 58 OAUTH_ACCESS_TOKEN_TTL: positiveIntFromString('900'), 59 60 // Monitoring (GlitchTip - Sentry SDK compatible) 61 GLITCHTIP_DSN: z.string().optional(), 62 63 // Optional: semantic search 64 EMBEDDING_URL: z.string().optional(), 65 AI_EMBEDDING_DIMENSIONS: z 66 .string() 67 .default('768') 68 .transform((val) => Number(val)) 69 .pipe(z.number().int().min(384).max(1536)), 70 71 // Cross-posting 72 FEATURE_CROSSPOST_BLUESKY: z 73 .enum(['true', 'false']) 74 .default('true') 75 .transform((v) => v === 'true'), 76 FEATURE_CROSSPOST_FRONTPAGE: z 77 .enum(['true', 'false']) 78 .default('false') 79 .transform((v) => v === 'true'), 80 PUBLIC_URL: z.string().default('http://localhost:3001'), 81 82 // Multi mode: operator DIDs (comma-separated) 83 OPERATOR_DIDS: z 84 .string() 85 .default('') 86 .transform((v) => 87 v 88 .split(',') 89 .map((s) => s.trim()) 90 .filter((s) => s.length > 0) 91 ), 92 93 // Uploads 94 UPLOAD_DIR: z.string().default('./uploads'), 95 UPLOAD_MAX_SIZE_BYTES: z.coerce.number().default(5_242_880), // 5MB 96 UPLOAD_BASE_URL: z.string().optional(), 97 98 // Ozone labeler (opt-in) 99 OZONE_LABELER_URL: z.string().default('https://mod.bsky.app'), 100}) 101 102export const envSchema = baseEnvSchema.refine( 103 (data) => 104 data.COMMUNITY_MODE !== 'single' || (data.COMMUNITY_DID && data.COMMUNITY_DID.length > 0), 105 { 106 message: 'COMMUNITY_DID is required when COMMUNITY_MODE is "single"', 107 path: ['COMMUNITY_DID'], 108 } 109) 110 111export type Env = z.infer<typeof envSchema> 112 113/** 114 * Get the community DID, throwing if not set. 115 * Safe to call after env validation -- single mode requires COMMUNITY_DID at startup. 116 */ 117export function getCommunityDid(env: Env): string { 118 if (!env.COMMUNITY_DID) { 119 throw new Error('COMMUNITY_DID is required in single mode but not set') 120 } 121 return env.COMMUNITY_DID 122} 123 124export function parseEnv(env: Record<string, unknown>): Env { 125 const result = envSchema.safeParse(env) 126 if (!result.success) { 127 const formatted = z.prettifyError(result.error) 128 throw new Error(`Invalid environment configuration:\n${formatted}`) 129 } 130 return result.data 131}