package auth import ( "encoding/json" "fmt" "strings" ) // ErrorCodeRequestScoped identifies failures tied to the current request rather // than the selected credential. const ErrorCodeRequestScoped = "request_scoped" const requestScopedErrorCode = ErrorCodeRequestScoped // ErrorCodeConnectionLifecycle marks transport/session lifecycle failures that // must skip credential cooldown without being treated as request-scoped faults. const ErrorCodeConnectionLifecycle = "connection_lifecycle" const connectionLifecycleErrorCode = ErrorCodeConnectionLifecycle // ErrorCodeForceCooldown marks failures that must enforce credential cooldown. const ErrorCodeForceCooldown = "force_cooldown" // Error describes an authentication related failure in a provider agnostic format. type Error struct { // Code is a short machine readable identifier. Code string `json:"code,omitempty"` // Message is a human readable description of the failure. Message string `json:"message"` // Retryable indicates whether a retry might fix the issue automatically. Retryable bool `json:"retryable"` // HTTPStatus optionally records an HTTP-like status code for the error. HTTPStatus int `json:"http_status,omitempty"` } // Error implements the error interface. func (e *Error) Error() string { if e == nil { return "" } if e.Code == "" { return e.Message } return e.Code + ": " + e.Message } // StatusCode implements optional status accessor for manager decision making. func (e *Error) StatusCode() int { if e == nil { return 0 } return e.HTTPStatus } // IsRequestScoped reports whether the failure is tied to the current request // rather than the selected credential. func (e *Error) IsRequestScoped() bool { return e != nil && e.Code == ErrorCodeRequestScoped } // MarkRequestScoped marks the error as request-scoped in place and returns it. func (e *Error) MarkRequestScoped() *Error { if e != nil { e.Code = ErrorCodeRequestScoped } return e } // NewRequestScopedError creates an Error explicitly flagged as request-scoped so // that credential cooldown is skipped. func NewRequestScopedError(message string, httpStatus int) *Error { return &Error{ Code: ErrorCodeRequestScoped, Message: message, HTTPStatus: httpStatus, } } type errorWithCause struct { base *Error cause error } func (e *errorWithCause) Error() string { if e == nil || e.base == nil { return "" } baseText := e.base.Error() if e.cause == nil { return baseText } summary := ExtractUpstreamErrorSummary(e.cause.Error()) if summary != "" && !strings.Contains(baseText, summary) { return fmt.Sprintf("%s (last upstream error: %s)", baseText, summary) } return baseText } func (e *errorWithCause) Unwrap() error { if e == nil { return nil } return e.cause } func (e *errorWithCause) As(target any) bool { if e == nil { return false } if t, ok := target.(**Error); ok { *t = e.base return true } return false } func (e *errorWithCause) Is(target error) bool { if e == nil { return target == nil } if target == e || target == e.base { return true } if other, ok := target.(*errorWithCause); ok && other != nil { return e.base == other.base } if otherErr, ok := target.(*Error); ok && otherErr != nil { return e.base == otherErr } return false } func (e *errorWithCause) StatusCode() int { if e == nil || e.base == nil { return 0 } return e.base.HTTPStatus } func (e *errorWithCause) IsRequestScoped() bool { if e == nil || e.base == nil { return false } return e.base.IsRequestScoped() } func (e *errorWithCause) MarkRequestScoped() *Error { if e == nil || e.base == nil { return nil } return e.base.MarkRequestScoped() } func (e *errorWithCause) MarshalJSON() ([]byte, error) { if e == nil || e.base == nil { return []byte("null"), nil } return json.Marshal(e.base) } // WithCause wraps an *Error with an underlying cause without changing the Error struct layout. func WithCause(err *Error, cause error) error { if err == nil { return nil } if cause == nil { return err } return &errorWithCause{ base: err, cause: cause, } }