Add Go module, fail-fast config loader and /health endpoint
Independent review Not signed off — 2 high finding(s) (#2)

- go.mod: module code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest, go 1.25,
  sole dependency github.com/go-chi/chi/v5.
- internal/config: Load() reads MEALQUEST_* env vars and *_FILE secrets,
  collects every missing/invalid value into one error naming each variable,
  rejects inline secrets, and redacts secrets when formatted.
- internal/server: chi router serving GET /health -> {"status":"ok","version"}.
- cmd/server: wiring only; slog JSON logging, version via -ldflags
  "-X main.version=...", graceful shutdown on SIGINT/SIGTERM.
- .gitignore: cover .env*, deploy/secrets/, node_modules, dist, *.db.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 22:02:44 -04:00
co-authored by Claude Opus 5.5
parent d2dc58a2ec
commit 5707682018
8 changed files with 597 additions and 0 deletions
+11
View File
@@ -1,2 +1,13 @@
.DS_Store
design/.DS_Store
# Secrets and local environment
.env*
deploy/secrets/
# Build artifacts and dependencies
node_modules
dist
# Local databases
*.db
+74
View File
@@ -0,0 +1,74 @@
// Command server runs the MealQuest HTTP service.
package main
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/config"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/server"
)
// version is set at build time with -ldflags "-X main.version=<version>".
var version = "dev"
func main() {
cfg, err := config.Load()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
if err := run(cfg, logger); err != nil {
logger.Error("server stopped", "error", err)
os.Exit(1)
}
}
func run(cfg config.Config, logger *slog.Logger) error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
srv := &http.Server{
Addr: cfg.HTTPAddr,
Handler: server.NewRouter(logger, version),
ReadHeaderTimeout: 10 * time.Second,
ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
}
errc := make(chan error, 1)
go func() {
logger.Info("listening",
"addr", cfg.HTTPAddr,
"public_url", cfg.PublicURL.String(),
"data_dir", cfg.DataDir,
"version", version,
)
errc <- srv.ListenAndServe()
}()
select {
case err := <-errc:
return err
case <-ctx.Done():
}
logger.Info("shutting down", "timeout", cfg.ShutdownTimeout.String())
shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
return fmt.Errorf("shutdown: %w", err)
}
if err := <-errc; !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}
+5
View File
@@ -0,0 +1,5 @@
module code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest
go 1.25
require github.com/go-chi/chi/v5 v5.3.1
+2
View File
@@ -0,0 +1,2 @@
github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
+200
View File
@@ -0,0 +1,200 @@
// Package config loads MealQuest configuration from MEALQUEST_* environment
// variables. Secrets are read only from files named by *_FILE variables.
package config
import (
"errors"
"fmt"
"net"
"net/url"
"os"
"strings"
"time"
)
// Environment variable names.
const (
EnvHTTPAddr = "MEALQUEST_HTTP_ADDR"
EnvPublicURL = "MEALQUEST_PUBLIC_URL"
EnvDataDir = "MEALQUEST_DATA_DIR"
EnvShutdownTimeout = "MEALQUEST_SHUTDOWN_TIMEOUT"
EnvSessionSecretFile = "MEALQUEST_SESSION_SECRET_FILE"
)
// Config is the validated service configuration.
type Config struct {
HTTPAddr string
PublicURL *url.URL
DataDir string
ShutdownTimeout time.Duration
SessionSecret Secret
}
// Load reads and validates configuration from the process environment.
// It reports every missing or invalid variable in a single *Error.
func Load() (Config, error) {
return load(os.LookupEnv)
}
func load(lookup func(string) (string, bool)) (Config, error) {
r := reader{lookup: lookup}
cfg := Config{
HTTPAddr: r.listenAddr(EnvHTTPAddr),
PublicURL: r.httpURL(EnvPublicURL),
DataDir: r.dir(EnvDataDir),
ShutdownTimeout: r.duration(EnvShutdownTimeout),
SessionSecret: r.secretFile(EnvSessionSecretFile),
}
if len(r.problems) > 0 {
return Config{}, &Error{Problems: r.problems}
}
return cfg, nil
}
// Problem describes one missing or invalid variable.
type Problem struct {
Var string
Reason string
}
// Error lists every configuration problem found by Load.
type Error struct {
Problems []Problem
}
func (e *Error) Error() string {
var b strings.Builder
b.WriteString("invalid configuration:")
for _, p := range e.Problems {
fmt.Fprintf(&b, "\n %s: %s", p.Var, p.Reason)
}
return b.String()
}
// Secret holds a sensitive value. It never prints its contents.
type Secret struct {
value string
}
// Reveal returns the secret value. Call it only where the value is used.
func (s Secret) Reveal() string { return s.value }
// String redacts the secret so it cannot leak through fmt or logs.
func (s Secret) String() string { return "[REDACTED]" }
// GoString redacts the secret for %#v.
func (s Secret) GoString() string { return "[REDACTED]" }
// reader reads variables and collects every problem instead of stopping at
// the first one.
type reader struct {
lookup func(string) (string, bool)
problems []Problem
}
func (r *reader) fail(name, format string, args ...any) {
r.problems = append(r.problems, Problem{Var: name, Reason: fmt.Sprintf(format, args...)})
}
// required returns the variable's value, recording a problem when it is
// unset or empty.
func (r *reader) required(name string) (string, bool) {
v, ok := r.lookup(name)
if !ok || strings.TrimSpace(v) == "" {
r.fail(name, "required but not set")
return "", false
}
return v, true
}
func (r *reader) listenAddr(name string) string {
v, ok := r.required(name)
if !ok {
return ""
}
if _, _, err := net.SplitHostPort(v); err != nil {
r.fail(name, "invalid listen address %q (want host:port or :port)", v)
return ""
}
return v
}
func (r *reader) httpURL(name string) *url.URL {
v, ok := r.required(name)
if !ok {
return nil
}
u, err := url.Parse(v)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
r.fail(name, "invalid URL %q (want absolute http:// or https:// URL)", v)
return nil
}
return u
}
func (r *reader) dir(name string) string {
v, ok := r.required(name)
if !ok {
return ""
}
info, err := os.Stat(v)
if err != nil {
r.fail(name, "cannot access directory %q: %v", v, err)
return ""
}
if !info.IsDir() {
r.fail(name, "%q is not a directory", v)
return ""
}
return v
}
func (r *reader) duration(name string) time.Duration {
v, ok := r.required(name)
if !ok {
return 0
}
d, err := time.ParseDuration(v)
if err != nil {
r.fail(name, "invalid duration %q (want e.g. 10s, 1m)", v)
return 0
}
if d <= 0 {
r.fail(name, "duration must be positive, got %q", v)
return 0
}
return d
}
// secretFile reads a secret from the file named by name, which must end in
// _FILE. Setting the secret inline (name without _FILE) is rejected.
func (r *reader) secretFile(name string) Secret {
inline := strings.TrimSuffix(name, "_FILE")
if _, set := r.lookup(inline); set {
r.fail(inline, "secrets must not be set inline; use %s", name)
}
path, ok := r.required(name)
if !ok {
return Secret{}
}
data, err := os.ReadFile(path)
if err != nil {
// Report the cause without echoing any file contents.
r.fail(name, "cannot read secret file %q: %v", path, unwrapPathError(err))
return Secret{}
}
value := strings.TrimRight(string(data), "\r\n")
if strings.TrimSpace(value) == "" {
r.fail(name, "secret file %q is empty", path)
return Secret{}
}
return Secret{value: value}
}
func unwrapPathError(err error) error {
var pe *os.PathError
if errors.As(err, &pe) {
return pe.Err
}
return err
}
+217
View File
@@ -0,0 +1,217 @@
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"time"
)
func TestLoad(t *testing.T) {
t.Parallel()
dir := t.TempDir()
secretPath := filepath.Join(dir, "session_secret")
if err := os.WriteFile(secretPath, []byte("s3cret-value\n"), 0o600); err != nil {
t.Fatal(err)
}
unreadablePath := filepath.Join(dir, "missing_secret")
emptyPath := filepath.Join(dir, "empty_secret")
if err := os.WriteFile(emptyPath, []byte("\n"), 0o600); err != nil {
t.Fatal(err)
}
valid := func() map[string]string {
return map[string]string{
EnvHTTPAddr: ":8080",
EnvPublicURL: "https://meals.example.com",
EnvDataDir: dir,
EnvShutdownTimeout: "15s",
EnvSessionSecretFile: secretPath,
}
}
with := func(changes map[string]string, unset ...string) map[string]string {
env := valid()
for k, v := range changes {
env[k] = v
}
for _, k := range unset {
delete(env, k)
}
return env
}
tests := []struct {
name string
env map[string]string
badVars []string // variables the error must name; nil means success
}{
{
name: "all required vars present",
env: valid(),
},
{
name: "one missing",
env: with(nil, EnvHTTPAddr),
badVars: []string{EnvHTTPAddr},
},
{
name: "empty value counts as missing",
env: with(map[string]string{EnvDataDir: ""}),
badVars: []string{EnvDataDir},
},
{
name: "all missing are collected",
env: map[string]string{},
badVars: []string{
EnvHTTPAddr, EnvPublicURL, EnvDataDir, EnvShutdownTimeout, EnvSessionSecretFile,
},
},
{
name: "invalid duration",
env: with(map[string]string{EnvShutdownTimeout: "ten seconds"}),
badVars: []string{EnvShutdownTimeout},
},
{
name: "non-positive duration",
env: with(map[string]string{EnvShutdownTimeout: "0s"}),
badVars: []string{EnvShutdownTimeout},
},
{
name: "invalid listen address",
env: with(map[string]string{EnvHTTPAddr: "8080"}),
badVars: []string{EnvHTTPAddr},
},
{
name: "relative public URL",
env: with(map[string]string{EnvPublicURL: "meals.example.com"}),
badVars: []string{EnvPublicURL},
},
{
name: "data dir does not exist",
env: with(map[string]string{EnvDataDir: filepath.Join(dir, "nope")}),
badVars: []string{EnvDataDir},
},
{
name: "unreadable secret file",
env: with(map[string]string{EnvSessionSecretFile: unreadablePath}),
badVars: []string{EnvSessionSecretFile},
},
{
name: "secret file path is a directory",
env: with(map[string]string{EnvSessionSecretFile: dir}),
badVars: []string{EnvSessionSecretFile},
},
{
name: "empty secret file",
env: with(map[string]string{EnvSessionSecretFile: emptyPath}),
badVars: []string{EnvSessionSecretFile},
},
{
name: "inline secret rejected",
env: with(map[string]string{"MEALQUEST_SESSION_SECRET": "inline"}),
badVars: []string{"MEALQUEST_SESSION_SECRET"},
},
{
name: "several invalid values are all named",
env: with(map[string]string{
EnvShutdownTimeout: "soon",
EnvSessionSecretFile: unreadablePath,
}, EnvPublicURL),
badVars: []string{EnvPublicURL, EnvShutdownTimeout, EnvSessionSecretFile},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
lookup := func(k string) (string, bool) {
v, ok := tc.env[k]
return v, ok
}
cfg, err := load(lookup)
if tc.badVars == nil {
if err != nil {
t.Fatalf("load() error = %v, want nil", err)
}
return
}
var cerr *Error
if !errors.As(err, &cerr) {
t.Fatalf("load() error = %v, want *Error", err)
}
var got []string
for _, p := range cerr.Problems {
got = append(got, p.Var)
}
if !slices.Equal(got, tc.badVars) {
t.Errorf("problem vars = %v, want %v", got, tc.badVars)
}
for _, v := range tc.badVars {
if !strings.Contains(err.Error(), v) {
t.Errorf("error message %q does not name %s", err.Error(), v)
}
}
if cfg != (Config{}) {
t.Errorf("load() returned non-zero Config alongside error: %+v", cfg)
}
})
}
}
func TestLoadValues(t *testing.T) {
t.Parallel()
dir := t.TempDir()
secretPath := filepath.Join(dir, "session_secret")
if err := os.WriteFile(secretPath, []byte("s3cret-value\r\n"), 0o600); err != nil {
t.Fatal(err)
}
env := map[string]string{
EnvHTTPAddr: "127.0.0.1:9000",
EnvPublicURL: "http://localhost:9000",
EnvDataDir: dir,
EnvShutdownTimeout: "1m30s",
EnvSessionSecretFile: secretPath,
}
cfg, err := load(func(k string) (string, bool) { v, ok := env[k]; return v, ok })
if err != nil {
t.Fatalf("load() error = %v", err)
}
if cfg.HTTPAddr != "127.0.0.1:9000" {
t.Errorf("HTTPAddr = %q", cfg.HTTPAddr)
}
if cfg.PublicURL.String() != "http://localhost:9000" {
t.Errorf("PublicURL = %q", cfg.PublicURL)
}
if cfg.DataDir != dir {
t.Errorf("DataDir = %q, want %q", cfg.DataDir, dir)
}
if cfg.ShutdownTimeout != 90*time.Second {
t.Errorf("ShutdownTimeout = %v, want 1m30s", cfg.ShutdownTimeout)
}
if cfg.SessionSecret.Reveal() != "s3cret-value" {
t.Errorf("SessionSecret.Reveal() = %q, want trailing newline trimmed", cfg.SessionSecret.Reveal())
}
}
func TestSecretRedacted(t *testing.T) {
t.Parallel()
s := Secret{value: "s3cret-value"}
for _, format := range []string{"%v", "%s", "%+v", "%#v"} {
cfg := Config{SessionSecret: s}
for _, out := range []string{fmt.Sprintf(format, s), fmt.Sprintf(format, cfg)} {
if strings.Contains(out, "s3cret-value") {
t.Errorf("format %s leaked secret: %s", format, out)
}
}
}
}
+37
View File
@@ -0,0 +1,37 @@
// Package server builds the MealQuest HTTP router.
package server
import (
"encoding/json"
"log/slog"
"net/http"
"github.com/go-chi/chi/v5"
)
// NewRouter returns the HTTP handler for the service. version is reported by
// GET /health.
func NewRouter(logger *slog.Logger, version string) http.Handler {
r := chi.NewRouter()
r.Get("/health", health(logger, version))
return r
}
type healthResponse struct {
Status string `json:"status"`
Version string `json:"version"`
}
func health(logger *slog.Logger, version string) http.HandlerFunc {
body, err := json.Marshal(healthResponse{Status: "ok", Version: version})
if err != nil {
panic(err) // two string fields cannot fail to marshal
}
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if _, err := w.Write(body); err != nil {
logger.WarnContext(r.Context(), "write health response", "error", err)
}
}
}
+51
View File
@@ -0,0 +1,51 @@
package server
import (
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
)
func TestHealth(t *testing.T) {
t.Parallel()
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
srv := httptest.NewServer(NewRouter(logger, "1.2.3"))
t.Cleanup(srv.Close)
resp, err := http.Get(srv.URL + "/health")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d, want 200", resp.StatusCode)
}
if ct := resp.Header.Get("Content-Type"); ct != "application/json" {
t.Errorf("Content-Type = %q, want application/json", ct)
}
var got map[string]string
if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
t.Fatal(err)
}
want := map[string]string{"status": "ok", "version": "1.2.3"}
if len(got) != len(want) || got["status"] != want["status"] || got["version"] != want["version"] {
t.Errorf("body = %v, want %v", got, want)
}
}
func TestHealthRejectsOtherMethods(t *testing.T) {
t.Parallel()
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
rec := httptest.NewRecorder()
NewRouter(logger, "dev").ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/health", nil))
if rec.Code != http.StatusMethodNotAllowed {
t.Errorf("status = %d, want 405", rec.Code)
}
}