# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. > **Cross-repo rules** — see `/Users/wen/project/rag/CLAUDE.md` for full workspace conventions. > - .NET backends share JWT key: `RagJwtSecretKey2026MustBeAtLeast32CharsLong!` > - gRPC auth: call `rag-backend:50051` for token validation + permission checks > - Other repos: `rag-backend` (5211), `im-system` (5212), `work-flow`, `rag-frontend` (5666 Vue) ## Build & Run ```bash go run cmd/server/main.go # HTTP :8080 go test ./... # All tests docker compose up -d # Via private registry buf generate # Regenerate proto code ``` ## Architecture Go 1.25 microservice. Clean Architecture: `domain` ← `infrastructure` ← `api`. ``` cmd/server/main.go → Entry point, manual wiring internal/domain/repository/ → Pure interfaces (FileRepository) internal/infrastructure/ s3/ → S3 adapter (AWS SDK v2, RustFS/MinIO-compatible) grpc/ → gRPC auth client (token validation via rag-backend) mediator/ → Generic CQRS mediator (Go generics + reflect) internal/api/ endpoints/ → Gin HTTP handlers (thin controllers) handlers/ → CQRS command/query handlers (business logic) requests/ → Request DTOs (form/json struct tags) validators/ → Input validation + sanitization internal/middleware/ → Auth, CORS, logging, rate limit, request ID, timeout internal/common/ → Config (env-only), errors, logger, OTel, sanitization api/proto/ → Protobuf definitions + generated Go code ``` ## Code Patterns ### Generic Mediator (CQRS) ```go // Define command/query struct type UploadFileCommand struct { BucketName string; FileName string; Data io.Reader } // Handler implements generic interface type UploadFileHandler struct { Repo repository.FileRepository } func (h *UploadFileHandler) Handle(ctx context.Context, cmd UploadFileCommand) (string, error) { ... } // Register: mediator.Register[RequestType, ResponseType](m, handler) mediator.Register[UploadFileCommand, string](m, uploadHandler) // Dispatch: mediator.Send[RequestType, ResponseType](m, ctx, cmd) result, err := mediator.Send[UploadFileCommand, string](e.Mediator, c.Request.Context(), cmd) ``` Commands named `XxxCommand`, queries named `XxxQuery`. Split across files: `file_commands.go`, `file_queries.go`, `bucket_commands.go`. ### Endpoint Pattern (4 steps) ```go func (e *FileEndpoint) UploadFile(c *gin.Context) { // 1. Bind request var req requests.UploadFileRequest if err := c.ShouldBind(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 2. Validate if err := e.FileValidator.ValidateUpload(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 3. Send through mediator result, err := mediator.Send[XxxCommand, string](e.Mediator, ctx, cmd) if err != nil { handleError(c, err); return } // 4. Respond c.JSON(http.StatusOK, gin.H{"message": result}) } ``` ### Error Handling ```go // Domain errors type BusinessException struct { Message string; Code int } func NewBusinessError(msg string) *BusinessException // 400 func NewNotFoundError(msg string) *BusinessException // 404 // Endpoint error handler func handleError(c *gin.Context, err error) { if be, ok := err.(*common.BusinessException); ok { c.JSON(be.Code, gin.H{"error": be.Message}) } else { common.Logger.Error("unhandled error", "error", err, "path", c.Request.URL.Path) c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) } } ``` ### Request Struct Tags - GET query params: `form:"bucket_name"` (bound via `c.ShouldBind`) - JSON body: `json:"bucket_name"` (bound via `c.ShouldBindJSON`) - File upload: `form:"file"` with `*multipart.FileHeader` ### Auth Middleware Dual-mode (configured by `GRPC_AUTH_ADDR` env var): - **With gRPC**: `JWTAuthMiddleware` → calls rag-backend's `ValidateToken` RPC with token caching (2min TTL) - **Without gRPC**: `AuthMiddleware` → checks `X-API-Key` header against configured key ### Middleware Chain ``` gin.Default (Logger + Recovery) → OTel tracing → RequestID (X-Request-ID) → Logging (structured: method, path, status, duration_ms, request_id) → Timeout (30s context deadline) → CORS → Auth (API key or JWT) ``` ### Input Sanitization - `SanitizeObjectKey(key)` — blocks `..`, `//`, leading `/` - `SanitizeBucketName(name)` — regex: `^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$` - `SanitizeFilename(name)` — escapes quotes, strips CR/LF ### Testing Standard `testing` package (no testify). Table-driven tests for sanitization. `httptest.NewRecorder` + `gin.TestMode` for middleware tests. ### Config Environment variables only (12-factor). No YAML/JSON config files. Loaded in `internal/common/config.go`.