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) } } // writeErrorBody writes just the envelope, for callers that have already sent // their own status code and headers. func writeErrorBody(w http.ResponseWriter, code, message string) error { return json.NewEncoder(w).Encode(errorEnvelope{Error{Code: code, Message: message}}) } // 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) } }