From 2af70c77957a6444a68101f9b05e807947320aee Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Fri, 23 Oct 2026 10:45:55 +1000 Subject: [PATCH 1/4] feat(cli): add configure --clear and name the OS credential store in whoami help --- .changesets/1792716255-70167bd5.yaml | 12 +++ templates/templates/cli/auth.go.stmpl | 4 +- .../internal/config/keyring.go.stmpl | 13 +++ templates/templates/cli/configure.go.stmpl | 9 ++- templates/templates/cli/includes/security.ts | 66 ++++++++++++++++ templates/templates/cli/whoami.go.stmpl | 4 +- zSDKs/sdk-cli/.speakeasy/gen.lock | 8 +- zSDKs/sdk-cli/docs/cli_auth_whoami.md | 2 +- zSDKs/sdk-cli/docs/cli_configure.md | 1 + zSDKs/sdk-cli/docs/cli_whoami.md | 2 +- zSDKs/sdk-cli/internal/cli/auth.go | 2 +- zSDKs/sdk-cli/internal/cli/configure.go | 79 ++++++++++++++++++- zSDKs/sdk-cli/internal/cli/whoami.go | 2 +- zSDKs/sdk-cli/internal/config/keyring.go | 13 +++ 14 files changed, 204 insertions(+), 13 deletions(-) create mode 100644 .changesets/1792716255-70167bd5.yaml diff --git a/.changesets/1792716255-70167bd5.yaml b/.changesets/1792716255-70167bd5.yaml new file mode 100644 index 000000000..484bc1d07 --- /dev/null +++ b/.changesets/1792716255-70167bd5.yaml @@ -0,0 +1,12 @@ +id: 1792716255-70167bd5 +features: + - core + - globals + - globalSecurity +targets: + - cli +type: feat +bump: patch +description: add configure --clear to remove stored credentials, global parameters and the default output format, and name the OS credential store in whoami help +author: TristanSpeakEasy +date: "2026-10-23" diff --git a/templates/templates/cli/auth.go.stmpl b/templates/templates/cli/auth.go.stmpl index 00a3f509e..5714c6dd3 100644 --- a/templates/templates/cli/auth.go.stmpl +++ b/templates/templates/cli/auth.go.stmpl @@ -66,7 +66,9 @@ Subcommands: Sources are shown as: [flag] - Set via command line flag [env] - Set via environment variable ({{.Global.Config.EnvVarPrefix}}_*) - [keyring] - Set via OS keychain (stored by login/configure command) +{{- if hasGlobalSecurity}} + [keyring] - Set via ` + config.KeychainName() + ` (stored by login/configure command) +{{- end}} [config] - Set via config file (~/.config/{{sanitizeCliName}}/config.yaml) [unset] - Not configured {{- if hasGlobalSecurity}} diff --git a/templates/templates/cli/auxiliary/internal/config/keyring.go.stmpl b/templates/templates/cli/auxiliary/internal/config/keyring.go.stmpl index 13b223652..449e56f22 100644 --- a/templates/templates/cli/auxiliary/internal/config/keyring.go.stmpl +++ b/templates/templates/cli/auxiliary/internal/config/keyring.go.stmpl @@ -7,6 +7,7 @@ import ( "fmt" "io" "os" + "runtime" "sync" "sync/atomic" "time" @@ -170,6 +171,18 @@ func DeleteKeyringValue(key string) error { return withKeyringTimeout(func() error { return backend.Delete(cliName, key) }) } +// KeychainName is the user-facing name of the OS credential store. +func KeychainName() string { + switch runtime.GOOS { + case "darwin": + return "macOS Keychain" + case "windows": + return "Windows Credential Manager" + default: + return "Secret Service keyring" + } +} + // KeyringAvailable returns whether the OS keychain is accessible. // Useful for configure command to decide where to store secrets. func KeyringAvailable() bool { diff --git a/templates/templates/cli/configure.go.stmpl b/templates/templates/cli/configure.go.stmpl index 2c8cffee7..c5bc9654b 100644 --- a/templates/templates/cli/configure.go.stmpl +++ b/templates/templates/cli/configure.go.stmpl @@ -40,6 +40,7 @@ func initConfigureCmd(parent *cobra.Command) error { {{- end}} {{- if isAnyInteractiveEnabled}} cmd.Flags().String("{{defaultOutputFormatFlag}}", "", "Store the default output format without opening the form. Options: "+strings.Join(output.Formats, ", ")+". Pass an empty value to clear it.") + cmd.Flags().StringSlice("{{configureClearFlag}}", nil, "Remove stored values without opening the form (repeatable or comma-separated). Options: {{templateConfigureClearOptions}}") {{- end}} parent.AddCommand(cmd) return nil @@ -63,8 +64,9 @@ func runConfigureCmd(cmd *cobra.Command, args []string) error { {{- end}} {{- if isAnyInteractiveEnabled}} + var cleared []string formMode := interactive.Resolve(cmd).SetupFormMode(flagutil.AnyFlagChanged(cmd, {{templateConfigureFlagNames}})) - if cmd.Flags().Changed("{{defaultOutputFormatFlag}}") { + if cmd.Flags().Changed("{{defaultOutputFormatFlag}}") || cmd.Flags().Changed("{{configureClearFlag}}") { formMode = interactive.FormOff } {{- end}} @@ -175,6 +177,11 @@ func runConfigureCmd(cmd *cobra.Command, args []string) error { fmt.Fprintln(out, "Secret credentials stored in OS keychain") } {{- end}} + {{- if isAnyInteractiveEnabled}} + for _, name := range cleared { + fmt.Fprintf(out, "Cleared --%s\n", name) + } + {{- end}} fmt.Fprintf(out, "Configuration saved to %s\n", config.GetConfigPath()) return nil } diff --git a/templates/templates/cli/includes/security.ts b/templates/templates/cli/includes/security.ts index 51aed2638..2a1751d87 100644 --- a/templates/templates/cli/includes/security.ts +++ b/templates/templates/cli/includes/security.ts @@ -2721,6 +2721,8 @@ function templateConfigureNonInteractiveStore(): string { } } + lines.push(...genConfigureClear()); + lines.push(``); lines.push(`if !changed {`); lines.push( @@ -2735,6 +2737,69 @@ registerTemplateFunc( templateConfigureNonInteractiveStore, ); +const CONFIGURE_CLEAR_FLAG = "clear"; + +function configureClearableNames(): string[] { + return [...storedFlagNames("configure"), DEFAULT_OUTPUT_FORMAT_FLAG]; +} + +function genConfigureClear(): string[] { + const options = configureClearableNames().join(", "); + const lines: string[] = []; + lines.push( + `if f := cmd.Flags().Lookup("${CONFIGURE_CLEAR_FLAG}"); f != nil && f.Changed {`, + ); + lines.push( + ` names, _ := cmd.Flags().GetStringSlice("${CONFIGURE_CLEAR_FLAG}")`, + ); + lines.push(` for _, name := range names {`); + lines.push( + ` switch key := strings.TrimPrefix(strings.TrimSpace(name), "--"); key {`, + ); + + const secAcc = securityConfigAccessor(); + for (const field of getCLISecurityFields()) { + const cfgField = `${secAcc}${caser().ToPascal(field.field.Name)}`; + lines.push(` case "${field.flagName}":`); + if (field.isSecret) { + lines.push(` if config.KeyringAvailable() {`); + lines.push( + ` _ = config.DeleteKeyringValue("${field.flagName}")`, + ); + lines.push(` }`); + } + lines.push(` ${cfgField} = ${field.isArray ? "nil" : `""`}`); + } + if (hasGlobals()) { + const globAcc = globalsConfigAccessor(); + for (const field of context.Global.AST.MainSDK.Globals.Fields) { + lines.push(` case "${sanitizeFlagNameWithReserved(field.Name)}":`); + lines.push(` ${globAcc}${caser().ToPascal(field.Name)} = ""`); + } + } + lines.push(` case "${DEFAULT_OUTPUT_FORMAT_FLAG}":`); + lines.push(` cfg.OutputFormat = ""`); + lines.push(` default:`); + lines.push( + ` return flagutil.WithCLIValidation(fmt.Errorf("invalid --${CONFIGURE_CLEAR_FLAG} %q; options: ${escapeGoString( + options, + )}", name))`, + ); + lines.push(` }`); + lines.push( + ` cleared = append(cleared, strings.TrimPrefix(strings.TrimSpace(name), "--"))`, + ); + lines.push(` }`); + lines.push(` changed = true`); + lines.push(`}`); + return lines; +} + +registerTemplateFunc("configureClearFlag", () => CONFIGURE_CLEAR_FLAG); +registerTemplateFunc("templateConfigureClearOptions", () => + escapeGoString(configureClearableNames().join(", ")), +); + registerTemplateFunc( "defaultOutputFormatFlag", () => DEFAULT_OUTPUT_FORMAT_FLAG, @@ -2763,6 +2828,7 @@ function templateConfigureFlagNames(): string { return goFlagNameArgs([ ...storedFlagNames("configure"), DEFAULT_OUTPUT_FORMAT_FLAG, + CONFIGURE_CLEAR_FLAG, ]); } registerTemplateFunc("templateConfigureFlagNames", templateConfigureFlagNames); diff --git a/templates/templates/cli/whoami.go.stmpl b/templates/templates/cli/whoami.go.stmpl index 3674e0c82..1e3b80108 100644 --- a/templates/templates/cli/whoami.go.stmpl +++ b/templates/templates/cli/whoami.go.stmpl @@ -18,7 +18,9 @@ func initWhoamiCmd(parent *cobra.Command) error { Sources are shown as: [flag] - Set via command line flag [env] - Set via environment variable ({{.Global.Config.EnvVarPrefix}}_*) - [keyring] - Set via OS keychain (stored by configure command) +{{- if hasGlobalSecurity}} + [keyring] - Set via ` + config.KeychainName() + ` (stored by configure command) +{{- end}} [config] - Set via config file (~/.config/{{sanitizeCliName}}/config.yaml) [default] - Built-in global parameter flag default [unset] - Not configured diff --git a/zSDKs/sdk-cli/.speakeasy/gen.lock b/zSDKs/sdk-cli/.speakeasy/gen.lock index 5b420f96d..a41727f61 100644 --- a/zSDKs/sdk-cli/.speakeasy/gen.lock +++ b/zSDKs/sdk-cli/.speakeasy/gen.lock @@ -60,13 +60,13 @@ trackedFiles: go.mod: last_write_checksum: sha1:15ced474cbb1ec803f17960c0b73f10ea969c22a internal/cli/auth.go: - last_write_checksum: sha1:5120764443e6565b7a63c06ad91d6323806cdfea + last_write_checksum: sha1:288aa0138da525e34004083060372df173caeefb internal/cli/binaryandstringupload.go: last_write_checksum: sha1:20d8028b7618e73dcf1763372f9145ea325b32b0 internal/cli/chat.go: last_write_checksum: sha1:9a9077f9bd0eb3ef19d36d461243215c50f3442b internal/cli/configure.go: - last_write_checksum: sha1:91f681e0c0bb3ec3fccfe48939b8bb0b71d89619 + last_write_checksum: sha1:ee8e247c4e7da6ebbf9e20bc80bee879f3bc176a internal/cli/createuser.go: last_write_checksum: sha1:5b60000059db85dcf4c90f5e67c37b7fff3cf852 internal/cli/createwithunion.go: @@ -222,7 +222,7 @@ trackedFiles: internal/cli/version.go: last_write_checksum: sha1:ef4d924ad83b7b2e9e4698a9f0ac229bac4060d4 internal/cli/whoami.go: - last_write_checksum: sha1:ad27d6ccfed17dd213951f981030b010ababbf79 + last_write_checksum: sha1:5bc23866496c911a13dfc746f2d0acb65406dd2a internal/client/client.go: last_write_checksum: sha1:58db154dfc3b80e998471223136423d3ffc1ca92 internal/client/diagnostics.go: @@ -232,7 +232,7 @@ trackedFiles: internal/config/config.go: last_write_checksum: sha1:7d3e1555d3be75dcd615fb460f44bf1a4da784dd internal/config/keyring.go: - last_write_checksum: sha1:7c0201503038fee2f5eac51bc0f0189e06d3a224 + last_write_checksum: sha1:31bbb463ef1449d320b2fe9931bee698fb71021e internal/explorer/explorer.go: last_write_checksum: sha1:c149087aa4be52cdf0b3f9a41d8f0c6dbe47c53f internal/explorer/styles.go: diff --git a/zSDKs/sdk-cli/docs/cli_auth_whoami.md b/zSDKs/sdk-cli/docs/cli_auth_whoami.md index af9050168..0f27475b0 100644 --- a/zSDKs/sdk-cli/docs/cli_auth_whoami.md +++ b/zSDKs/sdk-cli/docs/cli_auth_whoami.md @@ -9,7 +9,7 @@ Display the currently configured settings and their sources. Sources are shown as: [flag] - Set via command line flag [env] - Set via environment variable (CLI_*) - [keyring] - Set via OS keychain (stored by login/configure command) + [keyring] - Set via macOS Keychain (stored by login/configure command) [config] - Set via config file (~/.config/cli/config.yaml) [unset] - Not configured diff --git a/zSDKs/sdk-cli/docs/cli_configure.md b/zSDKs/sdk-cli/docs/cli_configure.md index 8b1f92c8d..654311c6a 100644 --- a/zSDKs/sdk-cli/docs/cli_configure.md +++ b/zSDKs/sdk-cli/docs/cli_configure.md @@ -20,6 +20,7 @@ cli configure [flags] ### Options ``` + --clear strings Remove stored values without opening the form (repeatable or comma-separated). Options: username, password, bearer-auth, my-api-key, oauth2, app-id, secret, mobile-auth, client-id, client-secret, token-url, query-param1, deprecated-query-param1, deprecated-query-param2, lone-query-param, default-output-format --default-output-format string Store the default output format without opening the form. Options: pretty, json, yaml, table, toon. Pass an empty value to clear it. -h, --help help for configure ``` diff --git a/zSDKs/sdk-cli/docs/cli_whoami.md b/zSDKs/sdk-cli/docs/cli_whoami.md index acce7f0ab..06d6ce458 100644 --- a/zSDKs/sdk-cli/docs/cli_whoami.md +++ b/zSDKs/sdk-cli/docs/cli_whoami.md @@ -9,7 +9,7 @@ Display the currently configured settings and their sources. Sources are shown as: [flag] - Set via command line flag [env] - Set via environment variable (CLI_*) - [keyring] - Set via OS keychain (stored by configure command) + [keyring] - Set via macOS Keychain (stored by configure command) [config] - Set via config file (~/.config/cli/config.yaml) [default] - Built-in global parameter flag default [unset] - Not configured diff --git a/zSDKs/sdk-cli/internal/cli/auth.go b/zSDKs/sdk-cli/internal/cli/auth.go index 13d4974d7..68e8c5135 100644 --- a/zSDKs/sdk-cli/internal/cli/auth.go +++ b/zSDKs/sdk-cli/internal/cli/auth.go @@ -68,7 +68,7 @@ Use the configure command for both authentication and global parameters.`, Sources are shown as: [flag] - Set via command line flag [env] - Set via environment variable (CLI_*) - [keyring] - Set via OS keychain (stored by login/configure command) + [keyring] - Set via ` + config.KeychainName() + ` (stored by login/configure command) [config] - Set via config file (~/.config/cli/config.yaml) [unset] - Not configured diff --git a/zSDKs/sdk-cli/internal/cli/configure.go b/zSDKs/sdk-cli/internal/cli/configure.go index 99a8b5daf..f7ca06782 100644 --- a/zSDKs/sdk-cli/internal/cli/configure.go +++ b/zSDKs/sdk-cli/internal/cli/configure.go @@ -35,6 +35,7 @@ Priority: CLI flags > environment variables > OS keychain > config file`, } cmd.Annotations = map[string]string{"speakeasy_settings_command": "true"} cmd.Flags().String("default-output-format", "", "Store the default output format without opening the form. Options: "+strings.Join(output.Formats, ", ")+". Pass an empty value to clear it.") + cmd.Flags().StringSlice("clear", nil, "Remove stored values without opening the form (repeatable or comma-separated). Options: username, password, bearer-auth, my-api-key, oauth2, app-id, secret, mobile-auth, client-id, client-secret, token-url, query-param1, deprecated-query-param1, deprecated-query-param2, lone-query-param, default-output-format") parent.AddCommand(cmd) return nil } @@ -54,8 +55,9 @@ func runConfigureCmd(cmd *cobra.Command, args []string) error { keychainStored := false - formMode := interactive.Resolve(cmd).SetupFormMode(flagutil.AnyFlagChanged(cmd, "username", "password", "bearer-auth", "my-api-key", "oauth2", "app-id", "secret", "mobile-auth", "client-id", "client-secret", "token-url", "query-param1", "deprecated-query-param1", "deprecated-query-param2", "lone-query-param", "default-output-format")) - if cmd.Flags().Changed("default-output-format") { + var cleared []string + formMode := interactive.Resolve(cmd).SetupFormMode(flagutil.AnyFlagChanged(cmd, "username", "password", "bearer-auth", "my-api-key", "oauth2", "app-id", "secret", "mobile-auth", "client-id", "client-secret", "token-url", "query-param1", "deprecated-query-param1", "deprecated-query-param2", "lone-query-param", "default-output-format", "clear")) + if cmd.Flags().Changed("default-output-format") || cmd.Flags().Changed("clear") { formMode = interactive.FormOff } if formMode == interactive.FormOff { @@ -160,6 +162,76 @@ func runConfigureCmd(cmd *cobra.Command, args []string) error { cfg.Globals.LoneQueryParam = f.Value.String() changed = true } + if f := cmd.Flags().Lookup("clear"); f != nil && f.Changed { + names, _ := cmd.Flags().GetStringSlice("clear") + for _, name := range names { + switch key := strings.TrimPrefix(strings.TrimSpace(name), "--"); key { + case "username": + cfg.Security.Username = "" + case "password": + if config.KeyringAvailable() { + _ = config.DeleteKeyringValue("password") + } + cfg.Security.Password = "" + case "bearer-auth": + if config.KeyringAvailable() { + _ = config.DeleteKeyringValue("bearer-auth") + } + cfg.Security.BearerAuth = "" + case "my-api-key": + if config.KeyringAvailable() { + _ = config.DeleteKeyringValue("my-api-key") + } + cfg.Security.MyApiKey = "" + case "oauth2": + if config.KeyringAvailable() { + _ = config.DeleteKeyringValue("oauth2") + } + cfg.Security.Oauth2 = "" + case "app-id": + if config.KeyringAvailable() { + _ = config.DeleteKeyringValue("app-id") + } + cfg.Security.AppId = "" + case "secret": + if config.KeyringAvailable() { + _ = config.DeleteKeyringValue("secret") + } + cfg.Security.Secret = "" + case "mobile-auth": + if config.KeyringAvailable() { + _ = config.DeleteKeyringValue("mobile-auth") + } + cfg.Security.MobileAuth = "" + case "client-id": + if config.KeyringAvailable() { + _ = config.DeleteKeyringValue("client-id") + } + cfg.Security.ClientID = "" + case "client-secret": + if config.KeyringAvailable() { + _ = config.DeleteKeyringValue("client-secret") + } + cfg.Security.ClientSecret = "" + case "token-url": + cfg.Security.TokenURL = "" + case "query-param1": + cfg.Globals.QueryParam1 = "" + case "deprecated-query-param1": + cfg.Globals.DeprecatedQueryParam1 = "" + case "deprecated-query-param2": + cfg.Globals.DeprecatedQueryParam2 = "" + case "lone-query-param": + cfg.Globals.LoneQueryParam = "" + case "default-output-format": + cfg.OutputFormat = "" + default: + return flagutil.WithCLIValidation(fmt.Errorf("invalid --clear %q; options: username, password, bearer-auth, my-api-key, oauth2, app-id, secret, mobile-auth, client-id, client-secret, token-url, query-param1, deprecated-query-param1, deprecated-query-param2, lone-query-param, default-output-format", name)) + } + cleared = append(cleared, strings.TrimPrefix(strings.TrimSpace(name), "--")) + } + changed = true + } if !changed { return flagutil.WithCLIValidation(fmt.Errorf("no flags provided; use flags to store values in %s, or pass --interactive to open the form", config.GetConfigPath())) @@ -378,6 +450,9 @@ func runConfigureCmd(cmd *cobra.Command, args []string) error { if keychainStored { fmt.Fprintln(out, "Secret credentials stored in OS keychain") } + for _, name := range cleared { + fmt.Fprintf(out, "Cleared --%s\n", name) + } fmt.Fprintf(out, "Configuration saved to %s\n", config.GetConfigPath()) return nil } diff --git a/zSDKs/sdk-cli/internal/cli/whoami.go b/zSDKs/sdk-cli/internal/cli/whoami.go index 7a634a739..f7267ff59 100644 --- a/zSDKs/sdk-cli/internal/cli/whoami.go +++ b/zSDKs/sdk-cli/internal/cli/whoami.go @@ -20,7 +20,7 @@ func initWhoamiCmd(parent *cobra.Command) error { Sources are shown as: [flag] - Set via command line flag [env] - Set via environment variable (CLI_*) - [keyring] - Set via OS keychain (stored by configure command) + [keyring] - Set via ` + config.KeychainName() + ` (stored by configure command) [config] - Set via config file (~/.config/cli/config.yaml) [default] - Built-in global parameter flag default [unset] - Not configured diff --git a/zSDKs/sdk-cli/internal/config/keyring.go b/zSDKs/sdk-cli/internal/config/keyring.go index 2c632fa8d..67f9e75f3 100644 --- a/zSDKs/sdk-cli/internal/config/keyring.go +++ b/zSDKs/sdk-cli/internal/config/keyring.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "os" + "runtime" "sync" "sync/atomic" "time" @@ -170,6 +171,18 @@ func DeleteKeyringValue(key string) error { return withKeyringTimeout(func() error { return backend.Delete(cliName, key) }) } +// KeychainName is the user-facing name of the OS credential store. +func KeychainName() string { + switch runtime.GOOS { + case "darwin": + return "macOS Keychain" + case "windows": + return "Windows Credential Manager" + default: + return "Secret Service keyring" + } +} + // KeyringAvailable returns whether the OS keychain is accessible. // Useful for configure command to decide where to store secrets. func KeyringAvailable() bool { From 1d16e52986db728203d4b679b19668653bc8d61f Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Fri, 23 Oct 2026 11:23:18 +1000 Subject: [PATCH 2/4] fix(cli): keep whoami help text the same on every platform --- .changesets/1792716255-70167bd5.yaml | 2 +- templates/templates/cli/auth.go.stmpl | 2 +- .../cli/auxiliary/internal/config/keyring.go.stmpl | 13 ------------- templates/templates/cli/whoami.go.stmpl | 2 +- zSDKs/sdk-cli/.speakeasy/gen.lock | 6 +++--- zSDKs/sdk-cli/docs/cli_auth_whoami.md | 2 +- zSDKs/sdk-cli/docs/cli_whoami.md | 2 +- zSDKs/sdk-cli/internal/cli/auth.go | 2 +- zSDKs/sdk-cli/internal/cli/whoami.go | 2 +- zSDKs/sdk-cli/internal/config/keyring.go | 13 ------------- 10 files changed, 10 insertions(+), 36 deletions(-) diff --git a/.changesets/1792716255-70167bd5.yaml b/.changesets/1792716255-70167bd5.yaml index 484bc1d07..b09b158df 100644 --- a/.changesets/1792716255-70167bd5.yaml +++ b/.changesets/1792716255-70167bd5.yaml @@ -7,6 +7,6 @@ targets: - cli type: feat bump: patch -description: add configure --clear to remove stored credentials, global parameters and the default output format, and name the OS credential store in whoami help +description: add configure --clear to remove stored credentials, global parameters and the default output format, and name the supported OS credential stores in whoami help author: TristanSpeakEasy date: "2026-10-23" diff --git a/templates/templates/cli/auth.go.stmpl b/templates/templates/cli/auth.go.stmpl index 5714c6dd3..0fb46c2af 100644 --- a/templates/templates/cli/auth.go.stmpl +++ b/templates/templates/cli/auth.go.stmpl @@ -67,7 +67,7 @@ Sources are shown as: [flag] - Set via command line flag [env] - Set via environment variable ({{.Global.Config.EnvVarPrefix}}_*) {{- if hasGlobalSecurity}} - [keyring] - Set via ` + config.KeychainName() + ` (stored by login/configure command) + [keyring] - Set via OS credential store: macOS Keychain, Windows Credential Manager or Secret Service (stored by login/configure command) {{- end}} [config] - Set via config file (~/.config/{{sanitizeCliName}}/config.yaml) [unset] - Not configured diff --git a/templates/templates/cli/auxiliary/internal/config/keyring.go.stmpl b/templates/templates/cli/auxiliary/internal/config/keyring.go.stmpl index 449e56f22..13b223652 100644 --- a/templates/templates/cli/auxiliary/internal/config/keyring.go.stmpl +++ b/templates/templates/cli/auxiliary/internal/config/keyring.go.stmpl @@ -7,7 +7,6 @@ import ( "fmt" "io" "os" - "runtime" "sync" "sync/atomic" "time" @@ -171,18 +170,6 @@ func DeleteKeyringValue(key string) error { return withKeyringTimeout(func() error { return backend.Delete(cliName, key) }) } -// KeychainName is the user-facing name of the OS credential store. -func KeychainName() string { - switch runtime.GOOS { - case "darwin": - return "macOS Keychain" - case "windows": - return "Windows Credential Manager" - default: - return "Secret Service keyring" - } -} - // KeyringAvailable returns whether the OS keychain is accessible. // Useful for configure command to decide where to store secrets. func KeyringAvailable() bool { diff --git a/templates/templates/cli/whoami.go.stmpl b/templates/templates/cli/whoami.go.stmpl index 1e3b80108..0fb4f9020 100644 --- a/templates/templates/cli/whoami.go.stmpl +++ b/templates/templates/cli/whoami.go.stmpl @@ -19,7 +19,7 @@ Sources are shown as: [flag] - Set via command line flag [env] - Set via environment variable ({{.Global.Config.EnvVarPrefix}}_*) {{- if hasGlobalSecurity}} - [keyring] - Set via ` + config.KeychainName() + ` (stored by configure command) + [keyring] - Set via OS credential store: macOS Keychain, Windows Credential Manager or Secret Service (stored by configure command) {{- end}} [config] - Set via config file (~/.config/{{sanitizeCliName}}/config.yaml) [default] - Built-in global parameter flag default diff --git a/zSDKs/sdk-cli/.speakeasy/gen.lock b/zSDKs/sdk-cli/.speakeasy/gen.lock index a41727f61..b106eb58b 100644 --- a/zSDKs/sdk-cli/.speakeasy/gen.lock +++ b/zSDKs/sdk-cli/.speakeasy/gen.lock @@ -60,7 +60,7 @@ trackedFiles: go.mod: last_write_checksum: sha1:15ced474cbb1ec803f17960c0b73f10ea969c22a internal/cli/auth.go: - last_write_checksum: sha1:288aa0138da525e34004083060372df173caeefb + last_write_checksum: sha1:8c70569b35b5583ca7e8c23402a9e164d98f1d1b internal/cli/binaryandstringupload.go: last_write_checksum: sha1:20d8028b7618e73dcf1763372f9145ea325b32b0 internal/cli/chat.go: @@ -222,7 +222,7 @@ trackedFiles: internal/cli/version.go: last_write_checksum: sha1:ef4d924ad83b7b2e9e4698a9f0ac229bac4060d4 internal/cli/whoami.go: - last_write_checksum: sha1:5bc23866496c911a13dfc746f2d0acb65406dd2a + last_write_checksum: sha1:dbbfb30c1445584c4365afa9a81354a6721d8a50 internal/client/client.go: last_write_checksum: sha1:58db154dfc3b80e998471223136423d3ffc1ca92 internal/client/diagnostics.go: @@ -232,7 +232,7 @@ trackedFiles: internal/config/config.go: last_write_checksum: sha1:7d3e1555d3be75dcd615fb460f44bf1a4da784dd internal/config/keyring.go: - last_write_checksum: sha1:31bbb463ef1449d320b2fe9931bee698fb71021e + last_write_checksum: sha1:7c0201503038fee2f5eac51bc0f0189e06d3a224 internal/explorer/explorer.go: last_write_checksum: sha1:c149087aa4be52cdf0b3f9a41d8f0c6dbe47c53f internal/explorer/styles.go: diff --git a/zSDKs/sdk-cli/docs/cli_auth_whoami.md b/zSDKs/sdk-cli/docs/cli_auth_whoami.md index 0f27475b0..5867210b1 100644 --- a/zSDKs/sdk-cli/docs/cli_auth_whoami.md +++ b/zSDKs/sdk-cli/docs/cli_auth_whoami.md @@ -9,7 +9,7 @@ Display the currently configured settings and their sources. Sources are shown as: [flag] - Set via command line flag [env] - Set via environment variable (CLI_*) - [keyring] - Set via macOS Keychain (stored by login/configure command) + [keyring] - Set via OS credential store: macOS Keychain, Windows Credential Manager or Secret Service (stored by login/configure command) [config] - Set via config file (~/.config/cli/config.yaml) [unset] - Not configured diff --git a/zSDKs/sdk-cli/docs/cli_whoami.md b/zSDKs/sdk-cli/docs/cli_whoami.md index 06d6ce458..448753e7c 100644 --- a/zSDKs/sdk-cli/docs/cli_whoami.md +++ b/zSDKs/sdk-cli/docs/cli_whoami.md @@ -9,7 +9,7 @@ Display the currently configured settings and their sources. Sources are shown as: [flag] - Set via command line flag [env] - Set via environment variable (CLI_*) - [keyring] - Set via macOS Keychain (stored by configure command) + [keyring] - Set via OS credential store: macOS Keychain, Windows Credential Manager or Secret Service (stored by configure command) [config] - Set via config file (~/.config/cli/config.yaml) [default] - Built-in global parameter flag default [unset] - Not configured diff --git a/zSDKs/sdk-cli/internal/cli/auth.go b/zSDKs/sdk-cli/internal/cli/auth.go index 68e8c5135..7e13cc6f2 100644 --- a/zSDKs/sdk-cli/internal/cli/auth.go +++ b/zSDKs/sdk-cli/internal/cli/auth.go @@ -68,7 +68,7 @@ Use the configure command for both authentication and global parameters.`, Sources are shown as: [flag] - Set via command line flag [env] - Set via environment variable (CLI_*) - [keyring] - Set via ` + config.KeychainName() + ` (stored by login/configure command) + [keyring] - Set via OS credential store: macOS Keychain, Windows Credential Manager or Secret Service (stored by login/configure command) [config] - Set via config file (~/.config/cli/config.yaml) [unset] - Not configured diff --git a/zSDKs/sdk-cli/internal/cli/whoami.go b/zSDKs/sdk-cli/internal/cli/whoami.go index f7267ff59..54c8b5cac 100644 --- a/zSDKs/sdk-cli/internal/cli/whoami.go +++ b/zSDKs/sdk-cli/internal/cli/whoami.go @@ -20,7 +20,7 @@ func initWhoamiCmd(parent *cobra.Command) error { Sources are shown as: [flag] - Set via command line flag [env] - Set via environment variable (CLI_*) - [keyring] - Set via ` + config.KeychainName() + ` (stored by configure command) + [keyring] - Set via OS credential store: macOS Keychain, Windows Credential Manager or Secret Service (stored by configure command) [config] - Set via config file (~/.config/cli/config.yaml) [default] - Built-in global parameter flag default [unset] - Not configured diff --git a/zSDKs/sdk-cli/internal/config/keyring.go b/zSDKs/sdk-cli/internal/config/keyring.go index 67f9e75f3..2c632fa8d 100644 --- a/zSDKs/sdk-cli/internal/config/keyring.go +++ b/zSDKs/sdk-cli/internal/config/keyring.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "os" - "runtime" "sync" "sync/atomic" "time" @@ -171,18 +170,6 @@ func DeleteKeyringValue(key string) error { return withKeyringTimeout(func() error { return backend.Delete(cliName, key) }) } -// KeychainName is the user-facing name of the OS credential store. -func KeychainName() string { - switch runtime.GOOS { - case "darwin": - return "macOS Keychain" - case "windows": - return "Windows Credential Manager" - default: - return "Secret Service keyring" - } -} - // KeyringAvailable returns whether the OS keychain is accessible. // Useful for configure command to decide where to store secrets. func KeyringAvailable() bool { From de40bb7b479ac0ab4a6e7f0f905ab50b573eb55a Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Fri, 23 Oct 2026 11:59:26 +1000 Subject: [PATCH 3/4] fix(cli): validate every configure --clear name first and report keychain delete failures --- .../internal/config/keyring.go.stmpl | 12 +++++ templates/templates/cli/includes/security.ts | 32 ++++++----- .../cli/tests/primary/configure_test.go.stmpl | 54 +++++++++++++++++++ zSDKs/sdk-cli/.speakeasy/gen.lock | 4 +- zSDKs/sdk-cli/internal/cli/configure.go | 49 +++++++++-------- zSDKs/sdk-cli/internal/config/keyring.go | 12 +++++ 6 files changed, 128 insertions(+), 35 deletions(-) diff --git a/templates/templates/cli/auxiliary/internal/config/keyring.go.stmpl b/templates/templates/cli/auxiliary/internal/config/keyring.go.stmpl index 13b223652..ec42a4516 100644 --- a/templates/templates/cli/auxiliary/internal/config/keyring.go.stmpl +++ b/templates/templates/cli/auxiliary/internal/config/keyring.go.stmpl @@ -170,6 +170,18 @@ func DeleteKeyringValue(key string) error { return withKeyringTimeout(func() error { return backend.Delete(cliName, key) }) } +// DeleteStoredSecret removes a credential from the OS keychain when it is +// available. A credential that is not stored there is not an error. +func DeleteStoredSecret(key string) error { + if !KeyringAvailable() { + return nil + } + if err := DeleteKeyringValue(key); err != nil && !errors.Is(err, keyring.ErrNotFound) { + return err + } + return nil +} + // KeyringAvailable returns whether the OS keychain is accessible. // Useful for configure command to decide where to store secrets. func KeyringAvailable() bool { diff --git a/templates/templates/cli/includes/security.ts b/templates/templates/cli/includes/security.ts index 2a1751d87..b250ed4d5 100644 --- a/templates/templates/cli/includes/security.ts +++ b/templates/templates/cli/includes/security.ts @@ -2744,7 +2744,8 @@ function configureClearableNames(): string[] { } function genConfigureClear(): string[] { - const options = configureClearableNames().join(", "); + const clearable = configureClearableNames(); + const options = clearable.join(", "); const lines: string[] = []; lines.push( `if f := cmd.Flags().Lookup("${CONFIGURE_CLEAR_FLAG}"); f != nil && f.Changed {`, @@ -2752,19 +2753,34 @@ function genConfigureClear(): string[] { lines.push( ` names, _ := cmd.Flags().GetStringSlice("${CONFIGURE_CLEAR_FLAG}")`, ); + lines.push(` keys := make([]string, 0, len(names))`); lines.push(` for _, name := range names {`); lines.push( ` switch key := strings.TrimPrefix(strings.TrimSpace(name), "--"); key {`, ); + lines.push(` case ${clearable.map((n) => `"${n}"`).join(", ")}:`); + lines.push(` keys = append(keys, key)`); + lines.push(` default:`); + lines.push( + ` return flagutil.WithCLIValidation(fmt.Errorf("invalid --${CONFIGURE_CLEAR_FLAG} %q; options: ${escapeGoString( + options, + )}", name))`, + ); + lines.push(` }`); + lines.push(` }`); + lines.push(` for _, key := range keys {`); + lines.push(` switch key {`); const secAcc = securityConfigAccessor(); for (const field of getCLISecurityFields()) { const cfgField = `${secAcc}${caser().ToPascal(field.field.Name)}`; lines.push(` case "${field.flagName}":`); if (field.isSecret) { - lines.push(` if config.KeyringAvailable() {`); lines.push( - ` _ = config.DeleteKeyringValue("${field.flagName}")`, + ` if err := config.DeleteStoredSecret("${field.flagName}"); err != nil {`, + ); + lines.push( + ` return fmt.Errorf("failed to remove --${field.flagName} from the OS keychain: %w", err)`, ); lines.push(` }`); } @@ -2779,16 +2795,8 @@ function genConfigureClear(): string[] { } lines.push(` case "${DEFAULT_OUTPUT_FORMAT_FLAG}":`); lines.push(` cfg.OutputFormat = ""`); - lines.push(` default:`); - lines.push( - ` return flagutil.WithCLIValidation(fmt.Errorf("invalid --${CONFIGURE_CLEAR_FLAG} %q; options: ${escapeGoString( - options, - )}", name))`, - ); lines.push(` }`); - lines.push( - ` cleared = append(cleared, strings.TrimPrefix(strings.TrimSpace(name), "--"))`, - ); + lines.push(` cleared = append(cleared, key)`); lines.push(` }`); lines.push(` changed = true`); lines.push(`}`); diff --git a/templates/templates/cli/tests/primary/configure_test.go.stmpl b/templates/templates/cli/tests/primary/configure_test.go.stmpl index 842204b66..e425d873c 100644 --- a/templates/templates/cli/tests/primary/configure_test.go.stmpl +++ b/templates/templates/cli/tests/primary/configure_test.go.stmpl @@ -3,6 +3,7 @@ package tests import ( "context" "encoding/json" + "errors" "os" "path/filepath" "testing" @@ -308,6 +309,59 @@ func TestConfigureNoInteractiveStoresFlags(t *testing.T) { assert.Contains(t, string(data), "username: flag-only-user") } +type deleteFailingKeyring struct{ recordingKeyring } + +func (k *deleteFailingKeyring) Delete(service, key string) error { + k.deletes++ + return errors.New("delete denied") +} + +func runConfigureClear(t *testing.T, backend config.KeyringBackend, clear string) (string, error) { + t.Helper() + h, home := setupConfigureTest(t) + config.SetKeyringBackend(backend) + t.Cleanup(config.ResetKeyring) + writeConfigFile(t, home, map[string]interface{}{ + "security": map[string]interface{}{"api_key_auth": "stored-key"}, + "globals": map[string]interface{}{"global_query_param": "stored-query"}, + }) + rootCmd, err := cli.NewRootCommand() + require.NoError(t, err) + rootCmd.SetOut(h.stdout) + rootCmd.SetErr(h.stderr) + runErr := cli.ExecuteRoot(context.Background(), rootCmd, []string{"configure", "--no-interactive", "--clear", clear}) + data, err := os.ReadFile(cliConfigPath(home)) + require.NoError(t, err) + return string(data), runErr +} + +func TestConfigureClearRemovesValues(t *testing.T) { + recorder := &recordingKeyring{} + data, err := runConfigureClear(t, recorder, "api-key-auth,--global-query-param") + require.NoError(t, err) + assert.NotContains(t, data, "stored-key") + assert.NotContains(t, data, "stored-query") + assert.Equal(t, 1, recorder.deletes) +} + +func TestConfigureClearValidatesEveryNameFirst(t *testing.T) { + recorder := &recordingKeyring{} + data, err := runConfigureClear(t, recorder, "api-key-auth,not-a-setting") + require.Error(t, err) + assert.Contains(t, err.Error(), `invalid --clear "not-a-setting"`) + assert.Contains(t, data, "stored-key") + assert.Zero(t, recorder.deletes) +} + +func TestConfigureClearReportsKeychainDeleteFailure(t *testing.T) { + backend := &deleteFailingKeyring{} + data, err := runConfigureClear(t, backend, "api-key-auth") + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to remove --api-key-auth from the OS keychain") + assert.Contains(t, data, "stored-key") + assert.Equal(t, 1, backend.deletes) +} + func TestLocalMutationCommandsDryRunAreNoOps(t *testing.T) { for _, tt := range []struct { name string diff --git a/zSDKs/sdk-cli/.speakeasy/gen.lock b/zSDKs/sdk-cli/.speakeasy/gen.lock index b106eb58b..921f549c7 100644 --- a/zSDKs/sdk-cli/.speakeasy/gen.lock +++ b/zSDKs/sdk-cli/.speakeasy/gen.lock @@ -66,7 +66,7 @@ trackedFiles: internal/cli/chat.go: last_write_checksum: sha1:9a9077f9bd0eb3ef19d36d461243215c50f3442b internal/cli/configure.go: - last_write_checksum: sha1:ee8e247c4e7da6ebbf9e20bc80bee879f3bc176a + last_write_checksum: sha1:66da0edca07db643976427521977046f4bca216c internal/cli/createuser.go: last_write_checksum: sha1:5b60000059db85dcf4c90f5e67c37b7fff3cf852 internal/cli/createwithunion.go: @@ -232,7 +232,7 @@ trackedFiles: internal/config/config.go: last_write_checksum: sha1:7d3e1555d3be75dcd615fb460f44bf1a4da784dd internal/config/keyring.go: - last_write_checksum: sha1:7c0201503038fee2f5eac51bc0f0189e06d3a224 + last_write_checksum: sha1:5bd269488f34aaabc3f9817dee887d75f6144cbb internal/explorer/explorer.go: last_write_checksum: sha1:c149087aa4be52cdf0b3f9a41d8f0c6dbe47c53f internal/explorer/styles.go: diff --git a/zSDKs/sdk-cli/internal/cli/configure.go b/zSDKs/sdk-cli/internal/cli/configure.go index f7ca06782..aac3f0a01 100644 --- a/zSDKs/sdk-cli/internal/cli/configure.go +++ b/zSDKs/sdk-cli/internal/cli/configure.go @@ -164,53 +164,62 @@ func runConfigureCmd(cmd *cobra.Command, args []string) error { } if f := cmd.Flags().Lookup("clear"); f != nil && f.Changed { names, _ := cmd.Flags().GetStringSlice("clear") + keys := make([]string, 0, len(names)) for _, name := range names { switch key := strings.TrimPrefix(strings.TrimSpace(name), "--"); key { + case "username", "password", "bearer-auth", "my-api-key", "oauth2", "app-id", "secret", "mobile-auth", "client-id", "client-secret", "token-url", "query-param1", "deprecated-query-param1", "deprecated-query-param2", "lone-query-param", "default-output-format": + keys = append(keys, key) + default: + return flagutil.WithCLIValidation(fmt.Errorf("invalid --clear %q; options: username, password, bearer-auth, my-api-key, oauth2, app-id, secret, mobile-auth, client-id, client-secret, token-url, query-param1, deprecated-query-param1, deprecated-query-param2, lone-query-param, default-output-format", name)) + } + } + for _, key := range keys { + switch key { case "username": cfg.Security.Username = "" case "password": - if config.KeyringAvailable() { - _ = config.DeleteKeyringValue("password") + if err := config.DeleteStoredSecret("password"); err != nil { + return fmt.Errorf("failed to remove --password from the OS keychain: %w", err) } cfg.Security.Password = "" case "bearer-auth": - if config.KeyringAvailable() { - _ = config.DeleteKeyringValue("bearer-auth") + if err := config.DeleteStoredSecret("bearer-auth"); err != nil { + return fmt.Errorf("failed to remove --bearer-auth from the OS keychain: %w", err) } cfg.Security.BearerAuth = "" case "my-api-key": - if config.KeyringAvailable() { - _ = config.DeleteKeyringValue("my-api-key") + if err := config.DeleteStoredSecret("my-api-key"); err != nil { + return fmt.Errorf("failed to remove --my-api-key from the OS keychain: %w", err) } cfg.Security.MyApiKey = "" case "oauth2": - if config.KeyringAvailable() { - _ = config.DeleteKeyringValue("oauth2") + if err := config.DeleteStoredSecret("oauth2"); err != nil { + return fmt.Errorf("failed to remove --oauth2 from the OS keychain: %w", err) } cfg.Security.Oauth2 = "" case "app-id": - if config.KeyringAvailable() { - _ = config.DeleteKeyringValue("app-id") + if err := config.DeleteStoredSecret("app-id"); err != nil { + return fmt.Errorf("failed to remove --app-id from the OS keychain: %w", err) } cfg.Security.AppId = "" case "secret": - if config.KeyringAvailable() { - _ = config.DeleteKeyringValue("secret") + if err := config.DeleteStoredSecret("secret"); err != nil { + return fmt.Errorf("failed to remove --secret from the OS keychain: %w", err) } cfg.Security.Secret = "" case "mobile-auth": - if config.KeyringAvailable() { - _ = config.DeleteKeyringValue("mobile-auth") + if err := config.DeleteStoredSecret("mobile-auth"); err != nil { + return fmt.Errorf("failed to remove --mobile-auth from the OS keychain: %w", err) } cfg.Security.MobileAuth = "" case "client-id": - if config.KeyringAvailable() { - _ = config.DeleteKeyringValue("client-id") + if err := config.DeleteStoredSecret("client-id"); err != nil { + return fmt.Errorf("failed to remove --client-id from the OS keychain: %w", err) } cfg.Security.ClientID = "" case "client-secret": - if config.KeyringAvailable() { - _ = config.DeleteKeyringValue("client-secret") + if err := config.DeleteStoredSecret("client-secret"); err != nil { + return fmt.Errorf("failed to remove --client-secret from the OS keychain: %w", err) } cfg.Security.ClientSecret = "" case "token-url": @@ -225,10 +234,8 @@ func runConfigureCmd(cmd *cobra.Command, args []string) error { cfg.Globals.LoneQueryParam = "" case "default-output-format": cfg.OutputFormat = "" - default: - return flagutil.WithCLIValidation(fmt.Errorf("invalid --clear %q; options: username, password, bearer-auth, my-api-key, oauth2, app-id, secret, mobile-auth, client-id, client-secret, token-url, query-param1, deprecated-query-param1, deprecated-query-param2, lone-query-param, default-output-format", name)) } - cleared = append(cleared, strings.TrimPrefix(strings.TrimSpace(name), "--")) + cleared = append(cleared, key) } changed = true } diff --git a/zSDKs/sdk-cli/internal/config/keyring.go b/zSDKs/sdk-cli/internal/config/keyring.go index 2c632fa8d..474a9960c 100644 --- a/zSDKs/sdk-cli/internal/config/keyring.go +++ b/zSDKs/sdk-cli/internal/config/keyring.go @@ -170,6 +170,18 @@ func DeleteKeyringValue(key string) error { return withKeyringTimeout(func() error { return backend.Delete(cliName, key) }) } +// DeleteStoredSecret removes a credential from the OS keychain when it is +// available. A credential that is not stored there is not an error. +func DeleteStoredSecret(key string) error { + if !KeyringAvailable() { + return nil + } + if err := DeleteKeyringValue(key); err != nil && !errors.Is(err, keyring.ErrNotFound) { + return err + } + return nil +} + // KeyringAvailable returns whether the OS keychain is accessible. // Useful for configure command to decide where to store secrets. func KeyringAvailable() bool { From c667a31b1d0cef6387256616039ec50c5f1b7eed Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Fri, 23 Oct 2026 12:28:20 +1000 Subject: [PATCH 4/4] fix(cli): treat an unresponsive keychain as skipped when clearing credentials --- .../cli/auxiliary/internal/config/keyring.go.stmpl | 10 ++++++---- zSDKs/sdk-cli/.speakeasy/gen.lock | 2 +- zSDKs/sdk-cli/internal/config/keyring.go | 10 ++++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/templates/templates/cli/auxiliary/internal/config/keyring.go.stmpl b/templates/templates/cli/auxiliary/internal/config/keyring.go.stmpl index ec42a4516..e9b0df996 100644 --- a/templates/templates/cli/auxiliary/internal/config/keyring.go.stmpl +++ b/templates/templates/cli/auxiliary/internal/config/keyring.go.stmpl @@ -171,15 +171,17 @@ func DeleteKeyringValue(key string) error { } // DeleteStoredSecret removes a credential from the OS keychain when it is -// available. A credential that is not stored there is not an error. +// available. A credential that is not stored there, or a keychain that stops +// responding (already reported as skipped), is not an error. func DeleteStoredSecret(key string) error { if !KeyringAvailable() { return nil } - if err := DeleteKeyringValue(key); err != nil && !errors.Is(err, keyring.ErrNotFound) { - return err + err := DeleteKeyringValue(key) + if err == nil || errors.Is(err, keyring.ErrNotFound) || errors.Is(err, ErrKeyringUnavailable) { + return nil } - return nil + return err } // KeyringAvailable returns whether the OS keychain is accessible. diff --git a/zSDKs/sdk-cli/.speakeasy/gen.lock b/zSDKs/sdk-cli/.speakeasy/gen.lock index 921f549c7..9a693a3a0 100644 --- a/zSDKs/sdk-cli/.speakeasy/gen.lock +++ b/zSDKs/sdk-cli/.speakeasy/gen.lock @@ -232,7 +232,7 @@ trackedFiles: internal/config/config.go: last_write_checksum: sha1:7d3e1555d3be75dcd615fb460f44bf1a4da784dd internal/config/keyring.go: - last_write_checksum: sha1:5bd269488f34aaabc3f9817dee887d75f6144cbb + last_write_checksum: sha1:893474c3d2734f82532552c53eb3782623933eb1 internal/explorer/explorer.go: last_write_checksum: sha1:c149087aa4be52cdf0b3f9a41d8f0c6dbe47c53f internal/explorer/styles.go: diff --git a/zSDKs/sdk-cli/internal/config/keyring.go b/zSDKs/sdk-cli/internal/config/keyring.go index 474a9960c..315edc9bc 100644 --- a/zSDKs/sdk-cli/internal/config/keyring.go +++ b/zSDKs/sdk-cli/internal/config/keyring.go @@ -171,15 +171,17 @@ func DeleteKeyringValue(key string) error { } // DeleteStoredSecret removes a credential from the OS keychain when it is -// available. A credential that is not stored there is not an error. +// available. A credential that is not stored there, or a keychain that stops +// responding (already reported as skipped), is not an error. func DeleteStoredSecret(key string) error { if !KeyringAvailable() { return nil } - if err := DeleteKeyringValue(key); err != nil && !errors.Is(err, keyring.ErrNotFound) { - return err + err := DeleteKeyringValue(key) + if err == nil || errors.Is(err, keyring.ErrNotFound) || errors.Is(err, ErrKeyringUnavailable) { + return nil } - return nil + return err } // KeyringAvailable returns whether the OS keychain is accessible.