feat(mcp): add web.allowedorigins-removed rule; document v1.3.0 CORS migration

This commit is contained in:
2026-08-08 10:51:38 -06:00
parent 03a8d8a641
commit 3961ae7175
7 changed files with 118 additions and 9 deletions
+43
View File
@@ -17,9 +17,52 @@ func init() {
Module: "web",
Check: checkCORSWildcard,
},
Rule{
ID: "web.allowedorigins-removed",
Severity: SeverityError,
Module: "web",
Check: checkAllowedOriginsRemoved,
},
)
}
// checkAllowedOriginsRemoved flags any reference to the removed
// web.Config.AllowedOrigins field — both a selector (cfg.Web.AllowedOrigins) and a
// struct-literal key (web.Config{AllowedOrigins: ...}). It was env-backed through
// v1.1.x, became a code-only override in v1.2.0, and was removed in v2.0.0. Code
// that still reads it compiled but silently served no CORS in v1.2.0; in v2.0.0 it
// no longer compiles. CORS now lives solely on server.Config.CORSOrigins.
func checkAllowedOriginsRemoved(c *Context) []Finding {
const (
msg = "web.Config.AllowedOrigins was removed in v2.0.0 — CORS lives on Server.CORSOrigins (env EINHERJAR_SERVER_CORS_ORIGINS)"
hint = "Read cfg.Server.CORSOrigins (or set it in code); web.New applies it automatically. Never reintroduce a field/var for CORS origins."
)
seen := map[int]bool{}
var hits []Finding
add := func(pos token.Pos) {
line := c.Fset.Position(pos).Line
if seen[line] {
return
}
seen[line] = true
hits = append(hits, Finding{Message: msg, Hint: hint, Line: line})
}
ast.Inspect(c.File, func(n ast.Node) bool {
switch e := n.(type) {
case *ast.SelectorExpr:
if e.Sel != nil && e.Sel.Name == "AllowedOrigins" {
add(e.Sel.Pos())
}
case *ast.KeyValueExpr:
if id, ok := e.Key.(*ast.Ident); ok && id.Name == "AllowedOrigins" {
add(id.Pos())
}
}
return true
})
return hits
}
// checkCORSWildcard flags any call to <pkg>.CORS(...) whose arguments contain a
// "*" string literal — which mw.CORS rejects (panics) at boot.
func checkCORSWildcard(c *Context) []Finding {
+28
View File
@@ -38,6 +38,34 @@ func f() {
}
`
const allowedOriginsSelectorSnippet = `package wire
import "code.nochebuena.dev/einherjar/web/mw"
func f(cfg Config) { _ = mw.CORS(cfg.Web.AllowedOrigins) }
`
const allowedOriginsLiteralSnippet = `package wire
import "code.nochebuena.dev/einherjar/web"
func f() { _ = web.Config{AllowedOrigins: []string{"https://x"}} }
`
func TestAllowedOriginsSelectorFires(t *testing.T) {
got := findingsFor(Run(allowedOriginsSelectorSnippet), "web.allowedorigins-removed")
if len(got) == 0 {
t.Fatal("web.allowedorigins-removed did not fire on cfg.Web.AllowedOrigins")
}
}
func TestAllowedOriginsLiteralFires(t *testing.T) {
got := findingsFor(Run(allowedOriginsLiteralSnippet), "web.allowedorigins-removed")
if len(got) == 0 {
t.Fatal("web.allowedorigins-removed did not fire on web.Config{AllowedOrigins: ...}")
}
}
func TestCORSWildcardFires(t *testing.T) {
got := findingsFor(Run(corsWildcardSnippet), "cors.wildcard-noop")
if len(got) == 0 {