Require authentication on /api/* and /events (DGS-100) - #9
Merged
Conversation
Every /api/* route now carries AuthMiddleware, on BOTH handler paths, plus
/events. Before this, anonymous callers could read and write CAS content, store
and execute workflows, open the execution WebSocket, and stream the SSE
broadcaster on an internet-reachable gateway.
WHY THIS COULD FINALLY LAND. DGS-100 sat open from 2026-07-24 on one belief:
"the SPAs call these from the browser with no token, so attaching
AuthMiddleware would break the workflow builder." That consumer does not exist
in any deployed gateway, verified three independent ways:
- server/workflowui/dist and server/chatui/dist hold only .gitkeep; the SPA
build is a separate `make build-spa` step,
- .goreleaser.yml's only before-hook is `go mod download` — no npm — so no
release has ever embedded them,
- gateway.trespies.dev answers 503 on /workflow and /chat.
The blocker was protecting a browser client that has never shipped, while the
routes it protected served the whole internet. The SPA's auth story is now a
prerequisite for SHIPPING the workflow builder, not a security blocker.
BOTH HANDLER PATHS. The workflow CRUD routes are a bare http.ServeMux mounted
through gin.WrapH. That mux cannot take gin middleware, but the gin route that
WRAPS it can, so the middleware sits at the registration site. Gating only the
gin path would have left the DGS-108 storage route open.
TWO GUARD TESTS FIRED, AND THAT IS THE POINT. router_cas_auth_test.go carried
assertions written yesterday that these routes must stay anonymous — one for
the CLI, one for the SPA — precisely so that widening the gate could not happen
by accident. Both are rewritten here rather than deleted, recording which
premise held and which did not: the SPA premise was false, the CLI premise is
true and its breakage is accepted.
ACCEPTED BREAKAGE. The `dojo` CLI attaches a bearer token only when configured,
and empty is the default, so an unconfigured CLI now gets 401 on /api/cas/* and
/api/skills. The svc:dojo-cli credential already exists; the migration is in
docs/api-route-disposition.md and must be done before this reaches a host the
CLI talks to.
No "allow anonymous /api" escape hatch was added for local SPA development. A
switch that turns authentication off is the exact shape of defect DGS-112 was.
/mesh/* and /.well-known/did.json stay open: federation peers cannot hold a
gateway JWT and need DID-signature auth, a separate design. /health and
/metrics stay public — deploy/provision.sh probes /health unauthenticated.
Verified against a real running binary over real HTTP, not just in unit tests:
every listed route 401s anonymously, a valid service token gets through, and
/health and /metrics still answer 200.
Refs DGS-100, DGS-109
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the last unauthenticated surface on the gateway. Anonymous callers could previously read and write CAS content, store and execute workflows, open the execution WebSocket, and stream the SSE broadcaster — on a host reachable from the internet.
Why this could finally land
DGS-100 sat open from 2026-07-24 on one belief, quoted from the issue:
That consumer does not exist in any deployed gateway. Verified three independent ways:
git ls-files server/workflowui/dist server/chatui/dist.gitkeep— the SPA build is a separatemake build-spastep.goreleaser.ymlbefore.hooksgo mod downloadonly. NonpmGET https://gateway.trespies.dev/workflowand/chatThe blocker was protecting a browser client that has never shipped, while the routes it protected served everyone. The SPA's auth story is now a prerequisite for shipping the workflow builder, not a reason to leave the API open.
Both handler paths
The workflow CRUD routes are a bare
http.ServeMuxmounted throughgin.WrapH. That mux cannot take gin middleware — but the gin route that wraps it can, so the middleware sits at the registration site. Gating only the gin path would have leftPOST /api/workflows, the route that stored the DGS-108 payload, wide open. The test table covers both paths explicitly for that reason.Two guard tests fired, and that is the point
router_cas_auth_test.gocarried assertions written yesterday that these routes must stay anonymous — one for the CLI, one for the SPA — precisely so widening the gate could not happen by accident:Both fired. They are rewritten rather than deleted, recording which premise held and which did not: the SPA premise was false, the CLI premise is true and its breakage is accepted with a documented migration.
Accepted breakage: the
dojoCLIcli/internal/client/client.goattaches a bearer token only whenc.tokenis non-empty, and the default is empty. An unconfigured CLI now gets 401 on/api/cas/*,/api/skillsand the workflow routes.The credential already exists (
svc:dojo-cli, from the DGS-88 work). Do this before deploying:Deliberately not done
/api" escape hatch for local SPA development. A switch that turns authentication off is the exact shape of defect DGS-112 was. Local SPA work needs token plumbing inapi.ts, which it needs anyway./mesh/*and/.well-known/did.jsonstay open — federation peers cannot hold a gateway JWT and need DID-signature auth, a separate design the issue always scoped out./healthand/metricsstay public:deploy/provision.shprobes/healthunauthenticated, so gating it would fail every provision run. There is a test pinning that.Verification
Unit tests drive the real
setupMiddleware()+setupRoutes(), because this is a wiring bug — a hand-builtgin.New()cannot observe it. Coverage: a registration guard so no 401 assertion can pass vacuously against a 404; anonymous rejection across all 23 routes; garbage-token rejection; a valid service token reaching the handler (proving it is a gate, not a wall);/gcstill refusing a service token on the admin tier; and public routes staying public.Also verified against a real running binary over real HTTP — the boundary the unit suite does not cross, which is what caught the e2e problem in PR #8:
go build ./...,go vet,go test ./...andgo test -race ./...all pass.Not deployed
The running binary predates this; the Caddy
/api/*403 protects the host until it ships. Once deployed and the CLI is configured, that edge block becomes redundant defence-in-depth rather than the only control, and removing it is a separate deliberate step.Also supersedes DGS-109 —
/api/ada/validateand/api/ws/workfloware gated here rather than in a second PR touching the same lines.Refs DGS-100, DGS-109
🤖 Generated with Claude Code