package api import ( "encoding/json" "log/slog" "net/http" ) // Error is the single shape every failure takes. Clients get a stable machine // readable code plus a human message. A self-hosted bot instance can branch on // the code without parsing prose. type Error struct { Code string `json:"code"` Message string `json:"message"` } type errorEnvelope struct { Error Error `json:"error"` } const ( CodeMissingKey = "missing_key" CodeInvalidKey = "invalid_key" CodeKeyRevoked = "key_revoked" CodeKeySuspended = "key_suspended" CodeKeyExpired = "key_expired" CodeOwnerBlocked = "owner_blocked" CodeForbidden = "insufficient_scope" CodeRateLimited = "rate_limited" CodeInternal = "internal_error" ) // WriteError sends a JSON error. Deliberately specific about WHY authentication // failed rather than a uniform "unauthorized". // // The usual argument for being vague is to prevent enumeration, but a key is // 256 bits from crypto/rand - there is nothing to enumerate. Telling a // developer their key expired, rather than making them guess, is worth far more // than the non-existent secrecy gained. func WriteError(w http.ResponseWriter, status int, code, message string) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(errorEnvelope{Error{Code: code, Message: message}}); err != nil { slog.Error("writing error response", "err", err) } } // WriteJSON sends a success payload. func WriteJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(v); err != nil { slog.Error("writing response", "err", err) } }