Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `--data` input is now checked for well-formed JSON even when the operation has no local schema, so malformed payloads fail locally with a clear message instead of being sent to the API as-is.
- Encode `--query` filters with real JSON-to-query semantics. An array value now sends repeated parameters (`?fields=a&fields=b`) instead of the literal `[a b]`, numbers keep their original literal (no more `1e+06` for `1000000`), a JSON `null` omits the parameter instead of sending an empty value, and a nested object is rejected with a clear error rather than silently building a wrong request. A malformed or nested `--query` now classifies as a `validation` error in the JSON error envelope instead of `unknown`.
- Send every value of a repeated `-q`/`--query` param on `auth0 api` (for example `-q "fields=a" -q "fields=b"`), instead of keeping only the last one
- Classify local, user-fixable command failures with a specific `code` and `reason` in the JSON error envelope instead of the generic `unknown`/`unclassified`. Missing required flags, incompatible flag combinations, and invalid flag values (across `auth0 client-grants`, `auth0 apps`, `auth0 quickstarts`, `auth0 network-acl`, `auth0 guardian`, `auth0 email`, `auth0 actions`, `auth0 roles`, `auth0 users`, `auth0 flows`, `auth0 forms`, `auth0 login`, `auth0 terraform generate`, and others) now surface as `usage`; malformed local JSON payloads and other client-side input validation surface as `validation`. Human-facing messages are unchanged
- Classify `409` API responses as `conflict` in the JSON error envelope, and `410`/`415` as `validation`, instead of the generic `unknown`/`server_error`

# [v1.35.0](https://github.com/auth0/auth0-cli/tree/v1.35.0) (September 10, 2026)

Expand Down
12 changes: 6 additions & 6 deletions internal/cli/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -793,15 +793,15 @@ func diffActionCmd(cli *cli) *cobra.Command {
// Exactly one version was supplied. Require both or neither so an
// explicitly pinned version is never silently discarded (in
// interactive mode the picker would otherwise overwrite it).
return fmt.Errorf("provide both --version1 and --version2, or neither")
return usageError{err: fmt.Errorf("provide both --version1 and --version2, or neither"), reason: "incompatible_flags"}
case canPrompt(cmd):
var err error
inputs.version1, inputs.version2, err = pickTwoVersions(allVersions)
if err != nil {
return err
}
default:
return fmt.Errorf("missing required flags in non-interactive mode: --version1 and --version2")
return usageError{err: fmt.Errorf("missing required flags in non-interactive mode: --version1 and --version2"), reason: "missing_required_flags"}
}

var code1, code2 string
Expand Down Expand Up @@ -967,7 +967,7 @@ func inputModulesToActionModules(modules []string) (*[]management.ActionModules,
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
if !found || key == "" || value == "" {
return nil, fmt.Errorf("invalid --module value %q: expected comma-separated key=value pairs (e.g. \"module_id=<uuid>,module_version_id=<uuid>\")", raw)
return nil, usageError{err: fmt.Errorf("invalid --module value %q: expected comma-separated key=value pairs (e.g. \"module_id=<uuid>,module_version_id=<uuid>\")", raw), reason: "invalid_flag_value"}
}

switch key {
Expand All @@ -976,15 +976,15 @@ func inputModulesToActionModules(modules []string) (*[]management.ActionModules,
case "module_version_id":
module.ModuleVersionID = auth0.String(value)
default:
return nil, fmt.Errorf("invalid --module value %q: unknown key %q (supported keys: module_id, module_version_id)", raw, key)
return nil, usageError{err: fmt.Errorf("invalid --module value %q: unknown key %q (supported keys: module_id, module_version_id)", raw, key), reason: "invalid_flag_value"}
}
}

if module.ModuleID == nil {
return nil, fmt.Errorf("invalid --module value %q: module_id is required", raw)
return nil, usageError{err: fmt.Errorf("invalid --module value %q: module_id is required", raw), reason: "invalid_flag_value"}
}
if module.ModuleVersionID == nil {
return nil, fmt.Errorf("invalid --module value %q: module_version_id is required (the UUID of a specific module version)", raw)
return nil, usageError{err: fmt.Errorf("invalid --module value %q: module_version_id is required (the UUID of a specific module version)", raw), reason: "invalid_flag_value"}
}

actionModules = append(actionModules, module)
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/actions_modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ func createActionModuleCmd(cli *cli) *cobra.Command {
}

if !actionModuleNamePattern.MatchString(inputs.Name) {
return fmt.Errorf("invalid name %q: must start with a lowercase letter or digit and contain only lowercase letters, digits, underscores, and hyphens", inputs.Name)
return validationError{err: fmt.Errorf("invalid name %q: must start with a lowercase letter or digit and contain only lowercase letters, digits, underscores, and hyphens", inputs.Name), reason: "invalid_flag_value"}
}

if err := actionModuleCode.OpenEditor(
Expand Down
6 changes: 3 additions & 3 deletions internal/cli/acul_app_scaffolding.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,8 @@ func selectTemplate(cmd *cobra.Command, manifest *Manifest, providedTemplate str
return key, nil
}
}
return "", fmt.Errorf("invalid template '%s'. Available templates: %s",
providedTemplate, strings.Join(templateNames, ", "))
return "", usageError{err: fmt.Errorf("invalid template '%s'. Available templates: %s",
providedTemplate, strings.Join(templateNames, ", ")), reason: "invalid_flag_value"}
}

var chosenTemplateName string
Expand Down Expand Up @@ -326,7 +326,7 @@ func validateAndSelectScreens(cli *cli, screenIDs, providedScreens []string, mul
return nil, err
}
if len(selected) == 0 {
return nil, fmt.Errorf("at least one screen must be selected")
return nil, usageError{err: fmt.Errorf("at least one screen must be selected"), reason: "missing_required_flags"}
}
return selected, nil
}
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/acul_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ func fetchRenderSettings(cmd *cobra.Command, cli *cli, input aculConfigInput) (*
return nil, nil, false, fmt.Errorf("unable to read file %q: %v", input.filePath, err)
}
if err := json.Unmarshal(data, &renderSettings); err != nil {
return nil, nil, false, fmt.Errorf("file %q contains invalid JSON: %v", input.filePath, err)
return nil, nil, false, validationError{err: fmt.Errorf("file %q contains invalid JSON: %v", input.filePath, err), reason: "malformed_json"}
}
clearValue, shouldClear := detectHeadTagsClear(data)
return renderSettings, clearValue, shouldClear, nil
Expand All @@ -452,7 +452,7 @@ func fetchRenderSettings(cmd *cobra.Command, cli *cli, input aculConfigInput) (*
message := fmt.Sprintf("Use file '%s' for updating remote ACUL configs for '%s'? : ", ansi.Green(defaultFilePath), ansi.Blue(input.screenName))
if confirmed := prompt.Confirm(message); confirmed {
if err := json.Unmarshal(data, &renderSettings); err != nil {
return nil, nil, false, fmt.Errorf("file %s contains invalid JSON: %v", defaultFilePath, err)
return nil, nil, false, validationError{err: fmt.Errorf("file %s contains invalid JSON: %v", defaultFilePath, err), reason: "malformed_json"}
}
clearValue, shouldClear := detectHeadTagsClear(data)
return renderSettings, clearValue, shouldClear, nil
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/acul_dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, sc
}

if _, err = strconv.Atoi(portInput); err != nil {
return fmt.Errorf("invalid port number: %s", portInput)
return usageError{err: fmt.Errorf("invalid port number: %s", portInput), reason: "invalid_flag_value"}
}

port = portInput
Expand Down Expand Up @@ -409,7 +409,7 @@ func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, sc
func validateAculProject(projectDir string) error {
packagePath := filepath.Join(projectDir, "package.json")
if _, err := os.Stat(packagePath); os.IsNotExist(err) {
return fmt.Errorf("package.json not found. This doesn't appear to be a valid ACUL project")
return validationError{err: fmt.Errorf("package.json not found. This doesn't appear to be a valid ACUL project"), reason: "invalid_project_dir"}
}
return nil
}
Expand Down
40 changes: 23 additions & 17 deletions internal/cli/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,11 +253,14 @@ func (i *apiCmdInputs) validateAndSetMethod() error {
}
}

return fmt.Errorf(
"invalid method given: %s, accepting only %s",
i.RawMethod,
strings.Join(apiValidMethods, ", "),
)
return usageError{
err: fmt.Errorf(
"invalid method given: %s, accepting only %s",
i.RawMethod,
strings.Join(apiValidMethods, ", "),
),
reason: "invalid_flag_value",
}
}

func (i *apiCmdInputs) validateAndSetData() error {
Expand All @@ -272,7 +275,7 @@ func (i *apiCmdInputs) validateAndSetData() error {

if len(data) > 0 {
if err := json.Unmarshal(data, &i.Data); err != nil {
return fmt.Errorf("invalid JSON data provided: %w", err)
return validationError{err: fmt.Errorf("invalid JSON data provided: %w", err), reason: "malformed_json"}
}
}

Expand All @@ -293,14 +296,14 @@ func (i *apiCmdInputs) resolveData() ([]byte, error) {
return nil, err
}
if len(data) == 0 {
return nil, fmt.Errorf("no data received on stdin")
return nil, validationError{err: fmt.Errorf("no data received on stdin"), reason: "missing_input"}
}
return data, nil
case strings.HasPrefix(i.RawData, "@"):
path := i.RawData[1:]
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read data file %q: %w", path, err)
return nil, usageError{err: fmt.Errorf("failed to read data file %q: %w", path, err), reason: "invalid_flag_value"}
}
return data, nil
default:
Expand All @@ -314,7 +317,7 @@ func (i *apiCmdInputs) resolveData() ([]byte, error) {
func (i *apiCmdInputs) validateAndSetEndpoint(domain string) error {
endpoint, err := url.Parse(fmt.Sprintf("https://%s/api/v2/%s", domain, strings.Trim(i.RawURI, "/")))
if err != nil {
return fmt.Errorf("invalid uri given: %w", err)
return usageError{err: fmt.Errorf("invalid uri given: %w", err), reason: "invalid_flag_value"}
}

params := endpoint.Query()
Expand All @@ -325,7 +328,7 @@ func (i *apiCmdInputs) validateAndSetEndpoint(domain string) error {
for _, pair := range strings.Split(raw, ",") {
key, value, found := strings.Cut(pair, "=")
if !found {
return fmt.Errorf("invalid query parameter %q: expected key=value", pair)
return usageError{err: fmt.Errorf("invalid query parameter %q: expected key=value", pair), reason: "invalid_flag_value"}
}
// Add (not Set) so a repeated key sends every value instead of the last
// one overwriting the rest.
Expand Down Expand Up @@ -401,11 +404,14 @@ func isInsufficientScopeError(statusCode int, rawBody []byte) error {
}
}

return fmt.Errorf(
"request failed because access token lacks scope: %s.\n "+
"If authenticated via client credentials, add this scope to the designated client. "+
"If authenticated as a user, request this scope during login by running `auth0 login --scopes %s`",
recommendedScopeToAdd,
recommendedScopeToAdd,
)
return authError{
err: fmt.Errorf(
"request failed because access token lacks scope: %s.\n "+
"If authenticated via client credentials, add this scope to the designated client. "+
"If authenticated as a user, request this scope during login by running `auth0 login --scopes %s`",
recommendedScopeToAdd,
recommendedScopeToAdd,
),
reason: "insufficient_scope",
}
}
6 changes: 3 additions & 3 deletions internal/cli/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ func createAppCmd(cli *cli) *cobra.Command {

inputs.ResourceServerIdentifier = selectedAPI.GetIdentifier()
} else if strings.TrimSpace(inputs.ResourceServerIdentifier) == "" {
return fmt.Errorf("resource-server-identifier cannot be empty for resource_server app type")
return usageError{err: fmt.Errorf("resource-server-identifier cannot be empty for resource_server app type"), reason: "missing_required_flags"}
}
}

Expand Down Expand Up @@ -669,7 +669,7 @@ func createAppCmd(cli *cli) *cobra.Command {

if len(inputs.RefreshToken) != 0 {
if err := json.Unmarshal([]byte(inputs.RefreshToken), &a.RefreshToken); err != nil {
return fmt.Errorf("apps: %s refreshToken invalid JSON", err)
return validationError{err: fmt.Errorf("apps: %s refreshToken invalid JSON", err), reason: "malformed_json"}
}
}

Expand Down Expand Up @@ -975,7 +975,7 @@ func updateAppCmd(cli *cli) *cobra.Command {
a.RefreshToken = current.RefreshToken
} else {
if err := json.Unmarshal([]byte(inputs.RefreshToken), &a.RefreshToken); err != nil {
return fmt.Errorf("apps: %s refreshToken invalid JSON", err)
return validationError{err: fmt.Errorf("apps: %s refreshToken invalid JSON", err), reason: "malformed_json"}
}
}

Expand Down
6 changes: 3 additions & 3 deletions internal/cli/arguments.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type pickerOptionsFunc func(ctx context.Context) (pickerOptions, error)

func (a *Argument) Pick(cmd *cobra.Command, result *string, fn pickerOptionsFunc) error {
if !canPrompt(cmd) {
return fmt.Errorf("missing a required argument: %s", a.GetName())
return usageError{err: fmt.Errorf("missing a required argument: %s", a.GetName()), reason: "missing_required_args"}
}

var opts pickerOptions
Expand All @@ -64,7 +64,7 @@ func (a *Argument) Pick(cmd *cobra.Command, result *string, fn pickerOptionsFunc

func (a *Argument) PickMany(cmd *cobra.Command, result *[]string, fn pickerOptionsFunc) error {
if !canPrompt(cmd) {
return fmt.Errorf("missing a required argument: %s", a.GetName())
return usageError{err: fmt.Errorf("missing a required argument: %s", a.GetName()), reason: "missing_required_args"}
}

var opts pickerOptions
Expand Down Expand Up @@ -92,5 +92,5 @@ func askArgument(cmd *cobra.Command, i commandInput, value interface{}) error {
return ask(i, value, nil, false)
}

return fmt.Errorf("missing a required argument: %s", i.GetName())
return usageError{err: fmt.Errorf("missing a required argument: %s", i.GetName()), reason: "missing_required_args"}
}
10 changes: 5 additions & 5 deletions internal/cli/client_grants.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ func listClientGrantsCmd(cli *cli) *cobra.Command {
// The API requires a client_id, audience or default_for filter
// alongside subject_type; catch it early with a clearer message.
if inputs.SubjectType != "" && inputs.ClientID == "" && inputs.Audience == "" && inputs.DefaultFor == "" {
return fmt.Errorf("--subject-type must be combined with --client-id, --audience or --default-for")
return usageError{err: fmt.Errorf("--subject-type must be combined with --client-id, --audience or --default-for"), reason: "incompatible_flags"}
}

request := &managementv3.ListClientGrantsRequestParameters{}
Expand Down Expand Up @@ -352,7 +352,7 @@ func createClientGrantCmd(cli *cli) *cobra.Command {
}

if inputs.ClientID == "" && inputs.DefaultFor == "" {
return errors.New("one of --client-id or --default-for must be set")
return usageError{err: errors.New("one of --client-id or --default-for must be set"), reason: "missing_required_flags"}
}

// A default grant is a template for a group of clients rather than an
Expand All @@ -376,7 +376,7 @@ func createClientGrantCmd(cli *cli) *cobra.Command {
if isDefaultGrant {
for _, f := range []*Flag{&clientGrantSubjectType, &clientGrantOrganizationUsage, &clientGrantAllowAnyOrganization} {
if f.IsSet(cmd) {
return fmt.Errorf("--%s cannot be set with --default-for", f.LongForm)
return usageError{err: fmt.Errorf("--%s cannot be set with --default-for", f.LongForm), reason: "incompatible_flags"}
}
}
}
Expand Down Expand Up @@ -892,7 +892,7 @@ func clientGrantSubjectTypeAllowsOrganizations(subjectType string) bool {
// into a clear, actionable message for the non-interactive path.
func validateClientGrantSubjectType(subjectType, organizationUsage string, allowAnyOrganization bool) error {
if !clientGrantSubjectTypeAllowsOrganizations(subjectType) && (organizationUsage != "" || allowAnyOrganization) {
return fmt.Errorf("--organization-usage and --allow-any-organization cannot be set when --subject-type is %q", subjectType)
return usageError{err: fmt.Errorf("--organization-usage and --allow-any-organization cannot be set when --subject-type is %q", subjectType), reason: "incompatible_flags"}
}
return nil
}
Expand All @@ -909,7 +909,7 @@ func clientGrantOrganizationAllowsAny(organizationUsage string) bool {
// 400 into a clear, actionable message.
func validateClientGrantOrganization(organizationUsage string, allowAnyOrganization bool) error {
if allowAnyOrganization && !clientGrantOrganizationAllowsAny(organizationUsage) {
return errors.New("--allow-any-organization can only be enabled when --organization-usage is 'allow' or 'require'")
return usageError{err: errors.New("--allow-any-organization can only be enabled when --organization-usage is 'allow' or 'require'"), reason: "incompatible_flags"}
}
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/custom_domains.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func listCustomDomainsCmd(cli *cli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
// Validate EA-only flags.
if inputs.sortBy != "" && inputs.sortBy != "domain" {
return fmt.Errorf("sorting is only supported by domain at this time")
return usageError{err: fmt.Errorf("sorting is only supported by domain at this time"), reason: "invalid_flag_value"}
}

var domains []*management.CustomDomain
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/data_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func marshalFieldErrors(fieldErrors []openapi.FieldError) json.RawMessage {
// readJSONInput reads JSON from various input sources.
func (h *DataJSONHandler) readJSONInput(input string) ([]byte, error) {
if input == "" {
return nil, fmt.Errorf("no input provided")
return nil, validationError{err: fmt.Errorf("no input provided"), reason: "missing_input"}
}

// "@-" and "-" read the payload from stdin, the common agent/script idiom for
Expand Down
12 changes: 6 additions & 6 deletions internal/cli/email_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ func createEmailProviderCmd(cli *cli) *cobra.Command {
var credentials map[string]interface{}
if inputs.name == emailProviderCustom {
if len(inputs.credentials) > 0 {
return fmt.Errorf("credentials not supported for provider: %s", inputs.name)
return usageError{err: fmt.Errorf("credentials not supported for provider: %s", inputs.name), reason: "invalid_flag_value"}
}
credentials = make(map[string]interface{})
} else {
Expand Down Expand Up @@ -205,10 +205,10 @@ func createEmailProviderCmd(cli *cli) *cobra.Command {
emailProviderMS365,
emailProviderCustom:
if len(inputs.settings) > 0 {
return fmt.Errorf("settings not supported for provider: %s", inputs.name)
return usageError{err: fmt.Errorf("settings not supported for provider: %s", inputs.name), reason: "invalid_flag_value"}
}
default:
return fmt.Errorf("unknown provider: %s", inputs.name)
return usageError{err: fmt.Errorf("unknown provider: %s", inputs.name), reason: "invalid_flag_value"}
}

emailProvider := &management.EmailProvider{
Expand Down Expand Up @@ -319,7 +319,7 @@ func updateEmailProviderCmd(cli *cli) *cobra.Command {
// If we are changing providers, we need new credentials and settings.
if inputs.name == emailProviderCustom {
if len(inputs.credentials) > 0 {
return fmt.Errorf("credentials not supported for provider: %s", inputs.name)
return usageError{err: fmt.Errorf("credentials not supported for provider: %s", inputs.name), reason: "invalid_flag_value"}
}
credentials = make(map[string]interface{})
} else {
Expand Down Expand Up @@ -348,10 +348,10 @@ func updateEmailProviderCmd(cli *cli) *cobra.Command {
emailProviderMS365,
emailProviderCustom:
if len(inputs.settings) > 0 {
return fmt.Errorf("settings not supported for provider: %s", inputs.name)
return usageError{err: fmt.Errorf("settings not supported for provider: %s", inputs.name), reason: "invalid_flag_value"}
}
default:
return fmt.Errorf("unknown provider: %s", inputs.name)
return usageError{err: fmt.Errorf("unknown provider: %s", inputs.name), reason: "invalid_flag_value"}
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/cli/event_streams.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ func createEventStreamCmd(cli *cli) *cobra.Command {
}

if len(inputs.Configuration) == 0 {
return fmt.Errorf("must provider configuration for event stream")
return usageError{err: fmt.Errorf("must provider configuration for event stream"), reason: "missing_required_flags"}
}

var subscriptions []management.EventStreamSubscription
Expand Down
Loading
Loading