# logout: add --all to drain every saved login (context)

`3940664`·

toothbrush·1mo ago·4 files·+350 added/-13 removed

\`entire logout --all\` iterates all saved contexts, revokes each context's
current session server-side against its own core (or every session on that
core when combined with --everywhere), and removes each login locally —
then clears the legacy keyring entry so the machine ends fully logged out.
Per-context failures warn but never abort the sweep; local removal always
proceeds. Adds auth.RemoveContext (named sibling of RemoveCurrentContext,
sharing a keychain-cleanup helper). Also fixes the stale --all reference in
the logout help left by the --all→--everywhere rename.

## Sessions

51b6330bf003View transcript

## Changes

4

- cmd/entire/cli

- auth

- Mcontext\_store.go+35/-8

- Mcontexts\_test.go+45

- Mlogout.go+98/-5

```
49 unmodified lines

// RemoveContext deletes the named context from contexts.json and its keyring
tokens. A missing context is a no-op. Used by `logout --all` to drain every
saved login. File.Delete clears current_context when name was the active
one, so removing the current context this way also logs it out.
func RemoveContext(name string) error {
var svc, handle string
if err := contexts.Modify(contexts.DefaultConfigDir(), func(f *contexts.File) (bool, error) {
c := f.Find(name)
if c == nil {
return false, nil
}
svc, handle = c.KeychainService, c.Handle
f.Delete(name)
return true, nil
}); err != nil {
return fmt.Errorf("remove context %q: %w", name, err)
}
deleteContextKeychain(svc, handle)
return nil
}

// deleteContextKeychain best-effort removes a context's keyring slots,
// sequenced off the context just removed from contexts.json. A missing entry
// is fine — the contexts.json removal is what makes us "logged out". Both the
// access slot and its paired refresh slot must go: leaving the long-lived
// refresh token behind would let any later keyring-capable process mint fresh
// access tokens after logout.
func deleteContextKeychain(svc, handle string) {
if svc == "" || handle == "" {
return
}
_ = tokenstore.Delete(svc, handle)                            //nolint:errcheck // best-effort; contexts.json removal is the source of truth for logout
_ = tokenstore.Delete(tokenstore.RefreshService(svc), handle) //nolint:errcheck // best-effort; absent refresh slot is fine
}

// SetCurrentContext makes name the active context. Returns an error when
// no context with that name exists (a stale current pointer is a foot-gun).
func SetCurrentContext(name string) error {

Mcmd/entire/cli/auth/context\_store.go+35/-8

```
303 unmodified lines

func TestRemoveContext(t *testing.T) {
cfgDir := t.TempDir()
t.Setenv("ENTIRE_CONFIG_DIR", cfgDir)
restore := tokenstore.UseFileBackendForTesting(filepath.Join(t.TempDir(), "tokens.json"))
t.Cleanup(restore)

exp := time.Now().Add(time.Hour).Unix()
first, err := RecordLoginContext(makeJWT(t, fmt.Sprintf(`{"iss":"https://a.example.com","handle":"alice","exp":%d}`, exp)), "entr_a", true)
if err != nil {
t.Fatalf("record a: %v", err)
}
active, err := RecordLoginContext(makeJWT(t, fmt.Sprintf(`{"iss":"https://b.example.com","handle":"alice","exp":%d}`, exp)), "entr_b", true)
if err != nil {
t.Fatalf("record b: %v", err)
}

// Remove the non-current context by name: it must disappear (both slots)
// while the active context and current_context pointer are untouched.
if err := RemoveContext(first); err != nil {
t.Fatalf("RemoveContext: %v", err)
}
f, err := contexts.Load(cfgDir)
if err != nil {
t.Fatalf("load: %v", err)
}
if f.Find(first) != nil {
t.Fatalf("context %q should have been removed", first)
}
if f.CurrentContext != active {
t.Fatalf("current_context = %q, want the untouched active context %q", f.CurrentContext, active)
}
svcA := tokenstore.CoreKeyringService("https://a.example.com")
if v, err := tokenstore.Get(svcA, "alice"); !errors.Is(err, tokenstore.ErrNotFound) {
t.Fatalf("access slot survived RemoveContext: value=%q err=%v", v, err)
}
if v, err := tokenstore.Get(tokenstore.RefreshService(svcA), "alice"); !errors.Is(err, tokenstore.ErrNotFound) {
t.Fatalf("refresh slot survived RemoveContext: value=%q err=%v", v, err)
}

// Idempotent: removing a name that no longer exists is a no-op.
if err := RemoveContext(first); err != nil {
t.Fatalf("second RemoveContext: %v", err)
}
}

// TestSetCurrentContext tests the setting of the current context.
func TestSetCurrentContext(t *testing.T) {
cfgDir := t.TempDir()
t.Setenv("ENTIRE_CONFIG_DIR", cfgDir)

Mcmd/entire/cli/auth/contexts\_test.go+45

```
7 unmodified lines

func newLogoutCmd() *cobra.Command {
var insecureHTTPAuth bool
var everywhere bool
var all bool
cmd := &cobra.Command{
Use:   "logout",
Short: "Log out of Entire",
RunE: func(cmd *cobra.Command, _ []string) error {
if err := requireSecureBaseURL(insecureHTTPAuth); err != nil {
return err
}
outW, errW := cmd.OutOrStdout(), cmd.ErrOrStderr()

// Pick the per-target revocation: just the current session, or
// every session on that context's core when --everywhere is set.
revokeForTarget := revokeCurrentSession
if everywhere {
revokeForTarget = revokeAllSessions
}

if all {
return runLogoutAll(cmd.Context(), outW, errW, auth.Contexts,
	auth.LoginTokenForContext, revokeForTarget, auth.RemoveContext,
	auth.NewContextStore(), api.AuthBaseURL(), insecureHTTPAuth)
}

// Revoke against the active context's core (matching what
// `auth status` lists), not a static AuthBaseURL.
target := resolveStatusTarget(auth.NewContextStore(), auth.Contexts, api.AuthBaseURL())

if err := runLogout(cmd.Context(), outW, errW,
	auth.NewContextStore(), revokeCurrent, revokeAll,
	auth.RemoveCurrentContext, api.AuthBaseURL(), everywhere); err != nil {
return err
}

fmt.Fprintln(outW, "Logged out.")
return nil
}
}

// runLogoutAll drains every saved login. For each context it revokes the
// session(s) on that context's own core (using its own bearer) and removes
the login locally, then clears the legacy keyring entry. Per-context
// failures warn but never abort the sweep — one stuck login can't strand the
// rest, and local removal always proceeds so the CLI ends fully logged out.
