Skip to content

refactor(api): extract cors origin classification into internal/api/cors leaf (ADR-0011)#454

Merged
krisarmstrong merged 1 commit into
mainfrom
refactor/api-cors-leaf
Jun 16, 2026
Merged

refactor(api): extract cors origin classification into internal/api/cors leaf (ADR-0011)#454
krisarmstrong merged 1 commit into
mainfrom
refactor/api-cors-leaf

Conversation

@krisarmstrong

Copy link
Copy Markdown
Collaborator

Summary

Extracts the CORS Origin-header classification out of the flat
internal/api namespace into the internal/api/cors leaf package —
the fourth slice of the ADR-0011 internal/api sub-package decomposition
(after ratelimit #451, sse #452, tlsutil #453).

The leaf exposes IsLocalhostOrigin, IsSameOrigin, and
IsRFC1918Origin, with the strict complete-IP-structure validation that
rejects CORS-bypass tricks like localhost.evil.com and
192.168.1.1.evil.com. It depends only on stdlib (net/url, strings).
The new api-cors-isolated depguard rule statically forbids any upward
import of the transport layer.

The HTTP plumbing stays in internal/api: the corsMiddleware middleware,
the opt-in env read corsAllowPrivateEnabled (which logs the
credentialed-LAN-origin warning), and the response-header wiring. As a
side effect server.go shrinks from 1007 → 870 lines (god-file
reduction).

The security-sensitive origin logic is now unit-tested directly against
the leaf: the old cors_internal_test.go and four classifier tests that
were embedded in the 2900-line server_internal_test.go are relocated to
internal/api/cors (exported-API tests external, unexported-helper tests
internal). The corsMiddleware integration tests stay in internal/api.

No behaviour change: CORS allow/deny decisions and response headers are
identical.

Linked Issue

Related to #450

Testing Evidence

Gated from a clean worktree off origin/main (go 1.26.4,
golangci-lint v2.12.2):

$ go build ./...
BUILD_OK

$ go vet ./internal/api/...
VET_OK

$ golangci-lint run ./internal/api/...
0 issues.

$ go test ./internal/api/... -count=1
ok  github.com/MustardSeedNetworks/stem/internal/api            408.650s
ok  github.com/MustardSeedNetworks/stem/internal/api/cors       0.112s
ok  github.com/MustardSeedNetworks/stem/internal/api/ratelimit  0.164s
ok  github.com/MustardSeedNetworks/stem/internal/api/sse        0.065s
ok  github.com/MustardSeedNetworks/stem/internal/api/tlsutil    10.781s

$ bash scripts/check-route-policy.sh
✓ Route-policy gate: all /api routes go through the capability registry.

$ bash scripts/check-output-escaping.sh
OK: no raw innerHTML injection and no value-interpolating Fprintf in internal/api.

$ bash scripts/check-json-casing.sh
JSON casing gate OK — no new snake_case wire tags.

$ STRICT=1 bash scripts/check-file-size.sh
📊 Found 0 red flag(s), 38 warning(s)   # warnings are pre-existing UI files

$ gitleaks protect --staged --no-banner
no leaks found

Security and Release Checklist

  • No behaviour change — pure structural move; CORS allow/deny
    decisions and response headers are byte-identical.
  • Origin-classification logic (the CSRF-bypass-relevant security
    boundary) is preserved exactly and now has dedicated unit coverage in
    the leaf; the corsMiddleware integration tests still pass unchanged.
  • Leaf boundary statically enforced (api-cors-isolated depguard
    rule); golangci-lint clean.
  • No new secrets; gitleaks clean on staged content.
  • No new mutating routes / auth surfaces — CSRF, rate-limit, role
    gating, output encoding invariants untouched.
  • No customer-facing copy; no banned vocabulary.
  • ADR-0011 updated to record the cors slice.

…ors leaf (ADR-0011)

Move the Origin-header classification used by the CORS policy out of the
flat internal/api namespace into the internal/api/cors leaf package
(ADR-0011, fourth slice). The leaf exposes IsLocalhostOrigin,
IsSameOrigin, and IsRFC1918Origin — with the strict complete-IP-structure
validation that rejects bypass tricks like "localhost.evil.com" and
"192.168.1.1.evil.com" — and depends only on stdlib (net/url, strings).
The api-cors-isolated depguard rule statically forbids any upward import
of the transport layer.

The HTTP middleware that consumes the classifiers (corsMiddleware), the
opt-in env read (corsAllowPrivateEnabled), and the response-header wiring
stay in internal/api. server.go drops from 1007 to 870 lines.

The security-sensitive origin logic is now unit-tested directly against
the leaf: the existing cors_internal_test.go and four classifier tests
that were embedded in server_internal_test.go are relocated to
internal/api/cors (exported-API tests external, unexported-helper tests
internal).

No behaviour change: CORS allow/deny decisions and response headers are
identical.
@github-actions

Copy link
Copy Markdown
Contributor

License Compliance Report

All dependencies pass license compliance checks

Go Dependencies

  • Unknown: 35 package(s)
  • MIT: 26 package(s)
  • BSD-3-Clause: 16 package(s)
  • Apache-2.0: 11 package(s)
  • BSD-2-Clause: 1 package(s)

npm Dependencies

See full report in workflow artifacts

Allowed Licenses: MIT, Apache-2.0, BSD-*, ISC, CC0-1.0, MPL-2.0
Forbidden: GPL, AGPL, SSPL (strong copyleft)

@krisarmstrong krisarmstrong merged commit da2bd38 into main Jun 16, 2026
27 checks passed
@krisarmstrong krisarmstrong deleted the refactor/api-cors-leaf branch June 16, 2026 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant