package shared import ( "errors" "net/http" "strings" ) const ( // ErrorCodeInvalidRequest reports malformed or semantically invalid service // input. ErrorCodeInvalidRequest = "invalid_request" // ErrorCodeChallengeNotFound reports that the requested challenge does not // exist. ErrorCodeChallengeNotFound = "challenge_not_found" // ErrorCodeChallengeExpired reports that the requested challenge may no // longer be confirmed. ErrorCodeChallengeExpired = "challenge_expired" // ErrorCodeInvalidCode reports that the submitted confirmation code does not // match the stored challenge. ErrorCodeInvalidCode = "invalid_code" // ErrorCodeInvalidClientPublicKey reports that the submitted client public // key does not satisfy the Ed25519/base64 contract. ErrorCodeInvalidClientPublicKey = "invalid_client_public_key" // ErrorCodeBlockedByPolicy reports that the auth flow is denied by current // user or registration policy. ErrorCodeBlockedByPolicy = "blocked_by_policy" // ErrorCodeSessionLimitExceeded reports that creating another active session // would violate the configured limit. ErrorCodeSessionLimitExceeded = "session_limit_exceeded" // ErrorCodeSessionNotFound reports that the requested device session does // not exist. ErrorCodeSessionNotFound = "session_not_found" // ErrorCodeSubjectNotFound reports that the requested trusted internal // subject does not exist. ErrorCodeSubjectNotFound = "subject_not_found" // ErrorCodeServiceUnavailable reports that a required dependency or // propagation step is temporarily unavailable. ErrorCodeServiceUnavailable = "service_unavailable" // ErrorCodeInternalError reports that local state is inconsistent or an // invariant was broken unexpectedly. ErrorCodeInternalError = "internal_error" ) const genericInvalidRequestMessage = "request is invalid" var publicErrorStatusCodes = map[string]int{ ErrorCodeInvalidRequest: http.StatusBadRequest, ErrorCodeInvalidClientPublicKey: http.StatusBadRequest, ErrorCodeInvalidCode: http.StatusBadRequest, ErrorCodeChallengeNotFound: http.StatusNotFound, ErrorCodeChallengeExpired: http.StatusGone, ErrorCodeBlockedByPolicy: http.StatusForbidden, ErrorCodeSessionLimitExceeded: http.StatusConflict, ErrorCodeServiceUnavailable: http.StatusServiceUnavailable, } var publicStableMessages = map[string]string{ ErrorCodeChallengeNotFound: "challenge not found", ErrorCodeChallengeExpired: "challenge expired", ErrorCodeInvalidCode: "confirmation code is invalid", ErrorCodeInvalidClientPublicKey: "client_public_key is not a valid base64-encoded raw 32-byte Ed25519 public key", ErrorCodeBlockedByPolicy: "authentication is blocked by policy", ErrorCodeSessionLimitExceeded: "active session limit would be exceeded", ErrorCodeServiceUnavailable: "service is unavailable", } var internalErrorStatusCodes = map[string]int{ ErrorCodeInvalidRequest: http.StatusBadRequest, ErrorCodeSessionNotFound: http.StatusNotFound, ErrorCodeSubjectNotFound: http.StatusNotFound, ErrorCodeServiceUnavailable: http.StatusServiceUnavailable, ErrorCodeInternalError: http.StatusInternalServerError, } var internalStableMessages = map[string]string{ ErrorCodeSessionNotFound: "session not found", ErrorCodeSubjectNotFound: "subject not found", ErrorCodeServiceUnavailable: "service is unavailable", ErrorCodeInternalError: "internal server error", } // PublicErrorProjection describes one transport-ready public auth error after // internal service errors have been normalized to the frozen client-safe // surface. type PublicErrorProjection struct { // StatusCode is the HTTP status that should be returned to the public auth // caller. StatusCode int // Code is the stable client-safe error code written into the public JSON // envelope. Code string // Message is the client-safe error description exposed to the public auth // caller. Message string } // InternalErrorProjection describes one transport-ready internal API error // after service-layer failures have been normalized to the frozen trusted // caller surface. type InternalErrorProjection struct { // StatusCode is the HTTP status that should be returned to the trusted // caller. StatusCode int // Code is the stable error code written into the internal JSON envelope. Code string // Message is the trusted-caller-safe error description exposed by the // internal HTTP API. Message string } // ServiceError projects one stable application-layer failure with a service // error code and a caller-safe message. type ServiceError struct { // Code is the stable error code expected by later transport mapping. Code string // Message is the caller-safe error description. Message string // Err optionally stores the wrapped underlying cause. Err error } // Error returns the caller-safe error description. func (e *ServiceError) Error() string { if e == nil { return "" } switch { case strings.TrimSpace(e.Message) != "": return e.Message case strings.TrimSpace(e.Code) != "": return e.Code case e.Err != nil: return e.Err.Error() default: return ErrorCodeInternalError } } // Unwrap returns the wrapped cause, if any. func (e *ServiceError) Unwrap() error { if e == nil { return nil } return e.Err } // NewServiceError returns a new typed application-layer error. func NewServiceError(code string, message string, err error) *ServiceError { return &ServiceError{ Code: strings.TrimSpace(code), Message: strings.TrimSpace(message), Err: err, } } // IsPublicErrorCode reports whether code belongs to the frozen public auth // error surface. func IsPublicErrorCode(code string) bool { _, ok := publicErrorStatusCodes[strings.TrimSpace(code)] return ok } // IsInternalOnlyErrorCode reports whether code is intentionally excluded from // the public auth transport surface. func IsInternalOnlyErrorCode(code string) bool { switch strings.TrimSpace(code) { case ErrorCodeSessionNotFound, ErrorCodeSubjectNotFound, ErrorCodeInternalError: return true default: return false } } // IsSendEmailCodePublicErrorCode reports whether code may be exposed by the // public send-email-code route after public projection. func IsSendEmailCodePublicErrorCode(code string) bool { switch strings.TrimSpace(code) { case ErrorCodeInvalidRequest, ErrorCodeServiceUnavailable: return true default: return false } } // IsConfirmEmailCodePublicErrorCode reports whether code may be exposed by the // public confirm-email-code route after public projection. func IsConfirmEmailCodePublicErrorCode(code string) bool { switch strings.TrimSpace(code) { case ErrorCodeInvalidRequest, ErrorCodeChallengeNotFound, ErrorCodeChallengeExpired, ErrorCodeInvalidCode, ErrorCodeInvalidClientPublicKey, ErrorCodeBlockedByPolicy, ErrorCodeSessionLimitExceeded, ErrorCodeServiceUnavailable: return true default: return false } } // PublicHTTPStatusCode reports the frozen public HTTP status for code. Unknown // or internal-only codes are normalized to 503 service_unavailable. func PublicHTTPStatusCode(code string) int { if statusCode, ok := publicErrorStatusCodes[strings.TrimSpace(code)]; ok { return statusCode } return http.StatusServiceUnavailable } // ProjectPublicError normalizes err to the frozen public-auth error surface. // Unknown and internal-only service failures are intentionally projected as // 503 service_unavailable so internal invariants do not leak to public callers. func ProjectPublicError(err error) PublicErrorProjection { serviceErr, ok := errors.AsType[*ServiceError](err) code := CodeOf(err) if !IsPublicErrorCode(code) { return PublicErrorProjection{ StatusCode: http.StatusServiceUnavailable, Code: ErrorCodeServiceUnavailable, Message: publicMessageForCode(ErrorCodeServiceUnavailable, ""), } } message := "" if ok && serviceErr != nil { message = serviceErr.Message } return PublicErrorProjection{ StatusCode: PublicHTTPStatusCode(code), Code: code, Message: publicMessageForCode(code, message), } } // InternalHTTPStatusCode reports the frozen internal HTTP status for code. // Unknown codes are normalized to 500 internal_error. func InternalHTTPStatusCode(code string) int { if statusCode, ok := internalErrorStatusCodes[strings.TrimSpace(code)]; ok { return statusCode } return http.StatusInternalServerError } // ProjectInternalError normalizes err to the frozen internal trusted HTTP // error surface. Unknown failures are intentionally projected as // 500 internal_error so transport callers do not depend on unclassified local // failures. func ProjectInternalError(err error) InternalErrorProjection { serviceErr, ok := errors.AsType[*ServiceError](err) code := CodeOf(err) if _, known := internalErrorStatusCodes[code]; !known { return InternalErrorProjection{ StatusCode: http.StatusInternalServerError, Code: ErrorCodeInternalError, Message: internalMessageForCode(ErrorCodeInternalError, ""), } } message := "" if ok && serviceErr != nil { message = serviceErr.Message } return InternalErrorProjection{ StatusCode: InternalHTTPStatusCode(code), Code: code, Message: internalMessageForCode(code, message), } } // InvalidRequest reports one malformed or semantically invalid caller input. func InvalidRequest(message string) *ServiceError { return NewServiceError(ErrorCodeInvalidRequest, message, nil) } // ChallengeNotFound reports that the requested challenge does not exist. func ChallengeNotFound() *ServiceError { return NewServiceError(ErrorCodeChallengeNotFound, "challenge not found", nil) } // ChallengeExpired reports that the requested challenge is expired. func ChallengeExpired() *ServiceError { return NewServiceError(ErrorCodeChallengeExpired, "challenge expired", nil) } // InvalidCode reports that the submitted confirmation code is invalid. func InvalidCode() *ServiceError { return NewServiceError(ErrorCodeInvalidCode, "confirmation code is invalid", nil) } // InvalidClientPublicKey reports that the submitted client public key does not // satisfy the frozen contract. func InvalidClientPublicKey() *ServiceError { return NewServiceError( ErrorCodeInvalidClientPublicKey, "client_public_key is not a valid base64-encoded raw 32-byte Ed25519 public key", nil, ) } // BlockedByPolicy reports that the current auth flow is denied by policy. func BlockedByPolicy() *ServiceError { return NewServiceError(ErrorCodeBlockedByPolicy, "authentication is blocked by policy", nil) } // SessionLimitExceeded reports that creating another active session would // exceed the current configured limit. func SessionLimitExceeded() *ServiceError { return NewServiceError(ErrorCodeSessionLimitExceeded, "active session limit would be exceeded", nil) } // SessionNotFound reports that the requested session does not exist. func SessionNotFound() *ServiceError { return NewServiceError(ErrorCodeSessionNotFound, "session not found", nil) } // SubjectNotFound reports that the requested internal subject does not exist. func SubjectNotFound() *ServiceError { return NewServiceError(ErrorCodeSubjectNotFound, "subject not found", nil) } // ServiceUnavailable reports that a required dependency or propagation step is // temporarily unavailable. func ServiceUnavailable(err error) *ServiceError { return NewServiceError(ErrorCodeServiceUnavailable, "service is unavailable", err) } // InternalError reports an invariant-breaking local failure. func InternalError(err error) *ServiceError { return NewServiceError(ErrorCodeInternalError, "internal error", err) } // CodeOf returns the stable service error code of err when err wraps a // ServiceError. Otherwise it returns ErrorCodeInternalError. func CodeOf(err error) string { serviceErr, ok := errors.AsType[*ServiceError](err) if !ok || serviceErr == nil || strings.TrimSpace(serviceErr.Code) == "" { return ErrorCodeInternalError } return serviceErr.Code } func publicMessageForCode(code string, message string) string { trimmedMessage := strings.TrimSpace(message) switch strings.TrimSpace(code) { case ErrorCodeInvalidRequest: if trimmedMessage != "" { return trimmedMessage } return genericInvalidRequestMessage case ErrorCodeServiceUnavailable: return publicStableMessages[ErrorCodeServiceUnavailable] default: if stableMessage, ok := publicStableMessages[strings.TrimSpace(code)]; ok { return stableMessage } return publicStableMessages[ErrorCodeServiceUnavailable] } } func internalMessageForCode(code string, message string) string { trimmedMessage := strings.TrimSpace(message) switch strings.TrimSpace(code) { case ErrorCodeInvalidRequest: if trimmedMessage != "" { return trimmedMessage } return genericInvalidRequestMessage case ErrorCodeSessionNotFound, ErrorCodeSubjectNotFound, ErrorCodeServiceUnavailable, ErrorCodeInternalError: if stableMessage, ok := internalStableMessages[strings.TrimSpace(code)]; ok { return stableMessage } return internalStableMessages[ErrorCodeInternalError] default: return internalStableMessages[ErrorCodeInternalError] } }