feat(mcp): scaffold uses real EINHERJAR_SERVER_CORS_ORIGINS; document web.New vs server.New (v1.2.0)

This commit is contained in:
2026-08-08 02:29:16 -06:00
parent 6b4d9be141
commit 03a8d8a641
6 changed files with 70 additions and 37 deletions
+16 -15
View File
@@ -30,7 +30,8 @@ func TestRenderEnvExample(t *testing.T) {
// Required, no default → uncommented with a runnable dev value.
mustContain := []string{
"APP_ENV=local",
"APP_CORS_ORIGINS=",
// CORS now lives on server.Config (composed) → documented, commented, no default.
"# EINHERJAR_SERVER_CORS_ORIGINS=",
"EINHERJAR_PG_HOST=localhost",
"EINHERJAR_PG_USER=postgres",
"EINHERJAR_PG_PASSWORD=postgres",
@@ -57,10 +58,10 @@ func TestRenderEnvExample(t *testing.T) {
mustNotContain := []string{
"EINHERJAR_PG_DATABASE",
"EINHERJAR_SERVER_ADDR",
"APP_CORS_ORIGINS=*", // the wildcard trap must never be emitted
// CORS lives on web.Config, which the scaffold does not compose — so it
// must not be emitted (it would be a dead var).
"EINHERJAR_SERVER_CORS_ORIGINS",
// APP_CORS_ORIGINS was an invented var — the scaffold now uses the real
// framework var (EINHERJAR_SERVER_CORS_ORIGINS on server.Config), so no
// form of APP_CORS_ORIGINS may ever be emitted.
"APP_CORS_ORIGINS",
}
for _, s := range mustNotContain {
if strings.Contains(got, s) {
@@ -164,36 +165,36 @@ func TestCheckEnvUnknownModule(t *testing.T) {
}
// TestCheckEnvStructLevel proves the struct-granularity path catches a dead var
// that module granularity cannot: EINHERJAR_SERVER_CORS_ORIGINS lives on
// web.Config, so an app composing web/server/Config (not web.Config) never reads
// it — module "web" would hide that, struct selectors surface it.
// that module granularity cannot: EINHERJAR_HEALTH_CHECK_TIMEOUT lives on
// web/health.Config, so an app composing web/server/Config (not web/health/Config)
// never reads it — module "web" would hide that, struct selectors surface it.
func TestCheckEnvStructLevel(t *testing.T) {
idx := loadRealIndex(t)
env := "EINHERJAR_SERVER_CORS_ORIGINS=*\nEINHERJAR_PG_HOST=h\nEINHERJAR_PG_USER=u\nEINHERJAR_PG_PASSWORD=p\nEINHERJAR_PG_NAME=n\n"
env := "EINHERJAR_HEALTH_CHECK_TIMEOUT=5s\nEINHERJAR_PG_HOST=h\nEINHERJAR_PG_USER=u\nEINHERJAR_PG_PASSWORD=p\nEINHERJAR_PG_NAME=n\n"
// struct-level: server/Config has no CORS → CORS flagged as dead.
// struct-level: server/Config has no health timeout → the var is flagged as dead.
structF, err := checkEnvVars(idx, env, nil, []string{"web/server/Config", "db-postgres/Config"})
if err != nil {
t.Fatal(err)
}
dead := false
for _, f := range structF {
if f.Kind == "not-composed" && f.Var == "EINHERJAR_SERVER_CORS_ORIGINS" {
if f.Kind == "not-composed" && f.Var == "EINHERJAR_HEALTH_CHECK_TIMEOUT" {
dead = true
}
}
if !dead {
t.Errorf("struct-level: expected EINHERJAR_SERVER_CORS_ORIGINS flagged not-composed; got %+v", structF)
t.Errorf("struct-level: expected EINHERJAR_HEALTH_CHECK_TIMEOUT flagged not-composed; got %+v", structF)
}
// module-level ["web"] keeps CORS silent (documents the coarse behavior).
// module-level ["web"] keeps it silent (health lives in module web).
modF, err := checkEnvVars(idx, env, []string{"web", "db-postgres"}, nil)
if err != nil {
t.Fatal(err)
}
for _, f := range modF {
if f.Var == "EINHERJAR_SERVER_CORS_ORIGINS" {
t.Errorf("module-level should not flag CORS (it lives in module web), got %+v", f)
if f.Var == "EINHERJAR_HEALTH_CHECK_TIMEOUT" {
t.Errorf("module-level should not flag the health timeout (it lives in module web), got %+v", f)
}
}
}
+7 -9
View File
@@ -131,13 +131,14 @@ func Run() error {
db := postgres.New(logger, cfg.PG)
// CORS: allow-all in local dev; explicit origins elsewhere. mw.CORS panics on
// "*", so never pass a wildcard through APP_CORS_ORIGINS outside local.
// CORS: allow-all in local dev; explicit origins elsewhere. Origins come from the
// framework's own EINHERJAR_SERVER_CORS_ORIGINS (cfg.Server.CORSOrigins). mw.CORS
// panics on "*", so never set that var to a wildcard outside local.
var corsMW func(http.Handler) http.Handler
if strings.EqualFold(cfg.AppEnv, "local") {
corsMW = mw.CORSAllowAll()
} else {
corsMW = mw.CORS(cfg.CORSOrigins)
corsMW = mw.CORS(cfg.Server.CORSOrigins)
}
srv := server.New(logger, cfg.Server,
@@ -202,14 +203,13 @@ import (
// the nested framework configs, populating their EINHERJAR_* tags from the
// environment next to the app-owned APP_* fields.
type Config struct {
AppEnv string ` + "`" + `env:"APP_ENV" envDefault:"local"` + "`" + `
CORSOrigins []string ` + "`" + `env:"APP_CORS_ORIGINS" envSeparator:","` + "`" + `
AppEnv string ` + "`" + `env:"APP_ENV" envDefault:"local"` + "`" + `
// Framework component configs — composed verbatim; their EINHERJAR_* tags
// load through this same env.Parse call.
Launcher launcher.Config // EINHERJAR_COMPONENT_STOP_TIMEOUT
Log logz.Config // EINHERJAR_LOG_*
Server server.Config // EINHERJAR_SERVER_*
Server server.Config // EINHERJAR_SERVER_* (incl. EINHERJAR_SERVER_CORS_ORIGINS)
Health health.Config // EINHERJAR_HEALTH_CHECK_TIMEOUT
PG postgres.Config // EINHERJAR_PG_*
}
@@ -237,9 +237,7 @@ func renderEnvExample(idx *index.Index, app string) string {
b.WriteString("# ── App (APP_*) ────────────────────────────────────────────────────────────\n")
b.WriteString("APP_ENV=local\n")
b.WriteString("# APP_CORS_ORIGINS — explicit origins for non-local envs (comma-separated).\n")
b.WriteString("# Local uses mw.CORSAllowAll() and ignores this; \"*\" is rejected by mw.CORS — never use it.\n")
b.WriteString("APP_CORS_ORIGINS=\n\n")
b.WriteString("# CORS: local uses mw.CORSAllowAll(); non-local reads EINHERJAR_SERVER_CORS_ORIGINS below.\n\n")
writeEnvSection(&b, "Einherjar: launcher", envspec.FindStruct(idx, "core", "launcher", "Config"), app)
writeEnvSection(&b, "Einherjar: logging", envspec.FindStruct(idx, "core", "logz", "Config"), app)