109 lines
3.6 KiB
Go
109 lines
3.6 KiB
Go
|
|
// 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))
|
||
|
|
}
|