feat(api): document bearer auth in openapi

Add a bearerAuth security scheme to the generated OpenAPI document and mark non-public operations as secured, while keeping /health explicitly public.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 09:08:34 +00:00
parent f030665566
commit 9afaed4d7c
2 changed files with 50 additions and 0 deletions

View file

@ -29,6 +29,11 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) {
},
"paths": sb.buildPaths(groups),
"tags": sb.buildTags(groups),
"security": []any{
map[string]any{
"bearerAuth": []any{},
},
},
}
// Add component schemas for the response envelope.
@ -54,6 +59,13 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) {
},
},
},
"securitySchemes": map[string]any{
"bearerAuth": map[string]any{
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
},
},
}
return json.MarshalIndent(spec, "", " ")
@ -98,6 +110,11 @@ func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any {
"description": rd.Description,
"tags": rd.Tags,
"operationId": operationID(method, fullPath, operationIDs),
"security": []any{
map[string]any{
"bearerAuth": []any{},
},
},
"responses": map[string]any{
"200": map[string]any{
"description": "Successful response",
@ -141,6 +158,14 @@ func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any {
}
}
// The built-in health check remains public, so override the inherited
// default security requirement with an explicit empty array.
if health, ok := paths["/health"].(map[string]any); ok {
if op, ok := health["get"].(map[string]any); ok {
op["security"] = []any{}
}
}
return paths
}

View file

@ -74,6 +74,25 @@ func TestSpecBuilder_Good_EmptyGroups(t *testing.T) {
if !found {
t.Fatal("expected system tag in spec")
}
components := spec["components"].(map[string]any)
securitySchemes := components["securitySchemes"].(map[string]any)
bearerAuth := securitySchemes["bearerAuth"].(map[string]any)
if bearerAuth["type"] != "http" {
t.Fatalf("expected bearerAuth.type=http, got %v", bearerAuth["type"])
}
if bearerAuth["scheme"] != "bearer" {
t.Fatalf("expected bearerAuth.scheme=bearer, got %v", bearerAuth["scheme"])
}
security := spec["security"].([]any)
if len(security) != 1 {
t.Fatalf("expected one default security requirement, got %d", len(security))
}
req := security[0].(map[string]any)
if _, ok := req["bearerAuth"]; !ok {
t.Fatal("expected default bearerAuth security requirement")
}
}
func TestSpecBuilder_Good_WithDescribableGroup(t *testing.T) {
@ -351,6 +370,12 @@ func TestSpecBuilder_Good_NonDescribableGroup(t *testing.T) {
if health["operationId"] != "get_health" {
t.Fatalf("expected operationId='get_health', got %v", health["operationId"])
}
if security := health["security"]; security == nil {
t.Fatal("expected explicit public security override on /health")
}
if len(health["security"].([]any)) != 0 {
t.Fatalf("expected /health security to be empty, got %v", health["security"])
}
}
func TestSpecBuilder_Good_ToolBridgeIntegration(t *testing.T) {