From 9cb6243ec396c66fc8fa93561695936ffc3e70aa Mon Sep 17 00:00:00 2001 From: Jarrett Lusso Date: Wed, 7 Oct 2026 16:05:01 -0400 Subject: [PATCH] Warn on logout when EMAILABLE_API_KEY is still set Logout can't unset a variable in the parent shell, so an exported `EMAILABLE_API_KEY` kept authenticating every command after "Logged out." and logout looked broken. It now says so, as `gh auth logout` does for `GH_TOKEN`, and adds `api_key_env` to the JSON output. --- cmd/logout.go | 22 +++++++++++++++++++--- cmd/logout_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/cmd/logout.go b/cmd/logout.go index ebc7c97..10ceb50 100644 --- a/cmd/logout.go +++ b/cmd/logout.go @@ -1,6 +1,8 @@ package cmd import ( + "os" + "github.com/emailable/emailable-cli/internal/credentials" "github.com/emailable/emailable-cli/internal/output" "github.com/spf13/cobra" @@ -34,13 +36,27 @@ func runLogoutE(cmd *cobra.Command, _ []string) error { return err } + // A process can't unset its parent shell's variables, so an exported key + // keeps authenticating after logout; say so, as `gh auth logout` does. + envKey := os.Getenv(apiKeyEnv) != "" + if jsonOutput { - return newJSON(cmd.OutOrStdout()).Print(map[string]any{ + payload := map[string]any{ "logged_out": true, "message": "Logged out.", - }) + } + if envKey { + payload["api_key_env"] = true + } + return newJSON(cmd.OutOrStdout()).Print(payload) } h := &output.Human{W: cmd.OutOrStdout(), Quiet: ctx.Quiet} - return h.Success("Logged out.") + if err := h.Success("Logged out."); err != nil { + return err + } + if envKey { + return h.Hint("`EMAILABLE_API_KEY` is set, so commands still authenticate with it. Clear it from your environment to fully log out.") + } + return nil } diff --git a/cmd/logout_test.go b/cmd/logout_test.go index bb5eda5..ce8a9bd 100644 --- a/cmd/logout_test.go +++ b/cmd/logout_test.go @@ -79,3 +79,44 @@ func TestLogout_NoCredentials(t *testing.T) { t.Errorf("expected output to contain 'Logged out.', got %q", out.String()) } } + +// TestLogout_EnvKeyHint: an exported EMAILABLE_API_KEY outlives logout (a +// process can't unset its parent shell's variables), so logout says so. +func TestLogout_EnvKeyHint(t *testing.T) { + cases := []struct { + name string + envKey string + args []string + want string + absent string + }{ + {"human with env key", "sk_env", []string{"logout"}, "EMAILABLE_API_KEY is set, so commands still authenticate", ""}, + {"human without env key", "", []string{"logout"}, "Logged out.", "EMAILABLE_API_KEY"}, + {"json with env key", "sk_env", []string{"logout", "--json"}, `"api_key_env": true`, ""}, + {"json without env key", "", []string{"logout", "--json"}, `"logged_out": true`, "api_key_env"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Chdir(t.TempDir()) + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + t.Setenv("EMAILABLE_API_URL", "") + t.Setenv("EMAILABLE_OAUTH_URL", "") + t.Setenv("EMAILABLE_API_KEY", tc.envKey) + + root := newRootCmd("test") + var out bytes.Buffer + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs(tc.args) + if err := root.Execute(); err != nil { + t.Fatalf("execute: %v", err) + } + if !strings.Contains(out.String(), tc.want) { + t.Errorf("expected %q in output, got %q", tc.want, out.String()) + } + if tc.absent != "" && strings.Contains(out.String(), tc.absent) { + t.Errorf("did not expect %q in output, got %q", tc.absent, out.String()) + } + }) + } +}