UNPKG

webpods

Version:

Append-only log service with OAuth authentication

274 lines 10.5 kB
/** * Configuration loader with environment variable resolution * * Loads configuration from config.json and resolves environment variable references. * Supports: * - $VAR_NAME - replaced with environment variable value * - $VAR_NAME || default - uses environment variable or default value if not set */ import { readFileSync, existsSync } from "fs"; import { join } from "path"; import { createLogger } from "./logger.js"; const logger = createLogger("webpods:config-loader"); /** * Resolve environment variable references in a value * Supports format: $VAR_NAME */ function resolveEnvValue(value, defaultValue) { if (typeof value !== "string") { return value; } // Check if the value contains an environment variable reference if (!value.startsWith("$")) { return value; } // Remove the $ prefix const varName = value.substring(1); // Get environment variable value const envValue = process.env[varName]; if (envValue === undefined) { // Use default if provided if (defaultValue !== undefined) { return defaultValue; } // Otherwise error throw new Error(`Required environment variable ${varName} is not set`); } // Try to parse numbers for specific fields const num = Number(envValue); return !isNaN(num) && (varName.includes("PORT") || varName.includes("LIMIT")) ? num : envValue; } /** * Recursively resolve environment variables in an object with context-aware defaults */ function resolveEnvVars(obj, path = []) { if (obj === null || obj === undefined) { return obj; } if (Array.isArray(obj)) { return obj.map((item, index) => resolveEnvVars(item, [...path, String(index)])); } if (typeof obj === "object") { const resolved = {}; for (const [key, value] of Object.entries(obj)) { const currentPath = [...path, key]; const fullPath = currentPath.join("."); // Determine default value based on path let defaultValue; switch (fullPath) { case "server.host": defaultValue = "0.0.0.0"; break; case "server.port": defaultValue = 3000; break; case "server.publicUrl": defaultValue = "http://localhost:3000"; break; case "server.corsOrigin": defaultValue = "*"; break; case "server.maxPayloadSize": defaultValue = "10mb"; break; case "database.host": defaultValue = "localhost"; break; case "database.port": defaultValue = 5432; break; case "database.database": defaultValue = "webpodsdb"; break; case "database.user": defaultValue = "postgres"; break; case "auth.jwtExpiry": defaultValue = undefined; // No expiry by default break; case "rateLimits.writes": defaultValue = 1000; break; case "rateLimits.reads": defaultValue = 10000; break; case "rateLimits.podCreate": defaultValue = 10; break; case "rateLimits.streamCreate": defaultValue = 100; break; } if (Array.isArray(value)) { // Process arrays recursively resolved[key] = resolveEnvVars(value, currentPath); } else if (typeof value === "object" && value !== null) { // Process objects recursively resolved[key] = resolveEnvVars(value, currentPath); } else if (typeof value === "string" && value.startsWith("$")) { // Only apply defaults to environment variable references resolved[key] = resolveEnvValue(value, defaultValue); } else { // Keep non-env-var values as-is resolved[key] = value; } } return resolved; } return resolveEnvValue(obj); } /** * Apply default values to missing config fields */ function applyDefaults(config) { // Ensure base structure exists config.server = config.server || {}; config.database = config.database || {}; config.auth = config.auth || {}; config.rateLimits = config.rateLimits || {}; // Apply defaults for server (using env var references) config.server.host = config.server.host ?? "$HOST"; config.server.port = config.server.port ?? "$PORT"; config.server.publicUrl = config.server.publicUrl ?? "$PUBLIC_URL"; config.server.corsOrigin = config.server.corsOrigin ?? "$CORS_ORIGIN"; config.server.maxPayloadSize = config.server.maxPayloadSize ?? "$MAX_PAYLOAD_SIZE"; // Apply defaults for database config.database.host = config.database.host ?? "$WEBPODS_DB_HOST"; config.database.port = config.database.port ?? "$WEBPODS_DB_PORT"; config.database.database = config.database.database ?? "$WEBPODS_DB_NAME"; config.database.user = config.database.user ?? "$WEBPODS_DB_USER"; config.database.password = config.database.password ?? "$WEBPODS_DB_PASSWORD"; // Apply defaults for auth config.auth.jwtSecret = config.auth.jwtSecret ?? "$JWT_SECRET"; config.auth.jwtExpiry = config.auth.jwtExpiry ?? "$JWT_EXPIRY"; config.auth.sessionSecret = config.auth.sessionSecret ?? "$SESSION_SECRET"; // Apply defaults for rate limits config.rateLimits.writes = config.rateLimits.writes ?? "$RATE_LIMIT_WRITES"; config.rateLimits.reads = config.rateLimits.reads ?? "$RATE_LIMIT_READS"; config.rateLimits.podCreate = config.rateLimits.podCreate ?? "$RATE_LIMIT_POD_CREATE"; config.rateLimits.streamCreate = config.rateLimits.streamCreate ?? "$RATE_LIMIT_STREAM_CREATE"; return config; } /** * Load configuration from file */ export function loadConfig(configPath) { // Determine config file path const paths = [ configPath, process.env.WEBPODS_CONFIG_PATH, join(process.cwd(), "config.json"), ].filter(Boolean); let configFile; for (const path of paths) { if (existsSync(path)) { configFile = path; break; } } if (!configFile) { throw new Error("No configuration file found. Create config.json or use -c to specify config path"); } logger.info("Loading configuration", { path: configFile }); try { // Read and parse config file const configContent = readFileSync(configFile, "utf-8"); const rawConfig = JSON.parse(configContent); // Apply defaults for missing fields const configWithDefaults = applyDefaults(rawConfig); // Resolve environment variables const config = resolveEnvVars(configWithDefaults); // Parse publicUrl to extract components if (config.server?.publicUrl) { try { const url = new URL(config.server.publicUrl); config.server.public = { protocol: url.protocol.replace(":", ""), hostname: url.hostname, port: parseInt(url.port) || (url.protocol === "https:" ? 443 : 80), host: url.host, origin: url.origin, isSecure: url.protocol === "https:", }; } catch { throw new Error(`Invalid publicUrl: ${config.server.publicUrl}`); } } // Validate required fields validateConfig(config); logger.info("Configuration loaded successfully", { providers: config.oauth.providers.map((p) => p.id), defaultProvider: config.oauth.defaultProvider, }); return config; } catch (error) { logger.error("Failed to load configuration", { error: error.message }); throw error; } } /** * Validate configuration */ function validateConfig(config) { // Check OAuth providers if (!config.oauth || !config.oauth.providers || config.oauth.providers.length === 0) { throw new Error("No OAuth providers configured in config.oauth.providers"); } for (const provider of config.oauth.providers) { if (!provider.id) { throw new Error("OAuth provider missing id"); } if (!provider.clientId || !provider.clientSecret) { throw new Error(`OAuth provider ${provider.id} missing clientId or clientSecret. Set oauth.providers[].clientSecret or environment variable referenced in the config`); } // Must have either issuer (for OIDC discovery) or manual endpoints if (!provider.issuer && (!provider.authUrl || !provider.tokenUrl || !provider.userinfoUrl)) { throw new Error(`OAuth provider ${provider.id} must have either issuer or authUrl/tokenUrl/userinfoUrl`); } if (!provider.scope) { throw new Error(`OAuth provider ${provider.id} missing scope`); } } // Check auth config if (!config.auth?.jwtSecret) { throw new Error("auth.jwtSecret is required. Set it in config.json or provide environment variable JWT_SECRET"); } if (!config.auth?.sessionSecret) { throw new Error("auth.sessionSecret is required. Set it in config.json or provide environment variable SESSION_SECRET"); } // Check database config if (!config.database?.password) { throw new Error("database.password is required. Set it in config.json or provide environment variable WEBPODS_DB_PASSWORD"); } } // Singleton config instance let configInstance = null; /** * Get the current configuration (loads on first call) */ export function getConfig() { if (!configInstance) { configInstance = loadConfig(); } return configInstance; } /** * Reset configuration (mainly for testing) */ export function resetConfig() { configInstance = null; } //# sourceMappingURL=config-loader.js.map