WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
at main 114 lines 4.3 kB view raw
1import type { LogLevel } from "@atbb/logger"; 2 3export interface AppConfig { 4 port: number; 5 forumDid: string; 6 pdsUrl: string; 7 databaseUrl: string; 8 jetstreamUrl: string; 9 // Logging 10 logLevel: LogLevel; 11 // OAuth configuration 12 oauthPublicUrl: string; 13 sessionSecret: string; 14 sessionTtlDays: number; 15 redisUrl?: string; 16 // Forum credentials (optional - for server-side PDS writes) 17 forumHandle?: string; 18 forumPassword?: string; 19 // Backfill configuration 20 backfillRateLimit: number; 21 backfillConcurrency: number; 22 backfillCursorMaxAgeHours: number; 23} 24 25export function loadConfig(): AppConfig { 26 const config: AppConfig = { 27 port: parseInt(process.env.PORT ?? "3000", 10), 28 forumDid: process.env.FORUM_DID ?? "", 29 pdsUrl: process.env.PDS_URL ?? "https://bsky.social", 30 databaseUrl: process.env.DATABASE_URL ?? "", 31 jetstreamUrl: 32 process.env.JETSTREAM_URL ?? 33 "wss://jetstream2.us-east.bsky.network/subscribe", 34 // Logging 35 logLevel: parseLogLevel(process.env.LOG_LEVEL), 36 // OAuth configuration 37 oauthPublicUrl: process.env.OAUTH_PUBLIC_URL ?? `http://localhost:${process.env.PORT ?? "3000"}`, 38 sessionSecret: process.env.SESSION_SECRET ?? "", 39 sessionTtlDays: parseInt(process.env.SESSION_TTL_DAYS ?? "7", 10), 40 redisUrl: process.env.REDIS_URL, 41 // Forum credentials (optional - for server-side PDS writes) 42 forumHandle: process.env.FORUM_HANDLE, 43 forumPassword: process.env.FORUM_PASSWORD, 44 // Backfill configuration 45 backfillRateLimit: parseInt(process.env.BACKFILL_RATE_LIMIT ?? "10", 10), 46 backfillConcurrency: parseInt(process.env.BACKFILL_CONCURRENCY ?? "10", 10), 47 backfillCursorMaxAgeHours: parseInt(process.env.BACKFILL_CURSOR_MAX_AGE_HOURS ?? "48", 10), 48 }; 49 50 validateOAuthConfig(config); 51 52 return config; 53} 54 55const LOG_LEVELS: readonly LogLevel[] = ["debug", "info", "warn", "error", "fatal"]; 56 57function parseLogLevel(value: string | undefined): LogLevel { 58 if (value !== undefined && !(LOG_LEVELS as readonly string[]).includes(value)) { 59 console.warn( 60 `Invalid LOG_LEVEL "${value}". Must be one of: ${LOG_LEVELS.join(", ")}. Defaulting to "info".` 61 ); 62 } 63 return (LOG_LEVELS as readonly string[]).includes(value ?? "") 64 ? (value as LogLevel) 65 : "info"; 66} 67 68/** 69 * Validate OAuth-related configuration at startup. 70 * Fails fast if required config is missing or invalid. 71 */ 72function validateOAuthConfig(config: AppConfig): void { 73 // Check environment-specific requirements first 74 if (!process.env.OAUTH_PUBLIC_URL && process.env.NODE_ENV === 'production') { 75 throw new Error('OAUTH_PUBLIC_URL is required in production'); 76 } 77 78 // Validate OAuth public URL format 79 const url = new URL(config.oauthPublicUrl); 80 81 // AT Proto OAuth requires HTTPS in production (or proper domain in dev) 82 if (process.env.NODE_ENV === 'production' && url.protocol !== 'https:') { 83 throw new Error( 84 'OAUTH_PUBLIC_URL must use HTTPS in production. Your OAuth client_id must be publicly accessible over HTTPS.' 85 ); 86 } 87 88 // Warn about localhost usage (OAuth client will reject this) 89 if (url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname.endsWith('.local')) { 90 console.warn( 91 '\n⚠️ WARNING: AT Protocol OAuth requires a publicly accessible HTTPS URL.\n' + 92 'Local development URLs (localhost, 127.0.0.1) will not work for OAuth.\n\n' + 93 'Options for local development:\n' + 94 ' 1. Use ngrok or similar tunneling service: OAUTH_PUBLIC_URL=https://abc123.ngrok.io\n' + 95 ' 2. Use a local domain with mkcert for HTTPS: OAUTH_PUBLIC_URL=https://atbb.local\n' + 96 ' 3. Deploy to a staging environment with proper HTTPS\n\n' + 97 'See https://atproto.com/specs/oauth for details.\n' 98 ); 99 } 100 101 // Then check global requirements 102 if (!config.sessionSecret || config.sessionSecret.length < 32) { 103 throw new Error( 104 'SESSION_SECRET must be at least 32 characters. Generate one with: openssl rand -hex 32' 105 ); 106 } 107 108 // Warn about in-memory sessions in production 109 if (!config.redisUrl && process.env.NODE_ENV === 'production') { 110 console.warn( 111 '⚠️ Using in-memory session storage in production. Sessions will be lost on restart.' 112 ); 113 } 114}