# auth: remove `auth revoke`; redefine `logout --all` to revoke sessions

`7c8e04a`·

toothbrush·1mo ago·5 files·+126 added/-282 removed

Delete the `entire auth revoke` command. Session management collapses to two verbs: `auth status` shows active sessions, `logout` ends them.

Redefine the `logout --all` flag — it no longer removes all *local* contexts. Instead:

- `logout` revokes the active session server-side (DELETE .../tokens/current) and removes the active context locally. (Unchanged default behaviour.)
- `logout --all` additionally asks the server to revoke *every* session on the active core (list families -> delete each by id).
The local side is identical to the default.

After a logout clears the active context, the next saved context is promoted to active, so running `entire logout` repeatedly drains every saved login in turn.

Cross-core revoke is out of scope: these endpoints target AuthBaseURL's core only, pending the COR-389 control-plane retargeting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

## Sessions

9dc3b1c4312cView transcript

## Changes

5

- cmd/entire/cli

- Mauth.go+2/-97

- Mauth_context_test.go+26/-9

- Mauth_test.go+1/-141

- Mlogout.go+59/-28

- Mlogout_test.go+38/-7

```go
26 unmodified lines

// wrong-audience keyring token.
type authTokenLister func(ctx context.Context) ([]api.Token, error)

// authTokenRevoker revokes a single API token by id. Same bearer-
// resolution contract as authTokenLister.
type authTokenRevoker func(ctx context.Context, id string) error

// User-visible placeholder strings. Promoted to constants so tests and
// production share a single source of truth.
const (

func newAuthCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "auth",
		Short: "Manage authentication and API tokens",
		Long:  "Authentication subcommands. Includes login, logout, status, listing tokens, and revoking tokens.",
		Short: "Manage authentication",
		Long:  "Authentication subcommands. Includes login, logout, status, and login-context management (contexts, use).",
		RunE: func(cmd *cobra.Command, _ []string) error {
			return cmd.Help()
		},

}
	cmd.AddCommand(newLoginCmd())
	cmd.AddCommand(newLogoutCmd())
	cmd.AddCommand(newAuthStatusCmd())
	cmd.AddCommand(newAuthRevokeCmd())
	cmd.AddCommand(newAuthContextsCmd())
	cmd.AddCommand(newAuthUseCmd())
	return cmd
}
```

```go
// --- revoke -----------------------------------------------------------------

func newAuthRevokeCmd() *cobra.Command {
	var revokeCurrent bool
	var insecureHTTPAuth bool
	cmd := &cobra.Command{
		Use:   "revoke [id]",
		Short: "Revoke an API token by id",
		Long:  "Revoke a specific API token. Use --current to revoke the token used by this CLI (equivalent to 'entire logout').",
		Args:  cobra.MaximumNArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			id := ""
			if len(args) == 1 {
				id = args[0]
			}
			if id == "" && !revokeCurrent {
				return cmd.Help()
			}
			if id != "" && revokeCurrent {
				return errors.New("cannot use both <id> and --current")
			}
			if err := requireSecureBaseURL(insecureHTTPAuth); err != nil {
				return err
			}
			return runAuthRevoke(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(),
				auth.NewContextStore(), defaultListTokens, defaultRevokeTokenByID, defaultRevokeCurrentToken,
				auth.RemoveCurrentContext, api.AuthBaseURL(), id, revokeCurrent)
		},
	}
	cmd.Flags().BoolVar(&revokeCurrent, "current", false, "Revoke the token used by this CLI and remove the local copy")
	addInsecureHTTPAuthFlag(cmd, &insecureHTTPAuth)
	return cmd
}
```

```go
func defaultRevokeTokenByID(ctx context.Context, id string) error {
	token, err := resolveDataAPIToken(ctx)
	if err != nil {
		return err
	}
	return newAPITokensClient(token).RevokeToken(ctx, id) //nolint:wrapcheck // RevokeToken already wraps with action context
}

func runAuthRevoke(
	ctx context.Context,
	outW, errW io.Writer,
	store tokenStore,
	list authTokenLister,
	revokeByID authTokenRevoker,
	revokeCurrent revokeCurrentFunc,
	clearContext clearContextFunc,
	baseURL, id string,
	current bool,
) error {
	token, err := store.GetToken(baseURL)
	if err != nil {
		return fmt.Errorf("read keychain: %w", err)
	}
	if token == "" {
		return fmt.Errorf("not logged in to %s; run 'entire login' first", baseURL)
	}

if current {
		// Revoking our own token is just logout — reuse that path so behavior
		// stays identical (best-effort revoke + local delete + context clear).
		return runLogout(ctx, outW, errW, store, revokeCurrent, clearContext, baseURL)
	}

if err := revokeByID(ctx, id); err != nil {
		return err
	}

// The list endpoint requires bearer auth, so a 401 here means the id we
	// just revoked was the same one this CLI is using — the local copy is now
	// stale and would otherwise produce confusing 401s on every command, so
	// remove both the legacy keyring entry and the active context.
	if _, listErr := list(ctx); listErr != nil && api.IsHTTPErrorStatus(listErr, http.StatusUnauthorized) {
		if delErr := store.DeleteToken(baseURL); delErr != nil {
			return fmt.Errorf("revoked token %s but failed to remove local copy: %w", id, delErr)
		}
		if ctxErr := clearContext(); ctxErr != nil {
			fmt.Fprintf(errW, "Warning: revoked token %s but failed to clear current context: %v\n", id, ctxErr)
		}
		fmt.Fprintf(outW, "Revoked token %s (this was your local token; removed from keychain).\n", id)
		return nil
	}

fmt.Fprintf(outW, "Revoked token %s.\n", id)
	return nil
}
```

### Test Cases

```go
func TestRunAuthRevoke_ByIDCallsRevoker(t *testing.T) {
	t.Parallel()

store := newMockTokenStore()
	store.tokens[testBaseURL] = testAuthTok

var gotID string
	revokeByID := func(_ context.Context, id string) error {
		gotID = id
		return nil
	}

revokeCurrentCalled := false
	revokeCurrent := func(context.Context) error {
		revokeCurrentCalled = true
		return nil
	}

// list returns 200 → token id was someone else's, no local cleanup expected.
	list := func(context.Context) ([]api.Token, error) {
		return []api.Token{{ID: "other"}}, nil
	}

var out, errOut bytes.Buffer
	err := runAuthRevoke(context.Background(), &out, &errOut, store,
		list, revokeByID, revokeCurrent, func() error { return nil }, testBaseURL, testTokenID, false)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

if revokeCurrentCalled {
		t.Fatal("revokeCurrent should not be called when revoking by id")
	}
	if gotID != testTokenID {
		t.Errorf("revokeByID called with id=%q, want %q", gotID, testTokenID)
	}
	if store.deleted[testBaseURL] {
		t.Fatal("local token should NOT be deleted when revoking another token")
	}
	if !strings.Contains(out.String(), "Revoked token "+testTokenID) {
		t.Fatalf("output = %q, want confirmation", out.String())
	}
}
```
