package vault import ( "context" "errors" "net" "strings" "github.com/hashicorp/vault/api" ) // ErrKind classifies a Vault API error into a small, UI-actionable set. // See internal/ui's error surfacing tiers: Cancelled is dropped silently, // NotFound/Forbidden/CAS become a toast plus an inline hint, Unauthorized // sends the user back to the auth screen, Sealed/Network get a full-body // retry panel. type ErrKind uint8 const ( ErrUnknown ErrKind = iota ErrForbidden ErrNotFound ErrUnauthorized ErrSealed ErrCAS ErrNetwork ErrCancelled ) // Classify inspects err (typically returned from a Logical() call) and // returns its kind plus a short human-readable message. func Classify(err error) (ErrKind, string) { if err == nil { return ErrUnknown, "" } if errors.Is(err, context.Canceled) { return ErrCancelled, "cancelled" } var respErr *api.ResponseError if errors.As(err, &respErr) { msg := strings.Join(respErr.Errors, "; ") if msg == "" { msg = err.Error() } switch respErr.StatusCode { case 403: if isTokenInvalid(msg) { return ErrUnauthorized, "token is invalid or expired" } return ErrForbidden, "permission denied" case 404: return ErrNotFound, "not found" case 400: if strings.Contains(strings.ToLower(msg), "check-and-set") { return ErrCAS, "changed underneath you (check-and-set mismatch)" } return ErrUnknown, msg case 503: return ErrSealed, "vault is sealed or in standby" default: return ErrUnknown, msg } } var netErr net.Error if errors.As(err, &netErr) { return ErrNetwork, netErr.Error() } return ErrUnknown, err.Error() } func isTokenInvalid(msg string) bool { m := strings.ToLower(msg) return strings.Contains(m, "permission denied") && strings.Contains(m, "token") }