Files
backend/internal/config/config.go

109 lines
3.6 KiB
Go
Raw Normal View History

// Package config loads runtime configuration for the CarrotAssistant backend.
//
// Configuration can be supplied via environment variables (preferred for
// containers) and falls back to sensible defaults suitable for local
// development. Secrets such as the master key MUST be provided in
// production through the environment.
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// Config holds all runtime configuration values.
type Config struct {
// HTTPAddr is the address the API server listens on.
HTTPAddr string
// DataDir is the root directory for persistent files (sqlite db,
// generated skill markdown, etc.).
DataDir string
// DBPath is the resolved SQLite database file path.
DBPath string
// SkillsDir is the root directory where skill markdown files live,
// organised as <SkillsDir>/<app_slug>/<skill_slug>.md.
SkillsDir string
// MasterKey is used to encrypt secrets at rest (model api keys). It
// must be 32 bytes; if a shorter value is supplied it is padded, if a
// longer one is supplied it is truncated. Provide a strong value in
// production via the MASTER_KEY env var.
MasterKey string
// JWTSecret signs admin session tokens.
JWTSecret string
// AdminBootstrapUser, if set and no admin user exists, is created on
// first run. Useful for fresh deployments.
AdminBootstrapUser string
AdminBootstrapPassword string
// DefaultAdminUser / DefaultAdminPassword are used when bootstrapping
// is requested and no explicit credentials are configured.
DefaultAdminUser string
DefaultAdminPassword string
}
// Load reads configuration from environment variables with defaults.
func Load() (*Config, error) {
c := &Config{
HTTPAddr: env("HTTP_ADDR", ":8080"),
DataDir: env("DATA_DIR", "./data"),
MasterKey: env("MASTER_KEY", ""),
JWTSecret: env("JWT_SECRET", ""),
AdminBootstrapUser: env("ADMIN_BOOTSTRAP_USER", ""),
AdminBootstrapPassword: env("ADMIN_BOOTSTRAP_PASSWORD", ""),
DefaultAdminUser: env("DEFAULT_ADMIN_USER", "admin"),
DefaultAdminPassword: env("DEFAULT_ADMIN_PASSWORD", "changeme"),
}
// Resolve derived paths to absolute form for stable behaviour in the
// file loader and the HTTP handler layer.
dataDir, err := filepath.Abs(c.DataDir)
if err != nil {
return nil, fmt.Errorf("resolve data dir: %w", err)
}
c.DataDir = dataDir
c.DBPath = filepath.Join(dataDir, "carrot.db")
c.SkillsDir = filepath.Join(dataDir, "skills")
for _, dir := range []string{c.DataDir, c.SkillsDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, fmt.Errorf("mkdir %s: %w", dir, err)
}
}
// In development we allow empty secrets and substitute a deterministic
// value so the server still runs. A warning is printed to make the
// insecure state obvious; production should always set these.
if c.MasterKey == "" {
c.MasterKey = "carrot-dev-master-key-please-change"
}
if c.JWTSecret == "" {
c.JWTSecret = "carrot-dev-jwt-secret-please-change"
}
if len(c.MasterKey) > 32 {
c.MasterKey = c.MasterKey[:32]
}
if len(c.MasterKey) < 32 {
c.MasterKey = padKey(c.MasterKey, 32)
}
return c, nil
}
// env returns the value of the named env var, or fallback when unset/empty.
func env(key, fallback string) string {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v
}
return fallback
}
// padKey right-pads a short master key to the requested length. This only
// exists so the dev defaults work; production must supply a real 32-byte key.
func padKey(k string, n int) string {
if len(k) >= n {
return k[:n]
}
return k + strings.Repeat("0", n-len(k))
}