Files
brain/internal/config/config.go
T
Andrew S Erwin 4e421f6ea2
release gate / check (push) Failing after 6m43s
Address code review findings across API, web and deployment
High severity:
- wire TRUSTED_PROXY through Compose/deploy/docs and warn at web startup
- bucket public rate limits on effective client IP, add per-bookmark budget,
  and forward the signed browser identity from the share page
- disable first-run bootstrap in server deployments and document seeding
- serialize last-admin demotion in one advisory-locked transaction

Medium/low:
- cap and batch share recipient inserts
- add a shared outbound budget for preview/CardDAV/IMAP/signal work
- pin CardDAV dials to 443 and bound IMAP logout
- fix rightward kanban column drags and normalize cards on column delete
- classify backend outages in auth guards, add request timeouts and an
  error boundary, remove the dead login route, surface throttling
- enforce pinned/unpinned page reorder grouping server-side
- avoid preview work for stale bookmark versions; drop duplicate dialog fetch
- sequence-guard collection/tag/calendar/quick-find loads
- validate property config and fix MULTI_SELECT/DATE filter inputs
- document index maintenance; apply same-origin check to sign-out
- trim signup names, unique shared dates and kanban keys (migration 010),
  reset reference cache on identity changes, hide OIDC issuer, return 413
  for oversized direct bodies, escape Markdown link destinations
- remove dead template files and duplicate test helper

Verified with go build/vet, full PostgreSQL race suite, db convergence,
tsc, both ESLint configs and a Next production build.
2026-10-20 00:13:30 -05:00

144 lines
4.5 KiB
Go

package config
import (
"fmt"
"net/url"
"os"
"strings"
"sync"
)
// DBURL returns the database connection string, preferring DATABASE_URL and
// falling back to DB_URL (used by the Makefile).
func DBURL() string {
if v := os.Getenv("DATABASE_URL"); v != "" {
return v
}
return os.Getenv("DB_URL")
}
// Config holds the runtime configuration, loaded from environment variables.
// See porting guide section 7.
type Config struct {
DatabaseURL string
NextAuthSecret string // == AUTH_SECRET; signs sessions and derives IMAP key
AuthSecret string
NextAuthURL string
OIDCIssuer string
OIDCClientID string
OIDCClientSecret string
AdminEmails []string
}
// Load reads configuration from the environment. Placeholders keep OIDC off.
func Load() *Config {
return &Config{
DatabaseURL: DBURL(),
NextAuthSecret: os.Getenv("NEXTAUTH_SECRET"),
AuthSecret: os.Getenv("AUTH_SECRET"),
NextAuthURL: os.Getenv("NEXTAUTH_URL"),
OIDCIssuer: os.Getenv("OIDC_ISSUER"),
OIDCClientID: os.Getenv("OIDC_CLIENT_ID"),
OIDCClientSecret: os.Getenv("OIDC_CLIENT_SECRET"),
AdminEmails: AdminEmails(),
}
}
// Secret returns the signing secret, preferring NEXTAUTH_SECRET then AUTH_SECRET.
// The original code falls back to a hard-coded string; a port must fail instead.
func (c *Config) Secret() string {
if c.NextAuthSecret != "" {
return c.NextAuthSecret
}
return c.AuthSecret
}
// EncryptionSecret returns the key material for at-rest encryption of
// integration credentials (review OPS-01). A dedicated ENCRYPTION_KEY lets
// operators rotate AUTH_SECRET (sessions) without invalidating stored IMAP
// passwords. Without it the auth secret remains the key, matching the
// original derivation, and stored rows keep decrypting via the legacy
// fallback in signals.LoadMailbox.
func (c *Config) EncryptionSecret() string {
if v := os.Getenv("ENCRYPTION_KEY"); v != "" {
return v
}
return c.Secret()
}
// CookieSecure reports whether session cookies must carry the Secure flag.
// An explicit COOKIE_SECURE setting wins; otherwise the NEXTAUTH_URL scheme
// decides, so TLS deployments are secure by default without knowing about
// the extra variable.
func (c *Config) CookieSecure() bool {
if v := os.Getenv("COOKIE_SECURE"); v != "" {
return v == "1"
}
u, err := url.Parse(strings.TrimSpace(c.NextAuthURL))
return err == nil && u.Scheme == "https"
}
// adminEmailsCache memoizes the parsed ADMIN_EMAILS value by its raw env
// string (review M-12: IsAdminEmail runs on the auth hot path and re-split
// and re-lowercased the list on every call). Keying on the raw value keeps
// t.Setenv-based tests correct without a process-global startup snapshot.
var adminEmailsCache struct {
mu sync.Mutex
raw string
out []string
}
// AdminEmails splits env ADMIN_EMAILS on commas, trims, lowercases.
func AdminEmails() []string {
s := os.Getenv("ADMIN_EMAILS")
adminEmailsCache.mu.Lock()
defer adminEmailsCache.mu.Unlock()
if adminEmailsCache.raw == s {
return adminEmailsCache.out
}
var out []string
for _, email := range strings.Split(s, ",") {
if email = strings.ToLower(strings.TrimSpace(email)); email != "" {
out = append(out, email)
}
}
adminEmailsCache.raw, adminEmailsCache.out = s, out
return out
}
// IsAdminEmail reports whether email is listed in ADMIN_EMAILS.
func IsAdminEmail(email string) bool {
e := strings.ToLower(strings.TrimSpace(email))
for _, a := range AdminEmails() {
if strings.ToLower(a) == e {
return true
}
}
return false
}
// AllowBootstrapSignup reports whether the first self-service signup on a
// fresh installation may claim administrator (review SEC-5). Default is on
// so the documented "sign up, then sign in" onboarding keeps working;
// exposed deployments set ALLOW_BOOTSTRAP_SIGNUP=0 and provision operators
// via ADMIN_EMAILS/seed instead, so an attacker scanning for new
// installations cannot claim the instance.
func AllowBootstrapSignup() bool {
return os.Getenv("ALLOW_BOOTSTRAP_SIGNUP") != "0"
}
// Validate fails fast on configuration the app cannot use, instead of
// discovering it on the first request (review M-12).
func (c *Config) Validate() error {
if strings.TrimSpace(c.Secret()) == "" {
return fmt.Errorf("AUTH_SECRET (or NEXTAUTH_SECRET) must be set")
}
if c.NextAuthURL != "" {
u, err := url.Parse(c.NextAuthURL)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
return fmt.Errorf("NEXTAUTH_URL must be an absolute http(s) URL, got %q", c.NextAuthURL)
}
}
return nil
}