package config import ( "fmt" "os" "strconv" ) // Config holds all application configuration type Config struct { // API Keys PlanToEatAPIKey string PlanToEatSession string // Session cookie for web scraping TrelloAPIKey string TrelloToken string // Google Calendar GoogleCredentialsFile string GoogleCalendarID string // Google Tasks (real 3-legged OAuth -- service-account auth cannot see a // user's personal task lists, see NewGoogleTasksClient's doc comment) GoogleTasksListID string GoogleOAuthClientID string GoogleOAuthClientSecret string // GoogleOAuthRedirectURL must exactly match an "Authorized redirect URI" // on the OAuth 2.0 Client ID in Google Cloud Console -- kept as its own // explicit var rather than derived from WebAuthnOrigin (which isn't set // in production; passkeys aren't configured either) so there's exactly // one place that has to match Console, not two things that have to // agree with each other. GoogleOAuthRedirectURL string // Paths DatabasePath string MigrationDir string TemplateDir string StaticDir string DocsDir string // root of superpowers spec/plan docs, served read-only at /docs // Server Port string CacheTTLMinutes int Debug bool // Display Timezone string // IANA timezone name (e.g., "Pacific/Honolulu") // WebAuthn WebAuthnRPID string // Relying Party ID (domain, e.g., "doot.terst.org") WebAuthnOrigin string // Expected origin (e.g., "https://doot.terst.org") // Widget API WidgetToken string // Static bearer token for /api/widget (WIDGET_TOKEN env var) // Claudomator ClaudomatorURL string // URL of Claudomator service // Service gateway — additional upstream services proxied through doot's auth layer PlaygroundURL string // URL of playground demo service (optional) ScoutURL string // URL of scout listing feed service (optional) HawaiiURL string // URL of Hawaii county data dashboard (optional) } // Load reads configuration from environment variables func Load() (*Config, error) { cfg := &Config{ // API Keys PlanToEatAPIKey: os.Getenv("PLANTOEAT_API_KEY"), PlanToEatSession: os.Getenv("PLANTOEAT_SESSION"), TrelloAPIKey: os.Getenv("TRELLO_API_KEY"), TrelloToken: os.Getenv("TRELLO_TOKEN"), // Google Calendar GoogleCredentialsFile: os.Getenv("GOOGLE_CREDENTIALS_FILE"), GoogleCalendarID: getEnvWithDefault("GOOGLE_CALENDAR_ID", "primary"), // Google Tasks GoogleTasksListID: getEnvWithDefault("GOOGLE_TASKS_LIST_ID", "@default"), GoogleOAuthClientID: os.Getenv("GOOGLE_OAUTH_CLIENT_ID"), GoogleOAuthClientSecret: os.Getenv("GOOGLE_OAUTH_CLIENT_SECRET"), GoogleOAuthRedirectURL: getEnvWithDefault("GOOGLE_OAUTH_REDIRECT_URL", "https://doot.terst.org/settings/google-tasks/callback"), // Paths DatabasePath: getEnvWithDefault("DATABASE_PATH", "./dashboard.db"), MigrationDir: getEnvWithDefault("MIGRATION_DIR", "migrations"), TemplateDir: getEnvWithDefault("TEMPLATE_DIR", "web/templates"), StaticDir: getEnvWithDefault("STATIC_DIR", "web/static"), DocsDir: getEnvWithDefault("DOCS_DIR", "docs/superpowers"), // Server Port: getEnvWithDefault("PORT", "8080"), CacheTTLMinutes: getEnvAsInt("CACHE_TTL_MINUTES", 5), Debug: getEnvAsBool("DEBUG", false), // Display Timezone: getEnvWithDefault("TIMEZONE", "Pacific/Honolulu"), // WebAuthn WebAuthnRPID: os.Getenv("WEBAUTHN_RP_ID"), WebAuthnOrigin: os.Getenv("WEBAUTHN_ORIGIN"), // Widget API WidgetToken: os.Getenv("WIDGET_TOKEN"), // Claudomator ClaudomatorURL: getEnvWithDefault("CLAUDOMATOR_URL", "http://127.0.0.1:8484"), // Service gateway PlaygroundURL: os.Getenv("PLAYGROUND_URL"), ScoutURL: os.Getenv("SCOUT_URL"), HawaiiURL: os.Getenv("HAWAII_URL"), } // Validate required fields if err := cfg.Validate(); err != nil { return nil, err } return cfg, nil } // Validate checks that required configuration is present func (c *Config) Validate() error { if c.TrelloAPIKey == "" { return fmt.Errorf("TRELLO_API_KEY is required") } if c.TrelloToken == "" { return fmt.Errorf("TRELLO_TOKEN is required") } return nil } // HasPlanToEat checks if PlanToEat is configured (API key or session) func (c *Config) HasPlanToEat() bool { return c.PlanToEatAPIKey != "" || c.PlanToEatSession != "" } // HasPlanToEatSession checks if PlanToEat session cookie is configured func (c *Config) HasPlanToEatSession() bool { return c.PlanToEatSession != "" } // HasTrello checks if Trello is configured func (c *Config) HasTrello() bool { return c.TrelloAPIKey != "" && c.TrelloToken != "" } // HasGoogleCalendar checks if Google Calendar is configured func (c *Config) HasGoogleCalendar() bool { return c.GoogleCredentialsFile != "" } // HasGoogleTasks checks if Google Tasks OAuth is configured (client ID and // secret from Google Cloud Console) -- doesn't mean a user has actually // connected yet, that's the oauth_tokens row, checked separately. func (c *Config) HasGoogleTasks() bool { return c.GoogleOAuthClientID != "" && c.GoogleOAuthClientSecret != "" } // getEnvWithDefault returns environment variable value or default if not set func getEnvWithDefault(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } // getEnvAsInt returns environment variable as int or default if not set or invalid func getEnvAsInt(key string, defaultValue int) int { valueStr := os.Getenv(key) if valueStr == "" { return defaultValue } value, err := strconv.Atoi(valueStr) if err != nil { return defaultValue } return value } // getEnvAsBool returns environment variable as bool or default if not set func getEnvAsBool(key string, defaultValue bool) bool { valueStr := os.Getenv(key) if valueStr == "" { return defaultValue } value, err := strconv.ParseBool(valueStr) if err != nil { return defaultValue } return value }