# auth: rename session identifiers to AuthSession forms

`5b21199`→[main](/content/gh/entireio/cli/commits/main/index.html)·  
  
toothbrush·1mo ago·7 files·+97 added/-97 removed

Apply the Auth-prefix renames across the moved files and call sites:  
api.Session -> AuthSession, ListSessions -> ListAuthSessions, etc., plus  
cli helpers (newAuthSessionsClient, defaultListAuthSessions, revokeAllAuthSessions, ...). JSON wire key stays "tokens".

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

## Sessions

c6c44b763a39View transcript

## Changes

7

- cmd/entire/cli

- api

- Mauth_sessions.go+22/-22

- Mauth_sessions_test.go+21/-21

- Mclient.go+8/-8

- Mauth.go+21/-21

- Mauth_test.go+6/-6

- Mlogout.go+15/-15

- Mlogout_test.go+4/-4

```
6 unmodified lines

7
8
9
10
10
11
12
13
14
15
15
16
17
18
3 unmodified lines

22
23
24
25
26
27
25
26
27
28
29
30
30
31
32
33
32
33
34
35
36
37
35
36
37
38
39
39
40
41
42
43
44
42
43
44
45
46
47
7 unmodified lines

55
56
57
58
58
59
60
61
62
63
64
65
65
66
67
68
67
68
69
70
71
9 unmodified lines

81
82
83
84
85
86
84
85
86
87
88
89

6 unmodified lines

"net/url"
// Session is a single active login session — an OAuth refresh-token family —
// AuthSession is a single active login session — an OAuth refresh-token family —
// returned by entire-core's session endpoint. One is created per
// `entire login`, across all of a user's devices. Plaintext token values are
// never returned by the server, only metadata. (The list envelope's wire key
// is "tokens"; the rows are sessions.)
type Session struct {
type AuthSession struct {
	ID         string  `json:"id"`
	UserID     string  `json:"user_id"`
	Name       string  `json:"name"`
3 unmodified lines

CreatedAt  string  `json:"created_at"`
}
}

// SessionsResponse is the envelope returned by the list endpoint.
type SessionsResponse struct {
	Sessions []Session `json:"tokens"`
// AuthSessionsResponse is the envelope returned by the list endpoint.
type AuthSessionsResponse struct {
	Sessions []AuthSession `json:"tokens"`
}

// errSessionsPathUnset surfaces when a session method is called on a Client
// errAuthSessionsPathUnset surfaces when a session method is called on a Client
// that wasn't given a base path. Construct via
// NewClientWithBaseURL(...).WithSessionsPath(...).
var errSessionsPathUnset = errors.New("api: sessions path is unset (call (*Client).WithSessionsPath before list/revoke)")
// NewClientWithBaseURL(...).WithAuthSessionsPath(...).
var errAuthSessionsPathUnset = errors.New("api: auth sessions path is unset (call (*Client).WithAuthSessionsPath before list/revoke)")

func (c *Client) sessionsBasePath() (string, error) {
	if c.sessionsPath == "" {
		return "", errSessionsPathUnset
func (c *Client) authSessionsBasePath() (string, error) {
	if c.authSessionsPath == "" {
		return "", errAuthSessionsPathUnset
	}
	return c.sessionsPath, nil
	return c.authSessionsPath, nil
}

// ListSessions returns the authenticated user's active login sessions.
func (c *Client) ListSessions(ctx context.Context) ([]Session, error) {
	base, err := c.sessionsBasePath()
// ListAuthSessions returns the authenticated user's active login sessions.
func (c *Client) ListAuthSessions(ctx context.Context) ([]AuthSession, error) {
	base, err := c.authSessionsBasePath()
	if err != nil {
		return nil, fmt.Errorf("list sessions: %w", err)
	}

var out SessionsResponse
	var out AuthSessionsResponse
	if err := DecodeJSON(resp, &out); err != nil {
		return nil, fmt.Errorf("list sessions: %w", err)
	}
	return out.Sessions, nil
}

// RevokeCurrentSession revokes the login session this client is authenticating
// RevokeCurrentAuthSession revokes the login session this client is authenticating
// with (the family the current bearer belongs to).
func (c *Client) RevokeCurrentSession(ctx context.Context) error {
	base, err := c.sessionsBasePath()
func (c *Client) RevokeCurrentAuthSession(ctx context.Context) error {
	base, err := c.authSessionsBasePath()
	if err != nil {
		return fmt.Errorf("revoke current session: %w", err)
	}

// RevokeSession revokes the login session with the given id.
	func (c *Client) RevokeSession(ctx context.Context, id string) error {
		base, err := c.sessionsBasePath()
// RevokeAuthSession revokes the login session with the given id.
	func (c *Client) RevokeAuthSession(ctx context.Context, id string) error {
		base, err := c.authSessionsBasePath()
		if err != nil {
			return fmt.Errorf("revoke session %s: %w", id, err)
		}
}

Mcmd/entire/cli/api/auth_sessions.go+22/-22

```
8 unmodified lines

9
10
11
12
12
13
14
15
7 unmodified lines

23
24
25
26
26
27
28
29
30
29
30
31
32
33
7 unmodified lines

41
42
43
44
44
45
46
47
3 unmodified lines

51
52
53
54
54
55
56
57
57
58
59
60
9 unmodified lines

70
71
72
73
73
74
75
76
10 unmodified lines

87
88
89
90
90
91
92
93
93
94
95
95
96
97
98
20 unmodified lines

119
120
121
122
122
123
124
125
3 unmodified lines

129
130
131
132
132
133
134
135
135
136
137
138
2 unmodified lines

141
142
143
144
144
145
146
147
7 unmodified lines

155
156
157
158
158
159
160
161
162
163
162
163
164
165
166
7 unmodified lines

174
175
176
177
177
178
179
180
3 unmodified lines

184
185
186
187
187
188
189
190
190
191
192
193

8 unmodified lines

testing
)

func TestClient_RevokeCurrentSession_SendsDeleteWithBearer(t *testing.T) {
func TestClient_RevokeCurrentAuthSession_SendsDeleteWithBearer(t *testing.T) {
	t.Parallel()

var gotMethod, gotPath, gotAuth string
7 unmodified lines

}))
	defer server.Close()

c := NewClient("tok").WithSessionsPath("/api/auth/tokens")
	c := NewClient("tok").WithAuthSessionsPath("/api/auth/tokens")
	c.baseURL = server.URL

if err := c.RevokeCurrentSession(context.Background()); err != nil {
		t.Fatalf("RevokeCurrentSession() error = %v", err)
	if err := c.RevokeCurrentAuthSession(context.Background()); err != nil {
		t.Fatalf("RevokeCurrentAuthSession() error = %v", err)
	}

if gotMethod != http.MethodDelete {
7 unmodified lines

}

func TestClient_RevokeCurrentSession_ReturnsHTTPErrorOn401(t *testing.T) {
func TestClient_RevokeCurrentAuthSession_ReturnsHTTPErrorOn401(t *testing.T) {
	t.Parallel()

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
3 unmodified lines

}))
	defer server.Close()

c := NewClient("tok").WithSessionsPath("/api/auth/tokens")
	c := NewClient("tok").WithAuthSessionsPath("/api/auth/tokens")
	c.baseURL = server.URL

err := c.RevokeCurrentSession(context.Background())
	err := c.RevokeCurrentAuthSession(context.Background())
	if err == nil {
		t.Fatal("expected error for 401 response")
	}
}

func TestClient_ListSessions_DecodesResponse(t *testing.T) {
func TestClient_ListAuthSessions_DecodesResponse(t *testing.T) {
	t.Parallel()

var gotMethod, gotPath, gotAuth string
10 unmodified lines

}))
	defer server.Close()

c := NewClient("tok").WithSessionsPath("/api/auth/tokens")
	c := NewClient("tok").WithAuthSessionsPath("/api/auth/tokens")
	c.baseURL = server.URL

tokens, err := c.ListSessions(context.Background())
	tokens, err := c.ListAuthSessions(context.Background())
	if err != nil {
		t.Fatalf("ListSessions() error = %v", err)
		t.Fatalf("ListAuthSessions() error = %v", err)
	}

if gotMethod != http.MethodGet {
20 unmodified lines

}

func TestClient_ListSessions_ReturnsHTTPErrorOn401(t *testing.T) {
func TestClient_ListAuthSessions_ReturnsHTTPErrorOn401(t *testing.T) {
	t.Parallel()

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
3 unmodified lines

}))
	defer server.Close()

c := NewClient("tok").WithSessionsPath("/api/auth/tokens")
	c := NewClient("tok").WithAuthSessionsPath("/api/auth/tokens")
	c.baseURL = server.URL

_, err := c.ListSessions(context.Background())
	_, err := c.ListAuthSessions(context.Background())
	if err == nil {
		t.Fatal("expected error for 401")
	}
}

func TestClient_RevokeSession_SendsDeleteWithEscapedID(t *testing.T) {
func TestClient_RevokeAuthSession_SendsDeleteWithEscapedID(t *testing.T) {
	t.Parallel()

var gotMethod, gotEscapedPath, gotDecodedPath string
7 unmodified lines

}))
	defer server.Close()

c := NewClient("tok").WithSessionsPath("/api/auth/tokens")
	c := NewClient("tok").WithAuthSessionsPath("/api/auth/tokens")
	c.baseURL = server.URL

// Use an id that needs URL escaping to verify we don't blindly concat.
	if err := c.RevokeSession(context.Background(), "abc/def 1"); err != nil {
		t.Fatalf("RevokeSession() error = %v", err)
	if err := c.RevokeAuthSession(context.Background(), "abc/def 1"); err != nil {
		t.Fatalf("RevokeAuthSession() error = %v", err)
	}

if gotMethod != http.MethodDelete {
7 unmodified lines

}

func TestClient_RevokeSession_ReturnsErrorBody(t *testing.T) {
func TestClient_RevokeAuthSession_ReturnsErrorBody(t *testing.T) {
	t.Parallel()

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
3 unmodified lines

}))
	defer server.Close()

c := NewClient("tok").WithSessionsPath("/api/auth/tokens")
	c := NewClient("tok").WithAuthSessionsPath("/api/auth/tokens")
	c.baseURL = server.URL

err := c.RevokeSession(context.Background(), "missing")
	err := c.RevokeAuthSession(context.Background(), "missing")
	if err == nil {
		t.Fatal("expected error for 404")
	}
```

Mcmd/entire/cli/api/auth_sessions_test.go+21/-21

```
21 unmodified lines

22
23
24
25
26
25
26
27
28
29
29
30
31
32
33
32
33
34
35
36
37
38
36
37
38
39
40
41

21 unmodified lines

httpClient *http.Client
	baseURL    string

// sessionsPath is the base path for entire-core's login-session
	// endpoints (list / revoke / current). Set via WithSessionsPath when the
	// authSessionsPath is the base path for entire-core's login-session
	// endpoints (list / revoke / current). Set via WithAuthSessionsPath when the
	// client targets the auth host; empty otherwise, and the session methods
	// error out if called against an empty path.
sessionsPath string
	authSessionsPath string
}

// WithSessionsPath sets the base path used by ListSessions,
// RevokeCurrentSession, and RevokeSession. Returns the receiver for chaining
// WithAuthSessionsPath sets the base path used by ListAuthSessions,
// RevokeCurrentAuthSession, and RevokeAuthSession. Returns the receiver for chaining
// at construction:
//
//	c := api.NewClientWithBaseURL(token, base).WithSessionsPath(p)
func (c *Client) WithSessionsPath(path string) *Client {
	c.sessionsPath = path
//	c := api.NewClientWithBaseURL(token, base).WithAuthSessionsPath(p)
func (c *Client) WithAuthSessionsPath(path string) *Client {
	c.authSessionsPath = path
	return c
}
```

Mcmd/entire/cli/api/client.go+8/-8

```
17 unmodified lines

18
19
20
21
21
22
23
24
25
26
26
27
28
29
36 unmodified lines

66
67
68
69
70
69
70
71
72
73
74
75
74
75
76
77
78
96 unmodified lines

175
176
177
178
178
179
180
181
14 unmodified lines

196
197
198
199
199
200
201
202
201
202
203
204
205
60 unmodified lines

266
267
268
269
270
271
269
270
271
272
273
274
275
276
277
278
278
279
280
281
24 unmodified lines

306
307
308
309
309
310
311
311
312
313
314
102 unmodified lines

417
418
419
420
420
421
422
423
423
424
425
426
12 unmodified lines

439
440
441
442
442
443
444
445
445
446
447
448
6 unmodified lines

455
456
457
458
458
459
460
461

17 unmodified lines

"github.com/spf13/cobra"
// coreSessionsPath is entire-core's login-session endpoint family
// coreAuthSessionsPath is entire-core's login-session endpoint family
// (list / revoke / current) on the auth host. Sessions are OAuth
// refresh-token families; the CLI authenticates against them with its core
// JWT. Session management must target the auth host (entire-core), never the
// data host.
const coreSessionsPath = "/api/auth/tokens"
const coreAuthSessionsPath = "/api/auth/tokens"

// User-visible placeholder strings. lastUsedJustNow is consumed by
// formatRelativeDuration in status.go.
36 unmodified lines

return nil
}
// newSessionsClient builds an api.Client for entire-core's login-session
// endpoints (coreSessionsPath) on coreURL, authenticated with the
// newAuthSessionsClient builds an api.Client for entire-core's login-session
// endpoints (coreAuthSessionsPath) on coreURL, authenticated with the
// session-scoped login JWT. coreURL is the active context's CoreURL (or the
// configured auth host when no context is active) — session management always
// targets a login server, never the data host.
func newSessionsClient(coreURL, token string) *api.Client {
	return api.NewClientWithBaseURL(token, coreURL).WithSessionsPath(coreSessionsPath)
func newAuthSessionsClient(coreURL, token string) *api.Client {
	return api.NewClientWithBaseURL(token, coreURL).WithAuthSessionsPath(coreAuthSessionsPath)
}
// resolveAuthHostToken returns a bearer scoped for the auth host (entire-core).
96 unmodified lines

return fmt.Errorf("context core URL check: %w", err)
		}
	}
	return runAuthStatus(cmd.Context(), cmd.OutOrStdout(), defaultFetchProfile, defaultListSessions, target)
	return runAuthStatus(cmd.Context(), cmd.OutOrStdout(), defaultFetchProfile, defaultListAuthSessions, target)
	},
}
	addInsecureHTTPAuthFlag(cmd, &insecureHTTPAuth)
// with token. Injected so status stays unit-testable without a live core.
type profileFetcher func(ctx context.Context, coreURL, token string) (*authProfile, error)
// sessionLister lists the active login sessions on coreURL (the user's
// authSessionLister lists the active login sessions on coreURL (the user's
// refresh-token families). Injected for testability; production wires
// defaultListSessions.
type sessionLister func(ctx context.Context, coreURL, token string) ([]api.Session, error)
// defaultListAuthSessions.
type authSessionLister func(ctx context.Context, coreURL, token string) ([]api.AuthSession, error)
// contextsProvider returns the stored login contexts and the active context
// name. Injected for testability; production wires auth.Contexts.
60 unmodified lines

return p, nil
// defaultListSessions lists the user's active login sessions on coreURL.
func defaultListSessions(ctx context.Context, coreURL, token string) ([]api.Session, error) {
	return newSessionsClient(coreURL, token).ListSessions(ctx) //nolint:wrapcheck // ListSessions already wraps with action context
// defaultListAuthSessions lists the user's active login sessions on coreURL.
func defaultListAuthSessions(ctx context.Context, coreURL, token string) ([]api.AuthSession, error) {
	return newAuthSessionsClient(coreURL, token).ListAuthSessions(ctx) //nolint:wrapcheck // ListSessions already wraps with action context
// runAuthStatus reports auth state against the target core: GET /me validates
// the token and supplies the profile header, the active login context is shown
// locally, and the active sessions (refresh-token families) on that core are
// listed so the effect of `logout` / `logout --everywhere` is visible.
func runAuthStatus(ctx context.Context, w io.Writer, fetchProfile profileFetcher, listSessions sessionLister, t statusTarget) error {
func runAuthStatus(ctx context.Context, w io.Writer, fetchProfile profileFetcher, listSessions authSessionLister, t statusTarget) error {
	if t.token == "" {
		fmt.Fprintf(w, "Not logged in to %s\n", t.coreURL)
		fmt.Fprintln(w, "Run 'entire login' to authenticate.")
	}
	
	case serr != nil:
		fmt.Fprintf(w, "\n(could not list active sessions: %v)\n", serr)
	case len(sessions) > 0:
		sortSessionsByRecency(sessions)
		sortAuthSessionsByRecency(sessions)
		fmt.Fprintf(w, "\nActive sessions (%d):\n", len(sessions))
		renderSessionsTable(w, newAuthTableStyles(w), sessions)
		renderAuthSessionsTable(w, newAuthTableStyles(w), sessions)
		fmt.Fprintln(w, "\nRun 'entire logout' to end this session, or 'entire logout --everywhere' to end all of them.")
	}

102 unmodified lines

return s
// renderSessionsTable prints the active login sessions as an aligned table.
// renderAuthSessionsTable prints the active login sessions as an aligned table.
// No id column: there's no per-session CLI action (revoke-by-id is gone), so
// NAME/CREATED/LAST USED/EXPIRES is what's useful.
func renderSessionsTable(w io.Writer, sty authTableStyles, sessions []api.Session) {
func renderAuthSessionsTable(w io.Writer, sty authTableStyles, sessions []api.AuthSession) {
	header := []string{
		sty.render(sty.header, "NAME"),
		sty.render(sty.header, "CREATED"),
12 unmodified lines

renderAlignedTable(w, header, rows)
// sortSessionsByRecency orders sessions most-recently-used first, then most
// sortAuthSessionsByRecency orders sessions most-recently-used first, then most
// recently created, then by id — a fully specified order independent of the
// server's response ordering.
func sortSessionsByRecency(sessions []api.Session) {
func sortAuthSessionsByRecency(sessions []api.AuthSession) {
	sort.Slice(sessions, func(i, j int) bool {
		li, lj := lastUsedSortKey(sessions[i]), lastUsedSortKey(sessions[j])
		if li != lj {
6 unmodified lines
}

func lastUsedSortKey(s api.Session) string {
func lastUsedSortKey(s api.AuthSession) string {
	if s.LastUsedAt == nil {
		return ""
	}
```

Mcmd/entire/cli/auth.go+21/-21

```
47 unmodified lines

48
49
50
51
52
51
52
53
54
55
42 unmodified lines

98
99
100
101
101
102
103
104
105
105
106
107
108
21 unmodified lines

130
131
132
133
133
134
135
136
140 unmodified lines

277
278
279
280
280
281
282
283

47 unmodified lines

return func(context.Context, string, string) (*authProfile, error) { return nil, err }
// noSessions is a sessionLister returning an empty list (no table rendered).
func noSessions(context.Context, string, string) ([]api.Session, error) { return nil, nil }
// noSessions is a authSessionLister returning an empty list (no table rendered).
func noSessions(context.Context, string, string) ([]api.AuthSession, error) { return nil, nil }

func TestRunAuthStatus_NotLoggedIn(t *testing.T) {
	t.Parallel()
42 unmodified lines
	target := statusTarget{coreURL: testCoreURL, token: "tok", activeContext: "eu.auth.entire.io", totalContexts: 1}
	lastUsed := "2026-05-01T00:00:00Z"
	listSessions := func(_ context.Context, coreURL, token string) {
	listSessions := func(_ context.Context, coreURL, token string) {
		if coreURL != testCoreURL || token != "tok" {
			t.Errorf("listSessions called with (%q, %q), want the active core+token", coreURL, token)
		}
		return []api.Session{
		return []api.AuthSession{
			{ID: "fam-1", Name: "OIDC login", CreatedAt: "2026-01-01T00:00:00Z", ExpiresAt: "2026-12-01T00:00:00Z", LastUsedAt: &lastUsed},
			{ID: "fam-2", Name: "OIDC login", CreatedAt: "2026-02-01T00:00:00Z", ExpiresAt: "2026-12-15T00:00:00Z"},
		}, nil
21 unmodified lines
	}
	t.Parallel()

target := statusTarget{coreURL: testCoreURL, token: "tok", activeContext: "eu.auth.entire.io", totalContexts: 1}
	listSessions := func(context.Context, string, string) ([]api.Session, error) {
	listSessions := func(context.Context, string, string) ([]api.AuthSession, error) {
		return nil, errors.New("sessions endpoint unreachable")
	}

140 unmodified lines

// tokenmanager.Manager via auth.SetManagerForTest and stub only the
// STS wire call via SetExchangeForTest. That covers the audience-
// matching logic the function-injection tests above can't reach
// (defaultRevokeCurrentSession / defaultRevokeAllSessions call
// (revokeCurrentAuthSession / revokeAllAuthSessions call
// resolveAuthHostToken directly, but unit tests for the surrounding flows
// inject fakes that bypass it).
```

Mcmd/entire/cli/logout.go+15/-15

```
465 unmodified lines

466
467
468
469
469
470
471
472
472
473
474
475
474
475
476
477
478

465 unmodified lines

rec.mu.Lock()
	defer rec.mu.Unlock()
	switch {
	case r.Method == http.MethodGet && r.URL.Path == coreSessionsPath:
	case r.Method == http.MethodGet && r.URL.Path == coreAuthSessionsPath:
		rec.listCount++
		fmt.Fprint(w, `{"tokens":[{"id":"s1"},{"id":"s2"}]}`)
	case r.Method == http.MethodDelete && r.URL.Path == coreSessionsPath+"/current":
	case r.Method == http.MethodDelete && r.URL.Path == coreAuthSessionsPath+"/current":
		rec.deleteCurrent++
	case r.Method == http.MethodDelete && strings.HasPrefix(r.URL.Path, coreSessionsPath+"/"):
		rec.deleteByID = append(rec.deleteByID, strings.TrimPrefix(r.URL.Path, coreSessionsPath+"/"))
	case r.Method == http.MethodDelete && strings.HasPrefix(r.URL.Path, coreAuthSessionsPath+"/"):
		rec.deleteByID = append(rec.deleteByID, strings.TrimPrefix(r.URL.Path, coreAuthSessionsPath+"/"))
	default:
		w.WriteHeader(http.StatusNotFound)
	}
