From 9afaed4d7cc32a93fa49245630bb8e0f7b95c37f Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 09:08:34 +0000 Subject: [PATCH] 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 --- openapi.go | 25 +++++++++++++++++++++++++ openapi_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/openapi.go b/openapi.go index 0186c48..e88aa70 100644 --- a/openapi.go +++ b/openapi.go @@ -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 } diff --git a/openapi_test.go b/openapi_test.go index 695bf4d..bd799f2 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -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) {