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 atb-52-css-token-extraction 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}