Skip to content

Require authentication on /api/* and /events (DGS-100) - #9

Merged
DojoGenesis merged 1 commit into
mainfrom
fix/dgs-100-api-auth
Aug 5, 2026
Merged

Require authentication on /api/* and /events (DGS-100)#9
DojoGenesis merged 1 commit into
mainfrom
fix/dgs-100-api-auth

Conversation

@DojoGenesis

Copy link
Copy Markdown
Owner

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:

The SPAs call /api/workflows* and /api/cas/* from the browser. Attaching AuthMiddleware blind would break the workflow builder.

That consumer does not exist in any deployed gateway. Verified three independent ways:

Check Result
git ls-files server/workflowui/dist server/chatui/dist only .gitkeep — the SPA build is a separate make build-spa step
.goreleaser.yml before.hooks go mod download only. No npm
GET https://gateway.trespies.dev/workflow and /chat 503 — the deployed binary has no SPA in it

The 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.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 POST /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.go carried 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:

"a 401 here means the gate was widened to the group without addressing that caller"
"a 401 here breaks the workflow builder"

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 dojo CLI

cli/internal/client/client.go attaches a bearer token only when c.token is non-empty, and the default is empty. An unconfigured CLI now gets 401 on /api/cas/*, /api/skills and the workflow routes.

The credential already exists (svc:dojo-cli, from the DGS-88 work). Do this before deploying:

export DOJO_GATEWAY_TOKEN="$(cat ~/.claude/.trespies-secrets/gateway-token-dojo-cli.token)"

Deliberately not done

  • No "allow anonymous /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 in api.ts, which it needs anyway.
  • /mesh/* and /.well-known/did.json stay open — federation peers cannot hold a gateway JWT and need DID-signature auth, a separate design the issue always scoped out.
  • /health and /metrics stay public: deploy/provision.sh probes /health unauthenticated, 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-built gin.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); /gc still 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:

anonymous  /api/skills /api/workflows /api/cas/tags /api/cas/status
           /api/cas/content /api/workflows/demo/execute /api/ada/validate /events  -> 401
authed     same routes                                                             -> 200
public     /health /metrics                                                        -> 200

go build ./..., go vet, go test ./... and go 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/validate and /api/ws/workflow are gated here rather than in a second PR touching the same lines.

Refs DGS-100, DGS-109

🤖 Generated with Claude Code

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
@DojoGenesis
DojoGenesis merged commit cf6fc4f into main Aug 5, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants